diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index 484d19a..92c2458 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -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 = ""; }; B5E84F351AFF85470064E85B /* NSManagedObjectContext+Querying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Querying.swift"; sourceTree = ""; }; B5E84F401AFF8CCD0064E85B /* ClauseTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClauseTypes.swift; sourceTree = ""; }; + B5FAD6A81B50A4B300714891 /* NSProgress+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSProgress+Convenience.swift"; sourceTree = ""; }; + B5FAD6AB1B51285300714891 /* MigrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationManager.swift; sourceTree = ""; }; /* 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 = ""; @@ -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 */, diff --git a/CoreStore/Convenience Helpers/NSProgress+Convenience.swift b/CoreStore/Convenience Helpers/NSProgress+Convenience.swift new file mode 100644 index 0000000..e6064f9 --- /dev/null +++ b/CoreStore/Convenience Helpers/NSProgress+Convenience.swift @@ -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) { + + 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) + } + } + } +} diff --git a/CoreStore/Internal/MigrationManager.swift b/CoreStore/Internal/MigrationManager.swift new file mode 100644 index 0000000..9f453b2 --- /dev/null +++ b/CoreStore/Internal/MigrationManager.swift @@ -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 +} diff --git a/CoreStore/Internal/NSManagedObjectContext+Setup.swift b/CoreStore/Internal/NSManagedObjectContext+Setup.swift index 53b84a1..214fcc1 100644 --- a/CoreStore/Internal/NSManagedObjectContext+Setup.swift +++ b/CoreStore/Internal/NSManagedObjectContext+Setup.swift @@ -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( diff --git a/CoreStore/Internal/NSManagedObjectModel+Setup.swift b/CoreStore/Internal/NSManagedObjectModel+Setup.swift index c9ddf00..49fa678 100644 --- a/CoreStore/Internal/NSManagedObjectModel+Setup.swift +++ b/CoreStore/Internal/NSManagedObjectModel+Setup.swift @@ -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? { + @nonobjc private(set) internal var modelVersions: Set? { 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 { diff --git a/CoreStore/Logging/CoreStore+Logging.swift b/CoreStore/Logging/CoreStore+Logging.swift index 78653e4..1e8f05c 100644 --- a/CoreStore/Logging/CoreStore+Logging.swift +++ b/CoreStore/Logging/CoreStore+Logging.swift @@ -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 - ) - } } diff --git a/CoreStore/Logging/CoreStoreLogger.swift b/CoreStore/Logging/CoreStoreLogger.swift index cf0401b..c5c5637 100644 --- a/CoreStore/Logging/CoreStoreLogger.swift +++ b/CoreStore/Logging/CoreStoreLogger.swift @@ -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(value: T) -> String { - return "<\(_stdlib_getDemangledTypeName(value))>" + return "'\(_stdlib_getDemangledTypeName(value))'" } internal func typeName(value: T.Type) -> String { - return "<\(value)>" + return "'\(value)'" } internal func typeName(value: AnyClass) -> String { - return "<\(value)>" + return "'\(value)'" } internal func typeName(name: String?) -> String { diff --git a/CoreStore/Logging/DefaultLogger.swift b/CoreStore/Logging/DefaultLogger.swift index b18c007..2cb9ea1 100644 --- a/CoreStore/Logging/DefaultLogger.swift +++ b/CoreStore/Logging/DefaultLogger.swift @@ -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") - } } diff --git a/CoreStore/Migrating/DataStack+Migration.swift b/CoreStore/Migrating/DataStack+Migration.swift index c944cef..9d50408 100644 --- a/CoreStore/Migrating/DataStack+Migration.swift +++ b/CoreStore/Migrating/DataStack+Migration.swift @@ -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 ".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 ".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 diff --git a/CoreStore/Migrating/MigrationChain.swift b/CoreStore/Migrating/MigrationChain.swift index 76ab413..09bbd05 100644 --- a/CoreStore/Migrating/MigrationChain.swift +++ b/CoreStore/Migrating/MigrationChain.swift @@ -137,6 +137,11 @@ public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, D internal let leafVersions: Set internal let valid: Bool + internal var empty: Bool { + + return self.versionTree.count <= 0 + } + internal func contains(version: String) -> Bool { return self.rootVersions.contains(version) diff --git a/CoreStore/Migrating/MigrationResult.swift b/CoreStore/Migrating/MigrationResult.swift index 1454e5f..5e36990 100644 --- a/CoreStore/Migrating/MigrationResult.swift +++ b/CoreStore/Migrating/MigrationResult.swift @@ -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) { diff --git a/CoreStore/Saving and Processing/BaseDataTransaction.swift b/CoreStore/Saving and Processing/BaseDataTransaction.swift index 671e679..de38ab0 100644 --- a/CoreStore/Saving and Processing/BaseDataTransaction.swift +++ b/CoreStore/Saving and Processing/BaseDataTransaction.swift @@ -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.") } } } diff --git a/CoreStore/Setting Up/DataStack.swift b/CoreStore/Setting Up/DataStack.swift index 4f37815..4dea818 100644 --- a/CoreStore/Setting Up/DataStack.swift +++ b/CoreStore/Setting Up/DataStack.swift @@ -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) { diff --git a/CoreStoreDemo/CoreStoreDemo/Base.lproj/Main.storyboard b/CoreStoreDemo/CoreStoreDemo/Base.lproj/Main.storyboard index a74c7d4..732a228 100644 --- a/CoreStoreDemo/CoreStoreDemo/Base.lproj/Main.storyboard +++ b/CoreStoreDemo/CoreStoreDemo/Base.lproj/Main.storyboard @@ -136,31 +136,54 @@ + + + + + + + + + + + + + + + + + + - + + - + + + + + - + @@ -206,6 +229,8 @@ + + diff --git a/CoreStoreDemo/CoreStoreDemo/MIgrations Demo/MigrationsDemoViewController.swift b/CoreStoreDemo/CoreStoreDemo/MIgrations Demo/MigrationsDemoViewController.swift index 4e34afe..6bc0c33 100644 --- a/CoreStoreDemo/CoreStoreDemo/MIgrations Demo/MigrationsDemoViewController.swift +++ b/CoreStoreDemo/CoreStoreDemo/MIgrations Demo/MigrationsDemoViewController.swift @@ -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() } }