mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-03-27 03:41:29 +01:00
improved migrationchain validation
This commit is contained in:
@@ -33,7 +33,7 @@ internal extension NSManagedObjectModel {
|
|||||||
|
|
||||||
// MARK: Internal
|
// MARK: Internal
|
||||||
|
|
||||||
@nonobjc internal class func fromBundle(bundle: NSBundle, modelName: String, modelVersion: String? = nil) -> NSManagedObjectModel {
|
@nonobjc internal class func fromBundle(bundle: NSBundle, modelName: String, modelVersionHints: Set<String> = []) -> NSManagedObjectModel {
|
||||||
|
|
||||||
guard let modelFilePath = bundle.pathForResource(modelName, ofType: "momd") else {
|
guard let modelFilePath = bundle.pathForResource(modelName, ofType: "momd") else {
|
||||||
|
|
||||||
@@ -46,19 +46,34 @@ internal extension NSManagedObjectModel {
|
|||||||
guard let versionInfo = NSDictionary(contentsOfURL: versionInfoPlistURL),
|
guard let versionInfo = NSDictionary(contentsOfURL: versionInfoPlistURL),
|
||||||
let versionHashes = versionInfo["NSManagedObjectModel_VersionHashes"] as? [String: AnyObject] else {
|
let versionHashes = versionInfo["NSManagedObjectModel_VersionHashes"] as? [String: AnyObject] else {
|
||||||
|
|
||||||
fatalError("Could not load \(typeName(NSManagedObjectModel)) metadata from path \"\(versionInfoPlistURL)\"."
|
fatalError("Could not load \(typeName(NSManagedObjectModel)) metadata from path \"\(versionInfoPlistURL)\".")
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let modelVersions = Set(versionHashes.keys)
|
let modelVersions = Set(versionHashes.keys)
|
||||||
let currentModelVersion: String
|
let currentModelVersion: String
|
||||||
if let modelVersion = modelVersion {
|
if let plistModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String where modelVersionHints.isEmpty || modelVersionHints.contains(plistModelVersion) {
|
||||||
|
|
||||||
currentModelVersion = modelVersion
|
currentModelVersion = plistModelVersion
|
||||||
|
}
|
||||||
|
else if let resolvedVersion = modelVersions.intersect(modelVersionHints).first {
|
||||||
|
|
||||||
|
CoreStore.log(
|
||||||
|
.Warning,
|
||||||
|
message: "The MigrationChain leaf versions do not include the model file's current version. Resolving to version \"\(resolvedVersion)\"."
|
||||||
|
)
|
||||||
|
currentModelVersion = resolvedVersion
|
||||||
|
}
|
||||||
|
else if let resolvedVersion = modelVersions.first ?? modelVersionHints.first {
|
||||||
|
|
||||||
|
CoreStore.log(
|
||||||
|
.Warning,
|
||||||
|
message: "The MigrationChain leaf versions do not include any of the model file's embedded versions. Resolving to version \"\(resolvedVersion)\"."
|
||||||
|
)
|
||||||
|
currentModelVersion = resolvedVersion
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
||||||
currentModelVersion = versionInfo["NSManagedObjectModel_CurrentVersionName"] as? String ?? modelVersions.first!
|
fatalError("No model files were found in URL \"\(modelFileURL)\".")
|
||||||
}
|
}
|
||||||
|
|
||||||
var modelVersionFileURL: NSURL?
|
var modelVersionFileURL: NSURL?
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public final class DefaultLogger: CoreStoreLogger {
|
|||||||
public func handleError(error error: NSError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
public func handleError(error error: NSError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString) {
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Swift.print("⚠️ [CoreStore: Error] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message): \(error)\n")
|
Swift.print("⚠️ [CoreStore: Error] \(fileName.stringValue.lastPathComponent):\(lineNumber) \(functionName)\n ↪︎ \(message)\n \(error)\n")
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ public extension DataStack {
|
|||||||
|
|
||||||
while let nextVersion = migrationChain.nextVersionFrom(currentVersion),
|
while let nextVersion = migrationChain.nextVersionFrom(currentVersion),
|
||||||
let sourceModel = model[currentVersion],
|
let sourceModel = model[currentVersion],
|
||||||
let destinationModel = model[nextVersion] {
|
let destinationModel = model[nextVersion] where sourceModel != model {
|
||||||
|
|
||||||
if let mappingModel = NSMappingModel(
|
if let mappingModel = NSMappingModel(
|
||||||
fromBundles: mappingModelBundles,
|
fromBundles: mappingModelBundles,
|
||||||
|
|||||||
@@ -29,6 +29,37 @@ import CoreData
|
|||||||
|
|
||||||
// MARK: - MigrationChain
|
// MARK: - MigrationChain
|
||||||
|
|
||||||
|
/**
|
||||||
|
A `MigrationChain` indicates the sequence of model versions to be used as the order for incremental migration. This is typically passed to the `DataStack` initializer and will be applied to all stores added to the `DataStack` with `addSQLiteStore(...)` and its variants.
|
||||||
|
|
||||||
|
Initializing with empty values (either `nil`, `[]`, or `[:]`) signifies to use the .xcdatamodel's current version as the final version, and to disable incremental migrations:
|
||||||
|
|
||||||
|
let dataStack = DataStack(migrationChain: nil)
|
||||||
|
|
||||||
|
This means that the mapping model will be computed from the store's version straight to the `DataStack`'s model version.
|
||||||
|
To support incremental migrations, specify the linear order of versions:
|
||||||
|
|
||||||
|
let dataStack = DataStack(migrationChain:
|
||||||
|
["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"])
|
||||||
|
|
||||||
|
or for more complex migration paths, a version tree that maps the key-values to the source-destination versions:
|
||||||
|
|
||||||
|
let dataStack = DataStack(migrationChain: [
|
||||||
|
"MyAppModel": "MyAppModelV3",
|
||||||
|
"MyAppModelV2": "MyAppModelV4",
|
||||||
|
"MyAppModelV3": "MyAppModelV4"
|
||||||
|
])
|
||||||
|
|
||||||
|
This allows for different migration paths depending on the starting version. The example above resolves to the following paths:
|
||||||
|
- MyAppModel-MyAppModelV3-MyAppModelV4
|
||||||
|
- MyAppModelV2-MyAppModelV4
|
||||||
|
- MyAppModelV3-MyAppModelV4
|
||||||
|
|
||||||
|
The `MigrationChain` is validated when passed to the `DataStack` and unless it is empty, will raise an assertion if any of the following conditions are met:
|
||||||
|
- a version appears twice in an array
|
||||||
|
- a version appears twice as a key in a dictionary literal
|
||||||
|
- a loop is found in any of the paths
|
||||||
|
*/
|
||||||
public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, DictionaryLiteralConvertible, ArrayLiteralConvertible {
|
public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, DictionaryLiteralConvertible, ArrayLiteralConvertible {
|
||||||
|
|
||||||
// MARK: NilLiteralConvertible
|
// MARK: NilLiteralConvertible
|
||||||
@@ -96,10 +127,28 @@ public struct MigrationChain: NilLiteralConvertible, StringLiteralConvertible, D
|
|||||||
}.map { $1 }
|
}.map { $1 }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let isVersionAmbiguous = { (start: String) -> Bool in
|
||||||
|
|
||||||
|
var checklist: Set<String> = [start]
|
||||||
|
var version = start
|
||||||
|
while let nextVersion = versionTree[version] where nextVersion != version {
|
||||||
|
|
||||||
|
if checklist.contains(version) {
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
checklist.insert(nextVersion)
|
||||||
|
version = nextVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
self.versionTree = versionTree
|
self.versionTree = versionTree
|
||||||
self.rootVersions = Set(versionTree.keys).subtract(versionTree.values)
|
self.rootVersions = Set(versionTree.keys).subtract(versionTree.values)
|
||||||
self.leafVersions = leafVersions
|
self.leafVersions = leafVersions
|
||||||
self.valid = valid
|
self.valid = valid
|
||||||
|
&& Set(versionTree.keys).union(versionTree.values).filter { isVersionAmbiguous($0) }.count <= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ public extension DataStack {
|
|||||||
NSThread.isMainThread(),
|
NSThread.isMainThread(),
|
||||||
"Attempted to observe objects from \(typeName(self)) outside the main thread."
|
"Attempted to observe objects from \(typeName(self)) outside the main thread."
|
||||||
)
|
)
|
||||||
|
CoreStore.assert(
|
||||||
|
fetchClauses.filter { $0 is OrderBy }.count > 0,
|
||||||
|
"A ListMonitor requires an OrderBy clause."
|
||||||
|
)
|
||||||
|
|
||||||
return ListMonitor(
|
return ListMonitor(
|
||||||
dataStack: self,
|
dataStack: self,
|
||||||
@@ -114,6 +118,10 @@ public extension DataStack {
|
|||||||
NSThread.isMainThread(),
|
NSThread.isMainThread(),
|
||||||
"Attempted to observe objects from \(typeName(self)) outside the main thread."
|
"Attempted to observe objects from \(typeName(self)) outside the main thread."
|
||||||
)
|
)
|
||||||
|
CoreStore.assert(
|
||||||
|
fetchClauses.filter { $0 is OrderBy }.count > 0,
|
||||||
|
"A ListMonitor requires an OrderBy clause."
|
||||||
|
)
|
||||||
|
|
||||||
return ListMonitor(
|
return ListMonitor(
|
||||||
dataStack: self,
|
dataStack: self,
|
||||||
|
|||||||
@@ -500,21 +500,9 @@ public final class ListMonitor<T: NSManagedObject> {
|
|||||||
self.sectionIndexTransformer = { $0 }
|
self.sectionIndexTransformer = { $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fetchedResultsControllerDelegate.handler = self
|
fetchedResultsControllerDelegate.handler = self
|
||||||
fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController
|
fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController
|
||||||
|
try! fetchedResultsController.performFetch()
|
||||||
do {
|
|
||||||
|
|
||||||
try fetchedResultsController.performFetch()
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
|
|
||||||
CoreStore.handleError(
|
|
||||||
error as NSError,
|
|
||||||
"Failed to perform fetch on \(typeName(NSFetchedResultsController))."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -202,19 +202,7 @@ public final class ObjectMonitor<T: NSManagedObject> {
|
|||||||
|
|
||||||
fetchedResultsControllerDelegate.handler = self
|
fetchedResultsControllerDelegate.handler = self
|
||||||
fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController
|
fetchedResultsControllerDelegate.fetchedResultsController = fetchedResultsController
|
||||||
|
try! fetchedResultsController.performFetch()
|
||||||
|
|
||||||
do {
|
|
||||||
|
|
||||||
try fetchedResultsController.performFetch()
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
|
|
||||||
CoreStore.handleError(
|
|
||||||
error as NSError,
|
|
||||||
"Failed to perform fetch for \(typeName(NSFetchedResultsController))."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.lastCommittedAttributes = (self.object?.committedValuesForKeys(nil) as? [String: NSObject]) ?? [:]
|
self.lastCommittedAttributes = (self.object?.committedValuesForKeys(nil) as? [String: NSObject]) ?? [:]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,11 +44,6 @@ public struct Into<T: NSManagedObject> {
|
|||||||
|
|
||||||
// MARK: Public
|
// MARK: Public
|
||||||
|
|
||||||
internal static var defaultConfigurationName: String {
|
|
||||||
|
|
||||||
return "PF_DEFAULT_CONFIGURATION_NAME"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Initializes an `Into` clause.
|
Initializes an `Into` clause.
|
||||||
Sample Usage:
|
Sample Usage:
|
||||||
@@ -139,6 +134,11 @@ public struct Into<T: NSManagedObject> {
|
|||||||
|
|
||||||
// MARK: Internal
|
// MARK: Internal
|
||||||
|
|
||||||
|
internal static var defaultConfigurationName: String {
|
||||||
|
|
||||||
|
return "PF_DEFAULT_CONFIGURATION_NAME"
|
||||||
|
}
|
||||||
|
|
||||||
internal let entityClass: AnyClass
|
internal let entityClass: AnyClass
|
||||||
internal let configuration: String?
|
internal let configuration: String?
|
||||||
internal let inferStoreIfPossible: Bool
|
internal let inferStoreIfPossible: Bool
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public final class DataStack {
|
|||||||
|
|
||||||
- parameter modelName: the name of the (.xcdatamodeld) model file. If not specified, the application name will be used.
|
- parameter 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 bundle: an optional bundle to load models from. If not specified, the main bundle will be used.
|
||||||
- parameter migrationChain: the `MigrationChain` that indicates the heirarchy of the model's version names. If not specified, will default to a non-migrating data stack.
|
- parameter migrationChain: the `MigrationChain` that indicates the sequence of model versions to be used as the order for incremental migration. If not specified, will default to a non-migrating data stack.
|
||||||
*/
|
*/
|
||||||
public required init(modelName: String = applicationName, bundle: NSBundle = NSBundle.mainBundle(), migrationChain: MigrationChain = nil) {
|
public required init(modelName: String = applicationName, bundle: NSBundle = NSBundle.mainBundle(), migrationChain: MigrationChain = nil) {
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ public final class DataStack {
|
|||||||
let model = NSManagedObjectModel.fromBundle(
|
let model = NSManagedObjectModel.fromBundle(
|
||||||
bundle,
|
bundle,
|
||||||
modelName: modelName,
|
modelName: modelName,
|
||||||
modelVersion: migrationChain.leafVersions.first
|
modelVersionHints: migrationChain.leafVersions
|
||||||
)
|
)
|
||||||
|
|
||||||
self.coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
|
self.coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
|
||||||
|
|||||||
Reference in New Issue
Block a user