diff --git a/CoreStore/Internal/NSManagedObjectModel+Setup.swift b/CoreStore/Internal/NSManagedObjectModel+Setup.swift index 6665035..20f93c1 100644 --- a/CoreStore/Internal/NSManagedObjectModel+Setup.swift +++ b/CoreStore/Internal/NSManagedObjectModel+Setup.swift @@ -33,7 +33,7 @@ internal extension NSManagedObjectModel { // MARK: Internal - @nonobjc internal class func fromBundle(bundle: NSBundle, modelName: String, modelVersion: String? = nil) -> NSManagedObjectModel { + @nonobjc internal class func fromBundle(bundle: NSBundle, modelName: String, modelVersionHints: Set = []) -> NSManagedObjectModel { guard let modelFilePath = bundle.pathForResource(modelName, ofType: "momd") else { @@ -46,19 +46,34 @@ internal extension NSManagedObjectModel { 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)\"." - ) + fatalError("Could not load \(typeName(NSManagedObjectModel)) metadata from path \"\(versionInfoPlistURL)\".") } let modelVersions = Set(versionHashes.keys) let currentModelVersion: String - if let modelVersion = modelVersion { + if let plistModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String where modelVersionHints.isEmpty || modelVersionHints.contains(plistModelVersion) { - currentModelVersion = modelVersion + currentModelVersion = plistModelVersion + } + else if let resolvedVersion = modelVersions.intersect(modelVersionHints).first { + + CoreStore.log( + .Warning, + message: "The MigrationChain leaf versions do not include the model file's current version. Resolving to version \"\(resolvedVersion)\"." + ) + currentModelVersion = resolvedVersion + } + else if let resolvedVersion = modelVersions.first ?? modelVersionHints.first { + + CoreStore.log( + .Warning, + message: "The MigrationChain leaf versions do not include any of the model file's embedded versions. Resolving to version \"\(resolvedVersion)\"." + ) + currentModelVersion = resolvedVersion } else { - currentModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String ?? modelVersions.first! + fatalError("No model files were found in URL \"\(modelFileURL)\".") } var modelVersionFileURL: NSURL? diff --git a/CoreStore/Logging/DefaultLogger.swift b/CoreStore/Logging/DefaultLogger.swift index 2cb9ea1..b604ade 100644 --- a/CoreStore/Logging/DefaultLogger.swift +++ b/CoreStore/Logging/DefaultLogger.swift @@ -69,7 +69,7 @@ public final class DefaultLogger: CoreStoreLogger { 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)\n \(error)\n") #endif } diff --git a/CoreStore/Migrating/DataStack+Migration.swift b/CoreStore/Migrating/DataStack+Migration.swift index de71274..cfa0145 100644 --- a/CoreStore/Migrating/DataStack+Migration.swift +++ b/CoreStore/Migrating/DataStack+Migration.swift @@ -413,7 +413,7 @@ public extension DataStack { while let nextVersion = migrationChain.nextVersionFrom(currentVersion), let sourceModel = model[currentVersion], - let destinationModel = model[nextVersion] { + let destinationModel = model[nextVersion] where sourceModel != model { if let mappingModel = NSMappingModel( fromBundles: mappingModelBundles, diff --git a/CoreStore/Migrating/MigrationChain.swift b/CoreStore/Migrating/MigrationChain.swift index 09bbd05..2fb501c 100644 --- a/CoreStore/Migrating/MigrationChain.swift +++ b/CoreStore/Migrating/MigrationChain.swift @@ -29,6 +29,37 @@ import CoreData // MARK: - MigrationChain +/** +A `MigrationChain` indicates the sequence of model versions to be used as the order for incremental migration. This is typically passed to the `DataStack` initializer and will be applied to all stores added to the `DataStack` with `addSQLiteStore(...)` and its variants. + +Initializing with empty values (either `nil`, `[]`, or `[:]`) signifies to use the .xcdatamodel's current version as the final version, and to disable incremental migrations: + + let dataStack = DataStack(migrationChain: nil) + +This means that the mapping model will be computed from the store's version straight to the `DataStack`'s model version. +To support incremental migrations, specify the linear order of versions: + + let dataStack = DataStack(migrationChain: + ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"]) + +or for more complex migration paths, a version tree that maps the key-values to the source-destination versions: + + let dataStack = DataStack(migrationChain: [ + "MyAppModel": "MyAppModelV3", + "MyAppModelV2": "MyAppModelV4", + "MyAppModelV3": "MyAppModelV4" + ]) + +This allows for different migration paths depending on the starting version. The example above resolves to the following paths: +- MyAppModel-MyAppModelV3-MyAppModelV4 +- MyAppModelV2-MyAppModelV4 +- MyAppModelV3-MyAppModelV4 + +The `MigrationChain` is validated when passed to the `DataStack` and unless it is empty, will raise an assertion if any of the following conditions are met: +- a version appears twice in an array +- a version appears twice as a key in a dictionary literal +- a loop is found in any of the paths +*/ public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, DictionaryLiteralConvertible, ArrayLiteralConvertible { // MARK: NilLiteralConvertible @@ -96,10 +127,28 @@ public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, D }.map { $1 } ) + let isVersionAmbiguous = { (start: String) -> Bool in + + var checklist: Set = [start] + var version = start + while let nextVersion = versionTree[version] where nextVersion != version { + + if checklist.contains(version) { + + return true + } + checklist.insert(nextVersion) + version = nextVersion + } + + return false + } + self.versionTree = versionTree self.rootVersions = Set(versionTree.keys).subtract(versionTree.values) self.leafVersions = leafVersions self.valid = valid + && Set(versionTree.keys).union(versionTree.values).filter { isVersionAmbiguous($0) }.count <= 0 } diff --git a/CoreStore/Observing/DataStack+Observing.swift b/CoreStore/Observing/DataStack+Observing.swift index 01b66c3..cfe081f 100644 --- a/CoreStore/Observing/DataStack+Observing.swift +++ b/CoreStore/Observing/DataStack+Observing.swift @@ -78,6 +78,10 @@ public extension DataStack { NSThread.isMainThread(), "Attempted to observe objects from \(typeName(self)) outside the main thread." ) + CoreStore.assert( + fetchClauses.filter { $0 is OrderBy }.count > 0, + "A ListMonitor requires an OrderBy clause." + ) return ListMonitor( dataStack: self, @@ -114,6 +118,10 @@ public extension DataStack { NSThread.isMainThread(), "Attempted to observe objects from \(typeName(self)) outside the main thread." ) + CoreStore.assert( + fetchClauses.filter { $0 is OrderBy }.count > 0, + "A ListMonitor requires an OrderBy clause." + ) return ListMonitor( dataStack: self, diff --git a/CoreStore/Observing/ListMonitor.swift b/CoreStore/Observing/ListMonitor.swift index b32055e..a7e93bb 100644 --- a/CoreStore/Observing/ListMonitor.swift +++ b/CoreStore/Observing/ListMonitor.swift @@ -500,21 +500,9 @@ public final class ListMonitor { self.sectionIndexTransformer = { $0 } } - fetchedResultsControllerDelegate.handler = self fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController - - do { - - try fetchedResultsController.performFetch() - } - catch { - - CoreStore.handleError( - error as NSError, - "Failed to perform fetch on \(typeName(NSFetchedResultsController))." - ) - } + try! fetchedResultsController.performFetch() } diff --git a/CoreStore/Observing/ObjectMonitor.swift b/CoreStore/Observing/ObjectMonitor.swift index 6ac31ca..cf266fe 100644 --- a/CoreStore/Observing/ObjectMonitor.swift +++ b/CoreStore/Observing/ObjectMonitor.swift @@ -202,19 +202,7 @@ public final class ObjectMonitor { fetchedResultsControllerDelegate.handler = self fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController - - - do { - - try fetchedResultsController.performFetch() - } - catch { - - CoreStore.handleError( - error as NSError, - "Failed to perform fetch for \(typeName(NSFetchedResultsController))." - ) - } + try! fetchedResultsController.performFetch() self.lastCommittedAttributes = (self.object?.committedValuesForKeys(nil) as? [String: NSObject]) ?? [:] } diff --git a/CoreStore/Saving and Processing/Into.swift b/CoreStore/Saving and Processing/Into.swift index fecd936..08face6 100644 --- a/CoreStore/Saving and Processing/Into.swift +++ b/CoreStore/Saving and Processing/Into.swift @@ -44,11 +44,6 @@ public struct Into { // MARK: Public - internal static var defaultConfigurationName: String { - - return "PF_DEFAULT_CONFIGURATION_NAME" - } - /** Initializes an `Into` clause. Sample Usage: @@ -139,6 +134,11 @@ public struct Into { // MARK: Internal + internal static var defaultConfigurationName: String { + + return "PF_DEFAULT_CONFIGURATION_NAME" + } + internal let entityClass: AnyClass internal let configuration: String? internal let inferStoreIfPossible: Bool diff --git a/CoreStore/Setting Up/DataStack.swift b/CoreStore/Setting Up/DataStack.swift index 1ae2dcc..47ffd78 100644 --- a/CoreStore/Setting Up/DataStack.swift +++ b/CoreStore/Setting Up/DataStack.swift @@ -49,7 +49,7 @@ public final class DataStack { - 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. + - parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for incremental migration. If not specified, will default to a non-migrating data stack. */ public required init(modelName: String = applicationName, bundle: NSBundle = NSBundle.mainBundle(), migrationChain: MigrationChain = nil) { @@ -61,7 +61,7 @@ public final class DataStack { let model = NSManagedObjectModel.fromBundle( bundle, modelName: modelName, - modelVersion: migrationChain.leafVersions.first + modelVersionHints: migrationChain.leafVersions ) self.coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)