mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-01-19 07:27:02 +01:00
added NSProgress support for migrations
This commit is contained in:
112
CoreStore/Convenience Helpers/NSProgress+Convenience.swift
Normal file
112
CoreStore/Convenience Helpers/NSProgress+Convenience.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
CoreStore/Internal/MigrationManager.swift
Normal file
61
CoreStore/Internal/MigrationManager.swift
Normal 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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user