added NSProgress support for migrations

This commit is contained in:
John Rommel Estropia
2015-07-12 00:49:53 +09:00
parent 33268eeea1
commit 8cfe8e2500
15 changed files with 729 additions and 406 deletions

View File

@@ -62,6 +62,8 @@
B5E84F381AFF85470064E85B /* NSManagedObject+Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */; };
B5E84F391AFF85470064E85B /* NSManagedObjectContext+Querying.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */; };
B5E84F411AFF8CCD0064E85B /* ClauseTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E84F401AFF8CCD0064E85B /* ClauseTypes.swift */; };
B5FAD6A91B50A4B400714891 /* NSProgress+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FAD6A81B50A4B300714891 /* NSProgress+Convenience.swift */; };
B5FAD6AC1B51285300714891 /* MigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FAD6AB1B51285300714891 /* MigrationManager.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -158,6 +160,8 @@
B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Transaction.swift"; sourceTree = "<group>"; };
B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Querying.swift"; sourceTree = "<group>"; };
B5E84F401AFF8CCD0064E85B /* ClauseTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClauseTypes.swift; sourceTree = "<group>"; };
B5FAD6A81B50A4B300714891 /* NSProgress+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSProgress+Convenience.swift"; sourceTree = "<group>"; };
B5FAD6AB1B51285300714891 /* MigrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationManager.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -383,6 +387,7 @@
isa = PBXGroup;
children = (
B5E84F271AFF84920064E85B /* NSManagedObject+Convenience.swift */,
B5FAD6A81B50A4B300714891 /* NSProgress+Convenience.swift */,
);
path = "Convenience Helpers";
sourceTree = "<group>";
@@ -391,13 +396,14 @@
isa = PBXGroup;
children = (
B5E84F2A1AFF849C0064E85B /* AssociatedObjects.swift */,
B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */,
B5E84F2B1AFF849C0064E85B /* NotificationObserver.swift */,
B5FAD6AB1B51285300714891 /* MigrationManager.swift */,
B5E84F341AFF85470064E85B /* NSManagedObject+Transaction.swift */,
B5E84F2C1AFF849C0064E85B /* NSManagedObjectContext+CoreStore.swift */,
B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */,
B5E84F321AFF85470064E85B /* NSManagedObjectContext+Setup.swift */,
B5E84F331AFF85470064E85B /* NSManagedObjectContext+Transaction.swift */,
B51BE0691B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift */,
B5E84F2D1AFF849C0064E85B /* WeakObject.swift */,
);
path = Internal;
@@ -558,6 +564,7 @@
2F291E2719C6D3CF007AF63F /* CoreStore.swift in Sources */,
B5E84F411AFF8CCD0064E85B /* ClauseTypes.swift in Sources */,
B5E84F0D1AFF847B0064E85B /* BaseDataTransaction+Querying.swift in Sources */,
B5FAD6AC1B51285300714891 /* MigrationManager.swift in Sources */,
B5E84EF61AFF846E0064E85B /* DataStack+Transaction.swift in Sources */,
B5E84EDF1AFF84500064E85B /* DataStack.swift in Sources */,
B5E84F231AFF84860064E85B /* ListMonitor.swift in Sources */,
@@ -566,6 +573,7 @@
B5E84EF51AFF846E0064E85B /* BaseDataTransaction.swift in Sources */,
B5E84EFB1AFF846E0064E85B /* SaveResult.swift in Sources */,
B5E84F0F1AFF847B0064E85B /* From.swift in Sources */,
B5FAD6A91B50A4B400714891 /* NSProgress+Convenience.swift in Sources */,
B5E84EFC1AFF846E0064E85B /* SynchronousDataTransaction.swift in Sources */,
B5E84F281AFF84920064E85B /* NSManagedObject+Convenience.swift in Sources */,
B51BE06A1B47FC4B0069F532 /* NSManagedObjectModel+Setup.swift in Sources */,

View File

@@ -0,0 +1,112 @@
//
// NSProgress+Convenience.swift
// CoreStore
//
// Copyright (c) 2015 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
import GCDKit
// MARK: - NSProgress
public extension NSProgress {
// MARK: Public
public func setProgressHandler(closure: ((progress: NSProgress) -> Void)?) {
self.progressObserver.progressHandler = closure
}
// MARK: Private
private struct PropertyKeys {
static var progressObserver: Void?
}
private var progressObserver: ProgressObserver {
get {
let object: AnyObject? = getAssociatedObjectForKey(&PropertyKeys.progressObserver, inObject: self)
if let observer = object as? ProgressObserver {
return observer
}
let observer = ProgressObserver(self)
setAssociatedRetainedObject(
observer,
forKey: &PropertyKeys.progressObserver,
inObject: self
)
return observer
}
}
}
@objc private final class ProgressObserver: NSObject {
private weak var progress: NSProgress?
private var progressHandler: ((progress: NSProgress) -> Void)?
private init(_ progress: NSProgress) {
self.progress = progress
super.init()
progress.addObserver(
self,
forKeyPath: "fractionCompleted",
options: .New,
context: nil
)
}
deinit {
progress?.removeObserver(self, forKeyPath: "fractionCompleted")
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard let progress = self.progress where object as? NSProgress == progress && keyPath == "fractionCompleted" else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
return
}
GCDQueue.Main.async { [weak self] () -> Void in
if let strongSelf = self, let progress = strongSelf.progress {
strongSelf.progressHandler?(progress: progress)
}
}
}
}

View File

@@ -0,0 +1,61 @@
//
// MigrationManager.swift
// CoreStore
//
// Copyright (c) 2015 John Rommel Estropia
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import Foundation
import CoreData
// MARK: - MigrationManager
internal final class MigrationManager: NSMigrationManager, NSProgressReporting {
// MARK: NSObject
override func didChangeValueForKey(key: String) {
super.didChangeValueForKey(key)
if key == "migrationProgress" {
let progress = self.progress
progress.completedUnitCount = Int64(Float(progress.totalUnitCount) * self.migrationProgress)
}
}
// MARK: NSMigrationManager
init(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, progress: NSProgress) {
self.progress = progress
super.init(sourceModel: sourceModel, destinationModel: destinationModel)
}
// MARK: NSProgressReporting
let progress: NSProgress
}

View File

@@ -74,7 +74,8 @@ internal extension NSManagedObjectContext {
let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
context.parentContext = rootContext
context.shouldCascadeSavesToParent = true
context.mergePolicy = NSRollbackMergePolicy
context.shouldCascadeSavesToParent = false
context.undoManager = nil
context.setupForCoreStoreWithContextName("com.corestore.maincontext")
context.observerForDidSaveNotification = NotificationObserver(

View File

@@ -33,15 +33,65 @@ internal extension NSManagedObjectModel {
// MARK: Internal
private var modelFileURL: NSURL? {
@nonobjc internal class func fromBundle(bundle: NSBundle, modelName: String, modelVersion: String? = nil) -> NSManagedObjectModel {
get {
guard let modelFilePath = bundle.pathForResource(modelName, ofType: "momd") else {
return self.modelVersionFileURL?.URLByDeletingLastPathComponent
fatalError("Could not find \"\(modelName).momd\" from the bundle. \(bundle)")
}
let modelFileURL = NSURL(fileURLWithPath: modelFilePath)
let versionInfoPlistURL = modelFileURL.URLByAppendingPathComponent("VersionInfo.plist", isDirectory: false)
guard let versionInfo = NSDictionary(contentsOfURL: versionInfoPlistURL),
let versionHashes = versionInfo["NSManagedObjectModel_VersionHashes"] as? [String: AnyObject] else {
fatalError("Could not load \(typeName(NSManagedObjectModel)) metadata from path \"\(versionInfoPlistURL)\"."
)
}
let modelVersions = Set(versionHashes.keys)
let currentModelVersion: String
if let modelVersion = modelVersion {
currentModelVersion = modelVersion
}
else {
currentModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String ?? modelVersions.first!
}
var modelVersionFileURL: NSURL?
for modelVersion in modelVersions {
let fileURL = modelFileURL.URLByAppendingPathComponent("\(modelVersion).mom", isDirectory: false)
if modelVersion == currentModelVersion {
modelVersionFileURL = fileURL
continue
}
precondition(
NSManagedObjectModel(contentsOfURL: fileURL) != nil,
"Could not find the \"\(modelVersion).mom\" version file for the model at URL \"\(modelFileURL)\"."
)
}
if let modelVersionFileURL = modelVersionFileURL,
let rootModel = NSManagedObjectModel(contentsOfURL: modelVersionFileURL) {
rootModel.modelVersionFileURL = modelVersionFileURL
rootModel.modelVersions = modelVersions
rootModel.currentModelVersion = currentModelVersion
return rootModel
}
fatalError("Could not create an \(typeName(NSManagedObjectModel)) from the model at URL \"\(modelFileURL)\".")
}
private(set) var currentModelVersion: String? {
@nonobjc private(set) internal var currentModelVersion: String? {
get {
@@ -61,7 +111,7 @@ internal extension NSManagedObjectModel {
}
}
private(set) var modelVersions: Set<String>? {
@nonobjc private(set) internal var modelVersions: Set<String>? {
get {
@@ -81,17 +131,17 @@ internal extension NSManagedObjectModel {
}
}
func entityNameForClass(entityClass: AnyClass) -> String {
@nonobjc internal func entityNameForClass(entityClass: AnyClass) -> String {
return self.entityNameMapping[NSStringFromClass(entityClass)]!
}
func mergedModels() -> [NSManagedObjectModel] {
@nonobjc internal func mergedModels() -> [NSManagedObjectModel] {
return self.modelVersions?.map { self[$0] }.flatMap { $0 == nil ? [] : [$0!] } ?? [self]
}
subscript(modelVersion: String) -> NSManagedObjectModel? {
@nonobjc internal subscript(modelVersion: String) -> NSManagedObjectModel? {
if modelVersion == self.currentModelVersion {
@@ -117,68 +167,32 @@ internal extension NSManagedObjectModel {
return model
}
class func fromBundle(bundle: NSBundle, modelName: String, modelVersion: String? = nil) -> NSManagedObjectModel {
@nonobjc internal subscript(metadata: [String: AnyObject]) -> NSManagedObjectModel? {
guard let modelFilePath = bundle.pathForResource(modelName, ofType: "momd") else {
if let modelHashes = metadata[NSStoreModelVersionHashesKey] as? [String : NSData] {
CoreStore.fatalError("Could not find \"\(modelName).momd\" from the bundle. \(bundle)")
}
let modelFileURL = NSURL(fileURLWithPath: modelFilePath)
let versionInfoPlistURL = modelFileURL.URLByAppendingPathComponent("VersionInfo.plist", isDirectory: false)
guard let versionInfo = NSDictionary(contentsOfURL: versionInfoPlistURL),
let versionHashes = versionInfo["NSManagedObjectModel_VersionHashes"] as? [String: AnyObject] else {
for modelVersion in self.modelVersions ?? [] {
CoreStore.fatalError("Could not load \(typeName(NSManagedObjectModel)) metadata from path \"\(versionInfoPlistURL)\"."
)
}
let modelVersions = Set(versionHashes.keys)
let currentModelVersion: String
if let modelVersion = modelVersion {
precondition(modelVersions.contains(modelVersion))
currentModelVersion = modelVersion
}
else {
currentModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String ?? modelVersions.first!
}
var modelVersionFileURL: NSURL?
for modelVersion in modelVersions {
let fileURL = modelFileURL.URLByAppendingPathComponent("\(modelVersion).mom", isDirectory: false)
if modelVersion == currentModelVersion {
modelVersionFileURL = fileURL
continue
if let versionModel = self[modelVersion] where modelHashes == versionModel.entityVersionHashesByName {
return versionModel
}
}
CoreStore.assert(
NSManagedObjectModel(contentsOfURL: fileURL) != nil,
"Could not find the \"\(modelVersion).mom\" version file for the model at URL \"\(modelFileURL)\"."
)
}
if let modelVersionFileURL = modelVersionFileURL,
let rootModel = NSManagedObjectModel(contentsOfURL: modelVersionFileURL) {
rootModel.modelVersionFileURL = modelVersionFileURL
rootModel.modelVersions = modelVersions
rootModel.currentModelVersion = currentModelVersion
return rootModel
}
CoreStore.fatalError("Could not create an \(typeName(NSManagedObjectModel)) from the model at URL \"\(modelFileURL)\".")
return nil
}
// MARK: Private
private var modelFileURL: NSURL? {
get {
return self.modelVersionFileURL?.URLByDeletingLastPathComponent
}
}
private var modelVersionFileURL: NSURL? {
get {

View File

@@ -46,10 +46,8 @@ public extension CoreStore {
level: level,
message: message,
fileName: fileName,
lineNumber:
lineNumber,
functionName:
functionName
lineNumber: lineNumber,
functionName: functionName
)
}
@@ -74,14 +72,4 @@ public extension CoreStore {
functionName: functionName
)
}
@noreturn internal static func fatalError(message: String, fileName: StaticString = __FILE__, lineNumber: Int = __LINE__, functionName: StaticString = __FUNCTION__) {
self.logger.fatalError(
message,
fileName: fileName,
lineNumber: lineNumber,
functionName: functionName
)
}
}

View File

@@ -79,16 +79,6 @@ public protocol CoreStoreLogger {
:functionName: the source function name
*/
func assert(@autoclosure condition: () -> Bool, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
/**
Handles fatal errors made throughout the `CoreStore` framework. Implementations should guarantee that the method does not return, either by calling fatalError() or preconditionFailure(), or by raising an exception.
:message: the error message
:fileName: the source file name
:lineNumber: the source line number
:functionName: the source function name
*/
@noreturn func fatalError(message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
}
@@ -96,17 +86,17 @@ public protocol CoreStoreLogger {
internal func typeName<T>(value: T) -> String {
return "<\(_stdlib_getDemangledTypeName(value))>"
return "'\(_stdlib_getDemangledTypeName(value))'"
}
internal func typeName<T>(value: T.Type) -> String {
return "<\(value)>"
return "'\(value)'"
}
internal func typeName(value: AnyClass) -> String {
return "<\(value)>"
return "'\(value)'"
}
internal func typeName(name: String?) -> String {

View File

@@ -42,33 +42,41 @@ public final class DefaultLogger: CoreStoreLogger {
public func log(level level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
#if DEBUG
let icon: String
let levelString: String
switch level {
case .Trace: levelString = "Trace"
case .Notice: levelString = "Notice"
case .Warning: levelString = "Warning"
case .Fatal: levelString = "Fatal"
case .Trace:
icon = "🔹"
levelString = "Trace"
case .Notice:
icon = "🔸"
levelString = "Notice"
case .Warning:
icon = "⚠️"
levelString = "Warning"
case .Fatal:
icon = ""
levelString = "Fatal"
}
Swift.print("[CoreStore:\(levelString)] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n")
Swift.print("\(icon) [CoreStore: \(levelString)] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n")
#endif
}
public func handleError(error error: NSError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
#if DEBUG
Swift.print("[CoreStore:Error] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message): \(error)\n")
Swift.print("⚠️ [CoreStore: Error] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message): \(error)\n")
#endif
}
public func assert(@autoclosure condition: () -> Bool, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
#if DEBUG
Swift.assert(condition, message, file: fileName, line: numericCast(lineNumber))
Swift.assert(condition, "❗ [CoreStore: Assertion Failure] \(message)", file: fileName, line: numericCast(lineNumber))
#endif
}
@noreturn public func fatalError(message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
Swift.fatalError("[CoreStore:Abort] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n")
}
}

View File

@@ -33,16 +33,201 @@ import GCDKit
public extension DataStack {
/**
Checks if the store with the specified filename and configuration needs to be migrated to the `DataStack`'s managed object model version.
Asynchronously adds to the stack an SQLite store from the given SQLite file name. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.)
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory. A new SQLite file will be created if it does not exist. Note that if you have multiple configurations, you will need to specify a different `fileName` explicitly for each of them.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`, the "Default" configuration. Note that if you have multiple configurations, you will need to specify a different `fileName` explicitly for each of them.
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
- parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result. If an error is thrown, this closure will not be executed.
- returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required
*/
public func addSQLiteStore(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil, completion: (PersistentStoreResult) -> Void) throws -> NSProgress? {
return try self.addSQLiteStore(
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
fileName,
isDirectory: false
),
configuration: configuration,
mappingModelBundles: mappingModelBundles,
completion: completion
)
}
/**
Asynchronously adds to the stack an SQLite store from the given SQLite file URL. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.)
- parameter fileURL: the local file URL for the SQLite persistent store. A new SQLite file will be created if it does not exist. If not specified, defaults to a file URL pointing to a "<Application name>.sqlite" file in the "Application Support" directory. Note that if you have multiple configurations, you will need to specify a different `fileURL` explicitly for each of them.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`, the "Default" configuration. Note that if you have multiple configurations, you will need to specify a different `fileURL` explicitly for each of them.
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
- parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result.
- returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required
*/
public func addSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle]? = NSBundle.allBundles(), completion: (PersistentStoreResult) -> Void) throws -> NSProgress? {
CoreStore.assert(
fileURL.fileURL,
"The specified file URL for the SQLite store is invalid: \"\(fileURL)\""
)
let coordinator = self.coordinator;
if let store = coordinator.persistentStoreForURL(fileURL) {
let isExistingStoreAutomigrating = store.options?[NSMigratePersistentStoresAutomaticallyOption] as? Bool == true
if store.type == NSSQLiteStoreType
&& isExistingStoreAutomigrating
&& store.configurationName == (configuration ?? Into.defaultConfigurationName) {
GCDQueue.Main.async {
completion(PersistentStoreResult(store))
}
return nil
}
let error = NSError(coreStoreErrorCode: .DifferentPersistentStoreExistsAtURL)
CoreStore.handleError(
error,
"Failed to add SQLite \(typeName(NSPersistentStore)) at \"\(fileURL)\" because a different \(typeName(NSPersistentStore)) at that URL already exists."
)
throw error
}
do {
try NSFileManager.defaultManager().createDirectoryAtURL(
fileURL.URLByDeletingLastPathComponent!,
withIntermediateDirectories: true,
attributes: nil
)
}
catch _ { }
do {
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(
NSSQLiteStoreType,
URL: fileURL
)
return self.upgradeSQLiteStoreIfNeeded(
fileURL: fileURL,
metadata: metadata,
configuration: configuration,
mappingModelBundles: mappingModelBundles,
completion: { (result) -> Void in
if case .Failure(let error) = result {
completion(PersistentStoreResult(error))
return
}
let persistentStoreResult = self.addSQLiteStoreAndWait(
fileURL: fileURL,
configuration: configuration,
automigrating: false,
resetStoreOnMigrationFailure: false
)
completion(persistentStoreResult)
}
)
}
catch let error as NSError
where error.code == NSFileReadNoSuchFileError && error.domain == NSCocoaErrorDomain {
let persistentStoreResult = self.addSQLiteStoreAndWait(
fileURL: fileURL,
configuration: configuration,
automigrating: false,
resetStoreOnMigrationFailure: false
)
completion(persistentStoreResult)
return nil
}
catch {
CoreStore.handleError(
error as NSError,
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata."
)
throw error
}
}
/**
Migrates an SQLite store with the specified filename to the `DataStack`'s managed object model version WITHOUT adding the migrated store to the data stack.
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
- returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required
*/
public func upgradeSQLiteStoreIfNeeded(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil, completion: (MigrationResult) -> Void) throws -> NSProgress? {
return try self.upgradeSQLiteStoreIfNeeded(
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
fileName,
isDirectory: false
),
configuration: configuration,
mappingModelBundles: mappingModelBundles,
completion: completion
)
}
/**
Migrates an SQLite store at the specified file URL and configuration name to the `DataStack`'s managed object model version. This method does NOT add the migrated store to the data stack.
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
- returns: an `NSProgress` instance if a migration has started, or `nil` is no migrations are required
*/
public func upgradeSQLiteStoreIfNeeded(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil, completion: (MigrationResult) -> Void) throws -> NSProgress? {
let metadata: [String: AnyObject]
do {
metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(
NSSQLiteStoreType,
URL: fileURL
)
}
catch {
CoreStore.handleError(
error as NSError,
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata."
)
throw error
}
return self.upgradeSQLiteStoreIfNeeded(
fileURL: fileURL,
metadata: metadata,
configuration: configuration,
mappingModelBundles: mappingModelBundles,
completion: completion
)
}
/**
Checks for the required migrations needed for the store with the specified filename and configuration to be migrated to the `DataStack`'s managed object model version. This method throws an error if the store does not exist, if inspection of the store failed, or no mapping model was found/inferred.
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
:return: a `MigrationType` indicating the type of migration required for the store; or `nil` if either inspection of the store failed, or no mapping model was found/inferred. `MigrationType` acts as a `Bool` and evaluates to `false` if no migration is required, and `true` if either a lightweight or custom migration is needed.
:return: an array of `MigrationType`s indicating the chain of migrations required for the store; or `nil` if either inspection of the store failed, or no mapping model was found/inferred. `MigrationType` acts as a `Bool` and evaluates to `false` if no migration is required, and `true` if either a lightweight or custom migration is needed.
*/
public func needsMigrationForSQLiteStore(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle] = NSBundle.allBundles() as [NSBundle]) -> MigrationType? {
public func requiredMigrationsForSQLiteStore(fileName fileName: String, configuration: String? = nil, mappingModelBundles: [NSBundle] = NSBundle.allBundles() as [NSBundle]) throws -> [MigrationType] {
return needsMigrationForSQLiteStore(
return try requiredMigrationsForSQLiteStore(
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
fileName,
isDirectory: false
@@ -60,7 +245,7 @@ public extension DataStack {
- parameter mappingModelBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.allBundles()`.
:return: a `MigrationType` indicating the type of migration required for the store; or `nil` if either inspection of the store failed, or no mapping model was found/inferred. `MigrationType` acts as a `Bool` and evaluates to `false` if no migration is required, and `true` if either a lightweight or custom migration is needed.
*/
public func needsMigrationForSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle] = NSBundle.allBundles() as [NSBundle]) -> MigrationType? {
public func requiredMigrationsForSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, mappingModelBundles: [NSBundle] = NSBundle.allBundles() as [NSBundle]) throws -> [MigrationType] {
let metadata: [String : AnyObject]
do {
@@ -74,101 +259,30 @@ public extension DataStack {
CoreStore.handleError(
error as NSError,
"Failed to add SQLite \(typeName(NSPersistentStore)) at \"\(fileURL)\"."
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata."
)
return nil
throw error
}
let coordinator = self.coordinator;
let destinationModel = coordinator.managedObjectModel
if destinationModel.isConfiguration(configuration, compatibleWithStoreMetadata: metadata) {
guard let migrationSteps = self.computeMigrationFromStoreMetadata(metadata, configuration: configuration, mappingModelBundles: mappingModelBundles) else {
return .None
}
guard let sourceModel = NSManagedObjectModel(byMergingModels: [destinationModel], forStoreMetadata: metadata) else {
return nil
}
if let _ = NSMappingModel(
fromBundles: mappingModelBundles,
forSourceModel: sourceModel,
destinationModel: destinationModel) {
return .Heavyweight
}
do {
try NSMappingModel.inferredMappingModelForSourceModel(
sourceModel,
destinationModel: destinationModel
)
return .Lightweight
}
catch {
return nil
}
}
/**
Migrates an SQLite store with the specified filename to the `DataStack`'s managed object model version. This method does NOT add the migrated store to the data stack.
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
*/
public func upgradeSQLiteStoreIfNeeded(fileName fileName: String, configuration: String? = nil, sourceBundles: [NSBundle]? = nil, completion: (MigrationResult) -> Void) -> MigrationType? {
return self.upgradeSQLiteStoreIfNeeded(
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
fileName,
isDirectory: false
),
configuration: configuration,
sourceBundles: sourceBundles,
completion: completion
)
}
/**
Migrates an SQLite store at the specified file URL and configuration name to the `DataStack`'s managed object model version. This method does NOT add the migrated store to the data stack.
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil` which indicates the "Default" configuration.
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
- parameter sourceBundles: an optional array of bundles to search mapping model files from. If not set, defaults to the `NSBundle.mainBundle()`.
*/
public func upgradeSQLiteStoreIfNeeded(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, sourceBundles: [NSBundle]? = nil, completion: (MigrationResult) -> Void) -> MigrationType? {
let metadata: [String: AnyObject]
do {
metadata = try NSPersistentStoreCoordinator.metadataForPersistentStoreOfType(
NSSQLiteStoreType,
URL: fileURL
)
}
catch {
let metadataError = error as NSError
let error = NSError(coreStoreErrorCode: .MappingModelNotFound)
CoreStore.handleError(
metadataError,
"Failed to load SQLite \(typeName(NSPersistentStore)) metadata at \"\(fileURL)\"."
error,
"Failed to find migration steps from the store at URL \"\(fileURL)\" to version model \"\(self.modelVersion)\"."
)
GCDQueue.Main.async {
completion(MigrationResult(metadataError))
}
return nil
throw error
}
guard let migrationSteps = self.computeMigrationFromStoreMetadata(metadata, configuration: configuration, sourceBundles: sourceBundles) else {
return migrationSteps.map { $0.migrationType }
}
// MARK: Private
private func upgradeSQLiteStoreIfNeeded(fileURL fileURL: NSURL, metadata: [String: AnyObject], configuration: String?, mappingModelBundles: [NSBundle]?, completion: (MigrationResult) -> Void) -> NSProgress? {
guard let migrationSteps = self.computeMigrationFromStoreMetadata(metadata, configuration: configuration, mappingModelBundles: mappingModelBundles) else {
CoreStore.handleError(
NSError(coreStoreErrorCode: .MappingModelNotFound),
@@ -182,30 +296,28 @@ public extension DataStack {
return nil
}
if migrationSteps.count == 0 {
let numberOfMigrations: Int64 = Int64(migrationSteps.count)
if numberOfMigrations == 0 {
GCDQueue.Main.async {
completion(MigrationResult(.None))
completion(MigrationResult([]))
return
}
return .None
return nil
}
var mergedMigrationType = MigrationType.None
let migrationTypes = migrationSteps.map { $0.migrationType }
var migrationResult: MigrationResult?
var operations = [NSOperation]()
var cancelled = false
for (sourceModel, destinationModel, mappingModel, migrationType) in migrationSteps {
let progress = NSProgress(totalUnitCount: numberOfMigrations)
for (sourceModel, destinationModel, mappingModel, _) in migrationSteps {
switch (mergedMigrationType, migrationType) {
case (.None, _), (.Lightweight, .Heavyweight):
mergedMigrationType = migrationType
default:
break
}
progress.becomeCurrentWithPendingUnitCount(1)
let childProgress = NSProgress(totalUnitCount: 100)
operations.append(
NSBlockOperation { [weak self] in
@@ -223,8 +335,10 @@ public extension DataStack {
fileURL: fileURL,
sourceModel: sourceModel,
destinationModel: destinationModel,
mappingModel: mappingModel
mappingModel: mappingModel,
progress: childProgress
)
childProgress.setProgressHandler(nil)
}
catch {
@@ -232,8 +346,16 @@ public extension DataStack {
cancelled = true
}
}
GCDQueue.Main.async {
withExtendedLifetime(childProgress) { (_: NSProgress) -> Void in }
return
}
}
)
progress.resignCurrent()
}
let migrationOperation = NSBlockOperation()
@@ -243,7 +365,9 @@ public extension DataStack {
GCDQueue.Main.async {
completion(migrationResult ?? MigrationResult(mergedMigrationType))
completion(migrationResult ?? MigrationResult(migrationTypes))
withExtendedLifetime(progress) { (_: NSProgress) -> Void in }
return
}
}
@@ -252,105 +376,10 @@ public extension DataStack {
self.migrationQueue.addOperations(operations, waitUntilFinished: false)
return mergedMigrationType
return progress
}
/**
Asynchronously adds to the stack an SQLite store from the given SQLite file name. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.)
- parameter fileName: the local filename for the SQLite persistent store in the "Application Support" directory. A new SQLite file will be created if it does not exist. Note that if you have multiple configurations, you will need to specify a different `fileName` explicitly for each of them.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`, the "Default" configuration. Note that if you have multiple configurations, you will need to specify a different `fileName` explicitly for each of them.
- parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result.
*/
public func addSQLiteStore(fileName fileName: String, configuration: String? = nil, sourceBundles: [NSBundle]? = nil, completion: (PersistentStoreResult) -> Void) {
self.addSQLiteStore(
fileURL: applicationSupportDirectory.URLByAppendingPathComponent(
fileName,
isDirectory: false
),
configuration: configuration,
sourceBundles: sourceBundles,
completion: completion
)
}
/**
Asynchronously adds to the stack an SQLite store from the given SQLite file URL. Note that using `addSQLiteStore(...)` instead of `addSQLiteStoreAndWait(...)` implies that the migrations are allowed and expected (thus the asynchronous `completion`.)
- parameter fileURL: the local file URL for the SQLite persistent store. A new SQLite file will be created if it does not exist. If not specified, defaults to a file URL pointing to a "<Application name>.sqlite" file in the "Application Support" directory. Note that if you have multiple configurations, you will need to specify a different `fileURL` explicitly for each of them.
- parameter configuration: an optional configuration name from the model file. If not specified, defaults to `nil`, the "Default" configuration. Note that if you have multiple configurations, you will need to specify a different `fileURL` explicitly for each of them.
- parameter completion: the closure to be executed on the main queue when the process completes, either due to success or failure. The closure's `PersistentStoreResult` argument indicates the result.
*/
public func addSQLiteStore(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, sourceBundles: [NSBundle]? = NSBundle.allBundles(), completion: (PersistentStoreResult) -> Void) {
let coordinator = self.coordinator;
if let store = coordinator.persistentStoreForURL(fileURL) {
let isExistingStoreAutomigrating = store.options?[NSMigratePersistentStoresAutomaticallyOption] as? Bool == true
if store.type == NSSQLiteStoreType
&& isExistingStoreAutomigrating
&& store.configurationName == (configuration ?? Into.defaultConfigurationName) {
GCDQueue.Main.async {
completion(PersistentStoreResult(store))
}
return
}
CoreStore.handleError(
NSError(coreStoreErrorCode: .DifferentPersistentStoreExistsAtURL),
"Failed to add SQLite \(typeName(NSPersistentStore)) at \"\(fileURL)\" because a different \(typeName(NSPersistentStore)) at that URL already exists."
)
GCDQueue.Main.async {
completion(PersistentStoreResult(.DifferentPersistentStoreExistsAtURL))
}
return
}
do {
try NSFileManager.defaultManager().createDirectoryAtURL(
fileURL.URLByDeletingLastPathComponent!,
withIntermediateDirectories: true,
attributes: nil
)
}
catch _ { }
self.upgradeSQLiteStoreIfNeeded(
fileURL: fileURL,
configuration: configuration,
sourceBundles: sourceBundles,
completion: { (result) -> Void in
if case .Failure(let error) = result
where error.domain != NSCocoaErrorDomain || error.code != NSFileReadNoSuchFileError {
completion(PersistentStoreResult(error))
return
}
let persistentStoreResult = self.addSQLiteStoreAndWait(
fileURL: fileURL,
configuration: configuration,
automigrating: false,
resetStoreOnMigrationFailure: false
)
completion(persistentStoreResult)
}
)
}
// MARK: Private
private func computeMigrationFromStoreMetadata(metadata: [String: AnyObject], configuration: String? = nil, sourceBundles: [NSBundle]? = nil) -> [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]? {
private func computeMigrationFromStoreMetadata(metadata: [String: AnyObject], configuration: String? = nil, mappingModelBundles: [NSBundle]? = nil) -> [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]? {
let model = self.model
if model.isConfiguration(configuration, compatibleWithStoreMetadata: metadata) {
@@ -358,42 +387,16 @@ public extension DataStack {
return []
}
let metadataModel = NSManagedObjectModel(byMergingModels: model.mergedModels(), forStoreMetadata: metadata)!
if let bypassModel = NSMappingModel(
fromBundles: sourceBundles,
forSourceModel: metadataModel,
destinationModel: model) {
guard let initialModel = model[metadata],
var currentVersion = initialModel.currentModelVersion else {
return [
(
sourceModel: metadataModel,
destinationModel: model,
mappingModel: bypassModel,
migrationType: .Heavyweight
)
]
return nil
}
var initialModel: NSManagedObjectModel?
if let modelHashes = metadata[NSStoreModelVersionHashesKey] as? [String : NSData],
let modelVersions = model.modelVersions {
for modelVersion in modelVersions {
if let versionModel = model[modelVersion] where modelHashes == versionModel.entityVersionHashesByName {
initialModel = versionModel
break
}
}
}
let migrationChain: MigrationChain = self.migrationChain.empty
? [currentVersion: model.currentModelVersion!]
: self.migrationChain
guard var currentVersion = initialModel?.currentModelVersion else {
return nil
}
let migrationChain = self.migrationChain
var migrationSteps = [(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, migrationType: MigrationType)]()
while let nextVersion = migrationChain.nextVersionFrom(currentVersion),
@@ -401,7 +404,7 @@ public extension DataStack {
let destinationModel = model[nextVersion] {
if let mappingModel = NSMappingModel(
fromBundles: sourceBundles,
fromBundles: mappingModelBundles,
forSourceModel: sourceModel,
destinationModel: destinationModel) {
@@ -410,7 +413,10 @@ public extension DataStack {
sourceModel: sourceModel,
destinationModel: destinationModel,
mappingModel: mappingModel,
migrationType: .Heavyweight
migrationType: .Heavyweight(
sourceVersion: currentVersion,
destinationVersion: nextVersion
)
)
)
}
@@ -428,7 +434,10 @@ public extension DataStack {
sourceModel: sourceModel,
destinationModel: destinationModel,
mappingModel: mappingModel,
migrationType: .Lightweight
migrationType: .Lightweight(
sourceVersion: currentVersion,
destinationVersion: nextVersion
)
)
)
}
@@ -448,7 +457,7 @@ public extension DataStack {
return nil
}
private func startMigrationForSQLiteStore(fileURL fileURL: NSURL, sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel) throws {
private func startMigrationForSQLiteStore(fileURL fileURL: NSURL, sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel, mappingModel: NSMappingModel, progress: NSProgress) throws {
autoreleasepool {
@@ -462,25 +471,10 @@ public extension DataStack {
try! journalUpdatingCoordinator.removePersistentStore(store)
}
let migrationManager = NSMigrationManager(
let migrationManager = MigrationManager(
sourceModel: sourceModel,
destinationModel: destinationModel
)
var lastReportedProgress: Float = -1
let timer = GCDTimer.createSuspended(
.Main,
interval: 0.1,
eventHandler: { (timer) -> Void in
let progress = migrationManager.migrationProgress
if progress > lastReportedProgress {
// TODO: progress
CoreStore.log(.Trace, message: "migration progress: \(progress)")
lastReportedProgress = progress
}
}
destinationModel: destinationModel,
progress: progress
)
let temporaryDirectoryURL = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).URLByAppendingPathComponent(NSProcessInfo().globallyUniqueString)
@@ -493,6 +487,7 @@ public extension DataStack {
)
let temporaryFileURL = temporaryDirectoryURL.URLByAppendingPathComponent(fileURL.lastPathComponent!, isDirectory: false)
do {
try migrationManager.migrateStoreFromURL(
@@ -507,25 +502,22 @@ public extension DataStack {
}
catch {
timer.suspend()
do {
try fileManager.removeItemAtURL(temporaryDirectoryURL)
}
catch _ { }
let migrationError = error as NSError
let sourceVersion = migrationManager.sourceModel.currentModelVersion ?? "???"
let destinationVersion = migrationManager.destinationModel.currentModelVersion ?? "???"
CoreStore.handleError(
migrationError,
"Failed to migrate from version model \"\(migrationManager.sourceModel)\" to version model \"\(migrationManager.destinationModel)\"."
error as NSError,
"Failed to migrate from version model \"\(sourceVersion)\" to version model \"\(destinationVersion)\"."
)
throw error
}
timer.suspend()
do {
try fileManager.replaceItemAtURL(
@@ -536,6 +528,8 @@ public extension DataStack {
resultingItemURL: nil
)
progress.completedUnitCount = progress.totalUnitCount
do {
try fileManager.removeItemAtPath(fileURL.path! + "-shm")
@@ -550,10 +544,11 @@ public extension DataStack {
}
catch _ { }
let replaceError = error as NSError
let sourceVersion = migrationManager.sourceModel.currentModelVersion ?? "???"
let destinationVersion = migrationManager.destinationModel.currentModelVersion ?? "???"
CoreStore.handleError(
replaceError,
"Failed to save store after migrating from version model \"\(migrationManager.sourceModel)\" to version model \"\(migrationManager.destinationModel)\"."
error as NSError,
"Failed to save store after migrating from version model \"\(sourceVersion)\" to version model \"\(destinationVersion)\"."
)
throw error

View File

@@ -137,6 +137,11 @@ public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, D
internal let leafVersions: Set<String>
internal let valid: Bool
internal var empty: Bool {
return self.versionTree.count <= 0
}
internal func contains(version: String) -> Bool {
return self.rootVersions.contains(version)

View File

@@ -38,17 +38,53 @@ public enum MigrationType: BooleanType {
/**
Indicates that the persistent store matches the latest model version and no migration is needed
*/
case None
case None(version: String)
/**
Indicates that the persistent store does not match the latest model version but Core Data can infer the mapping model, so a lightweight migration is needed
*/
case Lightweight
case Lightweight(sourceVersion: String, destinationVersion: String)
/**
Indicates that the persistent store does not match the latest model version and Core Data could not infer a mapping model, so a custom migration is needed
*/
case Heavyweight
case Heavyweight(sourceVersion: String, destinationVersion: String)
/**
Returns the source model version for the migration type. If no migration is required, `sourceVersion` will be equal to the `destinationVersion`.
*/
public var sourceVersion: String {
switch self {
case .None(let version):
return version
case .Lightweight(let sourceVersion, _):
return sourceVersion
case .Heavyweight(let sourceVersion, _):
return sourceVersion
}
}
/**
Returns the destination model version for the migration type. If no migration is required, `destinationVersion` will be equal to the `sourceVersion`.
*/
public var destinationVersion: String {
switch self {
case .None(let version):
return version
case .Lightweight(_, let destinationVersion):
return destinationVersion
case .Heavyweight(_, let destinationVersion):
return destinationVersion
}
}
// MARK: BooleanType
@@ -103,7 +139,7 @@ public enum MigrationResult {
/**
`MigrationResult.Success` indicates that the `commit()` for the transaction succeeded, either because the save succeeded or because there were no changes to save. The associated value `hasChanges` indicates if there were saved changes or not.
*/
case Success(MigrationType)
case Success([MigrationType])
/**
`SaveResult.Failure` indicates that the `commit()` for the transaction failed. The associated object for this value is the related `NSError` instance.
@@ -113,9 +149,9 @@ public enum MigrationResult {
// MARK: Internal
internal init(_ migrationType: MigrationType) {
internal init(_ migrationTypes: [MigrationType]) {
self = .Success(migrationType)
self = .Success(migrationTypes)
}
internal init(_ error: NSError) {

View File

@@ -70,10 +70,10 @@ public /*abstract*/ class BaseDataTransaction {
return object
case (.None, true):
CoreStore.fatalError("Attempted to create an entity of type \(typeName(entityClass)) with ambiguous destination persistent store, but the configuration name was not specified.")
fatalError("Attempted to create an entity of type \(typeName(entityClass)) with ambiguous destination persistent store, but the configuration name was not specified.")
default:
CoreStore.fatalError("Attempted to create an entity of type \(typeName(entityClass)), but a destination persistent store containing the entity type could not be found.")
fatalError("Attempted to create an entity of type \(typeName(entityClass)), but a destination persistent store containing the entity type could not be found.")
}
}
else {
@@ -88,11 +88,11 @@ public /*abstract*/ class BaseDataTransaction {
default:
if let configuration = into.configuration {
CoreStore.fatalError("Attempted to create an entity of type \(typeName(entityClass)) into the configuration \"\(configuration)\", which it doesn't belong to.")
fatalError("Attempted to create an entity of type \(typeName(entityClass)) into the configuration \"\(configuration)\", which it doesn't belong to.")
}
else {
CoreStore.fatalError("Attempted to create an entity of type \(typeName(entityClass)) into the default configuration, which it doesn't belong to.")
fatalError("Attempted to create an entity of type \(typeName(entityClass)) into the default configuration, which it doesn't belong to.")
}
}
}

View File

@@ -47,7 +47,7 @@ public final class DataStack {
/**
Initializes a `DataStack` from an `NSManagedObjectModel`.
- parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name will be used
- parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name will be used.
- parameter bundle: an optional bundle to load models from. If not specified, the main bundle will be used.
- parameter migrationChain: the `MigrationChain` that indicates the heirarchy of the model's version names. If not specified, will default to a non-migrating data stack.
*/
@@ -74,6 +74,14 @@ public final class DataStack {
self.rootSavingContext.parentStack = self
}
/**
Returns the `DataStack`'s model version. The version string is the same as the name of the .xcdatamodeld file.
*/
public var modelVersion: String {
return self.model.currentModelVersion!
}
/**
Adds an in-memory store to the stack.
@@ -160,6 +168,11 @@ public final class DataStack {
*/
public func addSQLiteStoreAndWait(fileURL fileURL: NSURL = defaultSQLiteStoreURL, configuration: String? = nil, automigrating: Bool = true, resetStoreOnMigrationFailure: Bool = false) -> PersistentStoreResult {
CoreStore.assert(
fileURL.fileURL,
"The specified file URL for the SQLite store is invalid: \"\(fileURL)\""
)
let coordinator = self.coordinator;
if let store = coordinator.persistentStoreForURL(fileURL) {

View File

@@ -136,31 +136,54 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Organism" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="C06-r4-27K">
<rect key="frame" x="20" y="20" width="560" height="26.5"/>
<rect key="frame" x="20" y="90" width="560" height="26.5"/>
<fontDescription key="fontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="22"/>
<color key="textColor" red="0.92549019610000005" green="0.94117647059999998" blue="0.94509803920000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="attributes" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fXh-OL-4Qb">
<rect key="frame" x="20" y="70" width="560" height="20.5"/>
<rect key="frame" x="20" y="140" width="560" height="20.5"/>
<fontDescription key="fontDescription" name="HelveticaNeue-Light" family="Helvetica Neue" pointSize="17"/>
<color key="textColor" red="0.92549019610000005" green="0.94117647059999998" blue="0.94509803920000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="Hwa-fO-fC5">
<rect key="frame" x="20" y="20" width="560" height="29"/>
<segments>
<segment title="First"/>
<segment title="Second"/>
<segment title=""/>
</segments>
<color key="tintColor" cocoaTouchSystemColor="tableCellGroupedBackgroundColor"/>
<connections>
<action selector="segmentedControlValueChanged:" destination="hJK-5I-1TQ" eventType="valueChanged" id="jNH-gx-mOg"/>
</connections>
</segmentedControl>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="H1P-2g-DHX">
<rect key="frame" x="20" y="68" width="560" height="2"/>
<color key="progressTintColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
<color key="trackTintColor" white="1" alpha="0.20000000000000001" colorSpace="calibratedWhite"/>
</progressView>
</subviews>
<color key="backgroundColor" red="0.20392156859999999" green="0.28627450980000002" blue="0.36862745099999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="H1P-2g-DHX" firstAttribute="leading" secondItem="5Hd-jr-1nW" secondAttribute="leading" constant="20" id="2Nf-U0-bTq"/>
<constraint firstItem="fXh-OL-4Qb" firstAttribute="top" secondItem="C06-r4-27K" secondAttribute="bottom" constant="23.5" id="6d1-A5-Zc5"/>
<constraint firstAttribute="trailing" secondItem="C06-r4-27K" secondAttribute="trailing" constant="20" id="DC3-u7-f9M"/>
<constraint firstItem="Hwa-fO-fC5" firstAttribute="leading" secondItem="5Hd-jr-1nW" secondAttribute="leading" constant="20" id="CDZ-qV-3eK"/>
<constraint firstItem="H1P-2g-DHX" firstAttribute="top" secondItem="Hwa-fO-fC5" secondAttribute="bottom" constant="20" id="Dcs-Vv-Xfv"/>
<constraint firstItem="fXh-OL-4Qb" firstAttribute="leading" secondItem="C06-r4-27K" secondAttribute="leading" id="SnQ-Ra-KkU"/>
<constraint firstItem="C06-r4-27K" firstAttribute="top" secondItem="5Hd-jr-1nW" secondAttribute="top" constant="20" id="abW-Z0-uln"/>
<constraint firstItem="Hwa-fO-fC5" firstAttribute="top" secondItem="5Hd-jr-1nW" secondAttribute="top" constant="20" id="Zo1-kW-kAw"/>
<constraint firstItem="C06-r4-27K" firstAttribute="top" secondItem="H1P-2g-DHX" secondAttribute="bottom" constant="20" id="bBW-Bn-oeN"/>
<constraint firstAttribute="trailing" secondItem="fXh-OL-4Qb" secondAttribute="trailing" constant="20" id="mhR-0c-i0n"/>
<constraint firstAttribute="trailing" secondItem="C06-r4-27K" secondAttribute="trailing" constant="20" id="s0c-XG-e70"/>
<constraint firstAttribute="trailing" secondItem="H1P-2g-DHX" secondAttribute="trailing" constant="20" id="swp-18-f5e"/>
<constraint firstItem="C06-r4-27K" firstAttribute="top" secondItem="Hwa-fO-fC5" secondAttribute="bottom" constant="20" id="tYt-wb-Lk8"/>
<constraint firstAttribute="trailing" secondItem="Hwa-fO-fC5" secondAttribute="trailing" constant="20" id="ujX-gH-Wlm"/>
<constraint firstItem="C06-r4-27K" firstAttribute="leading" secondItem="5Hd-jr-1nW" secondAttribute="leading" constant="20" id="yiG-c9-BZg"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="DC3-u7-f9M"/>
<exclude reference="tYt-wb-Lk8"/>
</mask>
</variation>
</view>
@@ -206,6 +229,8 @@
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="organismLabel" destination="fXh-OL-4Qb" id="shX-fK-haw"/>
<outlet property="progressView" destination="H1P-2g-DHX" id="Asy-fE-gXI"/>
<outlet property="segmentedControl" destination="Hwa-fO-fC5" id="myL-tO-4ah"/>
<outlet property="titleLabel" destination="C06-r4-27K" id="zQr-YC-moe"/>
</connections>
</tableViewController>

View File

@@ -19,7 +19,39 @@ class MigrationsDemoViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.selectModelVersion(self.models.first!)
let models = self.models
if let segmentedControl = self.segmentedControl {
for (index, model) in models.enumerate() {
segmentedControl.setTitle(
model.label,
forSegmentAtIndex: index
)
}
}
let dataStack = DataStack(modelName: "MigrationDemo")
do {
let migrations = try dataStack.requiredMigrationsForSQLiteStore(
fileName: "MigrationDemo.sqlite"
)
let storeVersion = migrations.first?.sourceVersion ?? dataStack.modelVersion
for model in models {
if model.version == storeVersion {
self.selectModelVersion(model, animated: false)
return
}
}
}
catch _ { }
self.selectModelVersion(self.models.first!, animated: false)
}
@@ -47,21 +79,23 @@ class MigrationsDemoViewController: UITableViewController {
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.selectModelVersion(self.models[indexPath.row])
self.selectModelVersion(self.models[indexPath.row], animated: true)
}
// MARK: Private
private typealias ModelMetadata = (version: String, entityType: AnyClass, migrationChain: MigrationChain)
private typealias ModelMetadata = (label: String, version: String, entityType: AnyClass, migrationChain: MigrationChain)
private let models: [ModelMetadata] = [
(
label: "Model V1",
version: "MigrationDemo",
entityType: OrganismV1.self,
migrationChain: ["MigrationDemoV3", "MigrationDemoV2", "MigrationDemo"]
),
(
label: "Model V2",
version: "MigrationDemoV2",
entityType: OrganismV2.self,
migrationChain: [
@@ -70,6 +104,7 @@ class MigrationsDemoViewController: UITableViewController {
]
),
(
label: "Model V3",
version: "MigrationDemoV3",
entityType: OrganismV3.self,
migrationChain: ["MigrationDemo", "MigrationDemoV2", "MigrationDemoV3"]
@@ -81,6 +116,8 @@ class MigrationsDemoViewController: UITableViewController {
@IBOutlet private dynamic weak var titleLabel: UILabel?
@IBOutlet private dynamic weak var organismLabel: UILabel?
@IBOutlet private dynamic weak var segmentedControl: UISegmentedControl?
@IBOutlet private dynamic weak var progressView: UIProgressView?
@IBAction private dynamic func mutateBarButtonTapped(sender: AnyObject?) {
@@ -93,11 +130,21 @@ class MigrationsDemoViewController: UITableViewController {
transaction.commit()
}
self.updateDisplay()
self.updateDisplayWithCompletion()
}
}
private func selectModelVersion(model: ModelMetadata) {
@IBAction private dynamic func segmentedControlValueChanged(sender: AnyObject?) {
guard let index = self.segmentedControl?.selectedSegmentIndex else {
return
}
self.selectModelVersion(self.models[index], animated: true)
}
private func selectModelVersion(model: ModelMetadata, animated: Bool) {
if self.organism?.entity.managedObjectClassName == "\(model.entityType)" {
@@ -112,8 +159,8 @@ class MigrationsDemoViewController: UITableViewController {
migrationChain: model.migrationChain
)
self.setEnabled(false)
dataStack.addSQLiteStore(
self.setEnabled(false, animated: animated)
let progress = try! dataStack.addSQLiteStore(
fileName: "MigrationDemo.sqlite",
completion: { [weak self] (result) -> Void in
@@ -124,7 +171,7 @@ class MigrationsDemoViewController: UITableViewController {
guard case .Success = result else {
strongSelf.setEnabled(true)
strongSelf.setEnabled(true, animated: animated)
return
}
@@ -137,32 +184,44 @@ class MigrationsDemoViewController: UITableViewController {
dataStack.beginSynchronous { (transaction) -> Void in
let organism = transaction.create(Into(model.entityType))
(organism as! OrganismProtocol).mutate()
for _ in 0 ..< 100000 {
let organism = transaction.create(Into(model.entityType))
(organism as! OrganismProtocol).mutate()
}
transaction.commit()
}
strongSelf.organism = dataStack.fetchOne(From(model.entityType))!
}
strongSelf.updateDisplay()
strongSelf.updateDisplayWithCompletion()
let indexOfModel = strongSelf.models.map { $0.version }.indexOf(model.version)!
strongSelf.tableView.selectRowAtIndexPath(
NSIndexPath(
forRow: strongSelf.models.map { $0.version }.indexOf(model.version)!,
inSection: 0
),
NSIndexPath(forRow: indexOfModel, inSection: 0),
animated: false,
scrollPosition: .None
)
strongSelf.setEnabled(true)
strongSelf.segmentedControl?.selectedSegmentIndex = indexOfModel
strongSelf.setEnabled(true, animated: animated)
}
)
if let progress = progress {
self.updateDisplayWithProgress(progress)
progress.setProgressHandler { [weak self] (progress) -> Void in
self?.updateDisplayWithProgress(progress)
}
}
}
func setEnabled(enabled: Bool) {
func setEnabled(enabled: Bool, animated: Bool) {
UIView.animateKeyframesWithDuration(
0.2,
animated ? 0.2 : 0,
delay: 0,
options: .BeginFromCurrentState,
animations: { () -> Void in
@@ -182,7 +241,14 @@ class MigrationsDemoViewController: UITableViewController {
)
}
func updateDisplay() {
func updateDisplayWithProgress(progress: NSProgress) {
self.progressView?.setProgress(Float(progress.fractionCompleted), animated: true)
self.titleLabel?.text = "Migrating: \(progress.localizedDescription)"
self.organismLabel?.text = "Incremental step \(progress.localizedAdditionalDescription)"
}
func updateDisplayWithCompletion() {
var lines = [String]()
var organismType = ""
@@ -193,11 +259,12 @@ class MigrationsDemoViewController: UITableViewController {
let value: AnyObject = organism.valueForKey(property.name) ?? NSNull()
lines.append("\(property.name): \(value)")
}
organismType = "\(objc_getClass(organism.entity.managedObjectClassName))"
organismType = organism.entity.managedObjectClassName
}
self.titleLabel?.text = organismType
self.organismLabel?.text = "\n".join(lines)
self.progressView?.progress = 0
self.tableView.tableHeaderView?.setNeedsLayout()
}
}