From caa26bca702f18d60afc58ccba129b8422035579 Mon Sep 17 00:00:00 2001 From: Ian Bytchek Date: Sun, 28 Apr 2019 08:12:47 +0100 Subject: [PATCH 1/6] Fix source model typos --- CoreStoreTests/SetupTests.swift | 8 ++++---- README.md | 2 +- Sources/CSSQliteStore.swift | 4 ++-- Sources/CSStorageInterface.swift | 2 +- Sources/DataStack+Migration.swift | 10 +++++----- Sources/DataStack.swift | 4 ++-- Sources/SQLiteStore.swift | 10 +++++----- Sources/StorageInterface.swift | 6 +++--- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/CoreStoreTests/SetupTests.swift b/CoreStoreTests/SetupTests.swift index b053c005..69ac2414 100644 --- a/CoreStoreTests/SetupTests.swift +++ b/CoreStoreTests/SetupTests.swift @@ -231,7 +231,7 @@ class SetupTests: BaseTestDataTestCase { ) try sqliteStore.cs_eraseStorageAndWait( metadata: metadata, - soureModelHint: stack.schemaHistory.schema(for: metadata)?.rawModel() + sourceModelHint: stack.schemaHistory.schema(for: metadata)?.rawModel() ) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) @@ -244,7 +244,7 @@ class SetupTests: BaseTestDataTestCase { do { let metadata = try createStore() - try sqliteStore.cs_eraseStorageAndWait(metadata: metadata, soureModelHint: nil) + try sqliteStore.cs_eraseStorageAndWait(metadata: metadata, sourceModelHint: nil) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-shm"))) @@ -358,7 +358,7 @@ class SetupTests: BaseTestDataTestCase { ) try sqliteStore.cs_eraseStorageAndWait( metadata: metadata, - soureModelHint: stack.schemaHistory.schema(for: metadata)?.rawModel() + sourceModelHint: stack.schemaHistory.schema(for: metadata)?.rawModel() ) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) @@ -371,7 +371,7 @@ class SetupTests: BaseTestDataTestCase { do { let metadata = try createStore() - try sqliteStore.cs_eraseStorageAndWait(metadata: metadata, soureModelHint: nil) + try sqliteStore.cs_eraseStorageAndWait(metadata: metadata, sourceModelHint: nil) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path)) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-wal"))) XCTAssertFalse(fileManager.fileExists(atPath: sqliteStore.fileURL.path.appending("-shm"))) diff --git a/README.md b/README.md index 8edd35c5..43a5eae7 100644 --- a/README.md +++ b/README.md @@ -307,7 +307,7 @@ public protocol LocalStorage: StorageInterface { var migrationMappingProviders: [SchemaMappingProvider] { get } var localStorageOptions: LocalStorageOptions { get } func dictionary(forOptions: LocalStorageOptions) -> [String: AnyObject]? - func cs_eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws + func cs_eraseStorageAndWait(metadata: [String: Any], sourceModelHint: NSManagedObjectModel?) throws } ``` If you have custom `NSIncrementalStore` or `NSAtomicStore` subclasses, you can implement this protocol and use it similarly to `SQLiteStore`. diff --git a/Sources/CSSQliteStore.swift b/Sources/CSSQliteStore.swift index e84e29b7..ddf6467f 100644 --- a/Sources/CSSQliteStore.swift +++ b/Sources/CSSQliteStore.swift @@ -151,11 +151,11 @@ public final class CSSQLiteStore: NSObject, CSLocalStorage, CoreStoreObjectiveCT Called by the `CSDataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `CSSQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ @objc - public func cs_eraseStorageAndWait(metadata: NSDictionary, soureModelHint: NSManagedObjectModel?, error: NSErrorPointer) -> Bool { + public func cs_eraseStorageAndWait(metadata: NSDictionary, sourceModelHint: NSManagedObjectModel?, error: NSErrorPointer) -> Bool { return bridge(error) { - try self.bridgeToSwift.cs_eraseStorageAndWait(metadata: metadata as! [String: Any], soureModelHint: soureModelHint) + try self.bridgeToSwift.cs_eraseStorageAndWait(metadata: metadata as! [String: Any], sourceModelHint: sourceModelHint) } } diff --git a/Sources/CSStorageInterface.swift b/Sources/CSStorageInterface.swift index d8e09069..5c8bb2b4 100644 --- a/Sources/CSStorageInterface.swift +++ b/Sources/CSStorageInterface.swift @@ -121,5 +121,5 @@ public protocol CSLocalStorage: CSStorageInterface { Called by the `CSDataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (SQLite stores for example, can convert WAL journaling mode to DELETE before deleting) */ @objc - func cs_eraseStorageAndWait(metadata: NSDictionary, soureModelHint: NSManagedObjectModel?, error: NSErrorPointer) -> Bool + func cs_eraseStorageAndWait(metadata: NSDictionary, sourceModelHint: NSManagedObjectModel?, error: NSErrorPointer) -> Bool } diff --git a/Sources/DataStack+Migration.swift b/Sources/DataStack+Migration.swift index bda75e17..a92d2de7 100644 --- a/Sources/DataStack+Migration.swift +++ b/Sources/DataStack+Migration.swift @@ -175,7 +175,7 @@ extension DataStack { try storage.cs_eraseStorageAndWait( metadata: metadata, - soureModelHint: self.schemaHistory.schema(for: metadata)?.rawModel() + sourceModelHint: self.schemaHistory.schema(for: metadata)?.rawModel() ) _ = try self.addStorageAndWait(storage) @@ -341,7 +341,7 @@ extension DataStack { ) _ = try self.schemaHistory .schema(for: metadata) - .flatMap({ try storage.cs_eraseStorageAndWait(soureModel: $0.rawModel()) }) + .flatMap({ try storage.cs_eraseStorageAndWait(sourceModel: $0.rawModel()) }) _ = try self.createPersistentStoreFromStorage( storage, finalURL: cacheFileURL, @@ -685,7 +685,7 @@ extension DataStack { do { - try storage.cs_finalizeStorageAndWait(soureModelHint: sourceModel) + try storage.cs_finalizeStorageAndWait(sourceModelHint: sourceModel) } catch { @@ -739,7 +739,7 @@ extension DataStack { fakeProgress = 1 } - _ = try? storage.cs_finalizeStorageAndWait(soureModelHint: destinationModel) + _ = try? storage.cs_finalizeStorageAndWait(sourceModelHint: destinationModel) progress.completedUnitCount = progress.totalUnitCount return } @@ -801,7 +801,7 @@ extension DataStack { migrationMappingProviders: storage.migrationMappingProviders, localStorageOptions: storage.localStorageOptions ) - try temporaryStorage.cs_finalizeStorageAndWait(soureModelHint: destinationModel) + try temporaryStorage.cs_finalizeStorageAndWait(sourceModelHint: destinationModel) } catch { diff --git a/Sources/DataStack.swift b/Sources/DataStack.swift index 6a320c2f..3cc1db17 100644 --- a/Sources/DataStack.swift +++ b/Sources/DataStack.swift @@ -335,7 +335,7 @@ public final class DataStack: Equatable { ) try storage.cs_eraseStorageAndWait( metadata: metadata, - soureModelHint: self.schemaHistory.schema(for: metadata)?.rawModel() + sourceModelHint: self.schemaHistory.schema(for: metadata)?.rawModel() ) let finalStoreOptions = storage.dictionary(forOptions: storage.localStorageOptions) _ = try self.createPersistentStoreFromStorage( @@ -442,7 +442,7 @@ public final class DataStack: Equatable { ) _ = try self.schemaHistory .schema(for: metadata) - .flatMap({ try storage.cs_eraseStorageAndWait(soureModel: $0.rawModel()) }) + .flatMap({ try storage.cs_eraseStorageAndWait(sourceModel: $0.rawModel()) }) _ = try self.createPersistentStoreFromStorage( storage, finalURL: cacheFileURL, diff --git a/Sources/SQLiteStore.swift b/Sources/SQLiteStore.swift index 867a7f8b..4d1098f8 100644 --- a/Sources/SQLiteStore.swift +++ b/Sources/SQLiteStore.swift @@ -216,9 +216,9 @@ public final class SQLiteStore: LocalStorage { /** Called by the `DataStack` to perform checkpoint operations on the storage. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE to force a checkpoint. */ - public func cs_finalizeStorageAndWait(soureModelHint: NSManagedObjectModel) throws { + public func cs_finalizeStorageAndWait(sourceModelHint: NSManagedObjectModel) throws { - _ = try withExtendedLifetime(NSPersistentStoreCoordinator(managedObjectModel: soureModelHint)) { (coordinator: NSPersistentStoreCoordinator) in + _ = try withExtendedLifetime(NSPersistentStoreCoordinator(managedObjectModel: sourceModelHint)) { (coordinator: NSPersistentStoreCoordinator) in var storeOptions = self.storeOptions ?? [:] storeOptions[NSSQLitePragmasOption] = ["journal_mode": "DELETE"] @@ -235,7 +235,7 @@ public final class SQLiteStore: LocalStorage { /** Called by the `DataStack` to perform actual deletion of the store file from disk. Do not call directly! The `sourceModel` argument is a hint for the existing store's model version. For `SQLiteStore`, this converts the database's WAL journaling mode to DELETE before deleting the file. */ - public func cs_eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws { + public func cs_eraseStorageAndWait(metadata: [String: Any], sourceModelHint: NSManagedObjectModel?) throws { func deleteFiles(storeURL: URL, extraFiles: [String] = []) throws { @@ -283,9 +283,9 @@ public final class SQLiteStore: LocalStorage { let fileURL = self.fileURL try autoreleasepool { - if let soureModel = soureModelHint ?? NSManagedObjectModel.mergedModel(from: nil, forStoreMetadata: metadata) { + if let sourceModel = sourceModelHint ?? NSManagedObjectModel.mergedModel(from: nil, forStoreMetadata: metadata) { - let journalUpdatingCoordinator = NSPersistentStoreCoordinator(managedObjectModel: soureModel) + let journalUpdatingCoordinator = NSPersistentStoreCoordinator(managedObjectModel: sourceModel) var storeOptions = self.storeOptions ?? [:] storeOptions[NSSQLitePragmasOption] = ["journal_mode": "DELETE"] let store = try journalUpdatingCoordinator.addPersistentStore( diff --git a/Sources/StorageInterface.swift b/Sources/StorageInterface.swift index 441e47d6..8c2f0998 100644 --- a/Sources/StorageInterface.swift +++ b/Sources/StorageInterface.swift @@ -144,12 +144,12 @@ public protocol LocalStorage: StorageInterface { /** Called by the `DataStack` to perform checkpoint operations on the storage. (SQLite stores for example, can convert the database's WAL journaling mode to DELETE to force a checkpoint) */ - func cs_finalizeStorageAndWait(soureModelHint: NSManagedObjectModel) throws + func cs_finalizeStorageAndWait(sourceModelHint: NSManagedObjectModel) throws /** Called by the `DataStack` to perform actual deletion of the store file from disk. **Do not call directly!** The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (SQLite stores for example, can convert WAL journaling mode to DELETE before deleting) */ - func cs_eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws + func cs_eraseStorageAndWait(metadata: [String: Any], sourceModelHint: NSManagedObjectModel?) throws } extension LocalStorage { @@ -233,7 +233,7 @@ public protocol CloudStorage: StorageInterface { /** Called by the `DataStack` to perform actual deletion of the store file from disk. **Do not call directly!** The `sourceModel` argument is a hint for the existing store's model version. Implementers can use the `sourceModel` to perform necessary store operations. (Cloud stores for example, can set the NSPersistentStoreRemoveUbiquitousMetadataOption option before deleting) */ - func cs_eraseStorageAndWait(soureModel: NSManagedObjectModel) throws + func cs_eraseStorageAndWait(sourceModel: NSManagedObjectModel) throws } From b609cdd47dd197ec9f1c31d119f93bfc72d8b832 Mon Sep 17 00:00:00 2001 From: Ian Bytchek Date: Sun, 28 Apr 2019 11:08:57 +0100 Subject: [PATCH 2/6] Add migration unit test for CustomSchemaMappingProvider --- CoreStore.xcodeproj/project.pbxproj | 8 ++++ CoreStoreTests/MigrationTests.swift | 74 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 CoreStoreTests/MigrationTests.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index be1d808b..8c6c9e47 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -723,6 +723,9 @@ B5FEC18F1C9166E600532541 /* NSPersistentStore+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FEC18D1C9166E200532541 /* NSPersistentStore+Setup.swift */; }; B5FEC1901C9166E700532541 /* NSPersistentStore+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FEC18D1C9166E200532541 /* NSPersistentStore+Setup.swift */; }; B5FEC1911C9166E700532541 /* NSPersistentStore+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5FEC18D1C9166E200532541 /* NSPersistentStore+Setup.swift */; }; + E6B8DCE522229E340007BBA9 /* MigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8DCE422229E330007BBA9 /* MigrationTests.swift */; }; + E6B8DCE622229E340007BBA9 /* MigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8DCE422229E330007BBA9 /* MigrationTests.swift */; }; + E6B8DCE722229E340007BBA9 /* MigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8DCE422229E330007BBA9 /* MigrationTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -968,6 +971,7 @@ B5FE4DA61C84FB4400FA6A91 /* InMemoryStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryStore.swift; sourceTree = ""; }; B5FE4DAB1C85D44E00FA6A91 /* SQLiteStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteStore.swift; sourceTree = ""; }; B5FEC18D1C9166E200532541 /* NSPersistentStore+Setup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSPersistentStore+Setup.swift"; sourceTree = ""; }; + E6B8DCE422229E330007BBA9 /* MigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1101,6 +1105,7 @@ B5220E0B1D0D0D19009BC71E /* ImportTests.swift */, B525576B1CFAF18F00E51965 /* IntoTests.swift */, B5220E0F1D0DA6AB009BC71E /* ListObserverTests.swift */, + E6B8DCE422229E330007BBA9 /* MigrationTests.swift */, B5DC47C51C93D22900FA3BF3 /* MigrationChainTests.swift */, B5220E071D0C5F8D009BC71E /* ObjectObserverTests.swift */, B52557771D02826E00E51965 /* OrderByTests.swift */, @@ -2039,6 +2044,7 @@ B5220E0C1D0D0D19009BC71E /* ImportTests.swift in Sources */, B5D339B41E925C2B00C880DE /* DynamicModelTests.swift in Sources */, B5D372841A39CD6900F583D9 /* Model.xcdatamodeld in Sources */, + E6B8DCE522229E340007BBA9 /* MigrationTests.swift in Sources */, B52557881D02DE8100E51965 /* FetchTests.swift in Sources */, B5489F501CF603D5008B4978 /* FromTests.swift in Sources */, B52557781D02826E00E51965 /* OrderByTests.swift in Sources */, @@ -2234,6 +2240,7 @@ B5220E0D1D0D0D19009BC71E /* ImportTests.swift in Sources */, B5D339B51E925C2B00C880DE /* DynamicModelTests.swift in Sources */, B525576D1CFAF18F00E51965 /* IntoTests.swift in Sources */, + E6B8DCE622229E340007BBA9 /* MigrationTests.swift in Sources */, B580857B1CDF808D004C2EEB /* SetupTests.swift in Sources */, B52557891D02DE8100E51965 /* FetchTests.swift in Sources */, B5489F511CF603D5008B4978 /* FromTests.swift in Sources */, @@ -2429,6 +2436,7 @@ B525576E1CFAF18F00E51965 /* IntoTests.swift in Sources */, B5D339B61E925C2B00C880DE /* DynamicModelTests.swift in Sources */, B580857C1CDF808F004C2EEB /* SetupTests.swift in Sources */, + E6B8DCE722229E340007BBA9 /* MigrationTests.swift in Sources */, B525578A1D02DE8100E51965 /* FetchTests.swift in Sources */, B5220E281D1308E5009BC71E /* SectionByTests.swift in Sources */, B5489F521CF603D5008B4978 /* FromTests.swift in Sources */, diff --git a/CoreStoreTests/MigrationTests.swift b/CoreStoreTests/MigrationTests.swift new file mode 100644 index 00000000..0f4134cd --- /dev/null +++ b/CoreStoreTests/MigrationTests.swift @@ -0,0 +1,74 @@ +// +// MigrationChainTests.swift +// CoreStore +// +// Copyright © 2018 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 XCTest + +@testable +import CoreStore + + +final class MigrationTests: BaseTestCase { + func test_ThatCustomSchemaMappingProvider_CanInferTransformation() { + struct V1 { + class Animal: CoreStoreObject { + var name = Value.Required("name", initial: "") + } + } + + struct V2 { + class Animal: CoreStoreObject { + var nickname = Value.Required("nickname", initial: "", renamingIdentifier: "name") + } + } + + let schemaV1: CoreStoreSchema = CoreStoreSchema(modelVersion: "V1", entities: [Entity("Animal")]) + let schemaV2: CoreStoreSchema = CoreStoreSchema(modelVersion: "V2", entities: [Entity("Animal")]) + let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "V1", to: "V2", entityMappings: []) + + /// Create the source store and data set. + withExtendedLifetime(DataStack(schemaV1), { stack in + try! stack.addStorageAndWait(SQLiteStore()) + try! stack.perform(synchronous: { $0.create(Into()).name.value = "Willy" }) + try! stack.perform(synchronous: { XCTAssertEqual(try! $0.fetchOne(From())?.name.value, "Willy") }) + }) + + let stack: DataStack = DataStack(schemaV1, schemaV2, migrationChain: ["V1", "V2"]) + let store: SQLiteStore = SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]) + + let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete") + + _ = stack.addStorage(store, completion: { + switch $0 { + case .success(_): + XCTAssertEqual(try! stack.perform(synchronous: { try $0.fetchOne(From())?.nickname.value }), "Willy") + expectation.fulfill() + case .failure(let error): + XCTFail("\(error)") + } + }) + + self.waitAndCheckExpectations() + } +} From 242a1fa0440297c49b95e1f5765b4b55b4d2648e Mon Sep 17 00:00:00 2001 From: Ian Bytchek Date: Sun, 28 Apr 2019 20:34:50 +0100 Subject: [PATCH 3/6] Simplify migration mapping model creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify the code structure and remove autorelease blocks – they introduce a certain overhead and only make sense on loops with a large number of iterations and memory-intense data that should be disposed at the end of the block. In our case, in 99% cases there will be no more than 10 iterations (attributes and relations per entity) and most results of autorelease blocks are returned and stored into the mapping model, which defies their whole purpose in the first place. --- Sources/CustomSchemaMappingProvider.swift | 206 +++++++++------------- 1 file changed, 87 insertions(+), 119 deletions(-) diff --git a/Sources/CustomSchemaMappingProvider.swift b/Sources/CustomSchemaMappingProvider.swift index 505e530c..1ac33809 100644 --- a/Sources/CustomSchemaMappingProvider.swift +++ b/Sources/CustomSchemaMappingProvider.swift @@ -345,30 +345,23 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { // MARK: SchemaMappingProvider public func cs_createMappingModel(from sourceSchema: DynamicSchema, to destinationSchema: DynamicSchema, storage: LocalStorage) throws -> (mappingModel: NSMappingModel, migrationType: MigrationType) { - let sourceModel = sourceSchema.rawModel() let destinationModel = destinationSchema.rawModel() - let mappingModel = NSMappingModel() + let (deleteMappings, insertMappings, copyMappings, transformMappings) = self.resolveEntityMappings(sourceModel: sourceModel, destinationModel: destinationModel) - let (deleteMappings, insertMappings, copyMappings, transformMappings) = self.resolveEntityMappings( - sourceModel: sourceModel, - destinationModel: destinationModel - ) func expression(forSource sourceEntity: NSEntityDescription) -> NSExpression { - return NSExpression(format: "FETCH(FUNCTION($\(NSMigrationManagerKey), \"fetchRequestForSourceEntityNamed:predicateString:\" , \"\(sourceEntity.name!)\", \"\(NSPredicate(value: true))\"), FUNCTION($\(NSMigrationManagerKey), \"\(#selector(getter: NSMigrationManager.sourceContext))\"), \(false))") } let sourceEntitiesByName = sourceModel.entitiesByName let destinationEntitiesByName = destinationModel.entitiesByName - var entityMappings: [NSEntityMapping] = [] + for case .deleteEntity(let sourceEntityName) in deleteMappings { - let sourceEntity = sourceEntitiesByName[sourceEntityName]! - let entityMapping = NSEntityMapping() + entityMapping.sourceEntityName = sourceEntity.name entityMapping.sourceEntityVersionHash = sourceEntity.versionHash entityMapping.mappingType = .removeEntityMappingType @@ -376,93 +369,107 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { entityMappings.append(entityMapping) } + for case .insertEntity(let destinationEntityName) in insertMappings { - let destinationEntity = destinationEntitiesByName[destinationEntityName]! + var attributeMappings: [NSPropertyMapping] = [] + var relationshipMappings: [NSPropertyMapping] = [] + + for (_, destinationAttribute) in destinationEntity.attributesByName { + let propertyMapping = NSPropertyMapping() + propertyMapping.name = destinationAttribute.name + attributeMappings.append(propertyMapping) + } + + for (_, destinationRelationship) in destinationEntity.relationshipsByName { + let propertyMapping = NSPropertyMapping() + propertyMapping.name = destinationRelationship.name + relationshipMappings.append(propertyMapping) + } let entityMapping = NSEntityMapping() + entityMapping.destinationEntityName = destinationEntity.name entityMapping.destinationEntityVersionHash = destinationEntity.versionHash entityMapping.mappingType = .addEntityMappingType - entityMapping.attributeMappings = autoreleasepool { () -> [NSPropertyMapping] in - - var attributeMappings: [NSPropertyMapping] = [] - for (_, destinationAttribute) in destinationEntity.attributesByName { - - let propertyMapping = NSPropertyMapping() - propertyMapping.name = destinationAttribute.name - attributeMappings.append(propertyMapping) - } - return attributeMappings - } - entityMapping.relationshipMappings = autoreleasepool { () -> [NSPropertyMapping] in - - var relationshipMappings: [NSPropertyMapping] = [] - for (_, destinationRelationship) in destinationEntity.relationshipsByName { - - let propertyMapping = NSPropertyMapping() - propertyMapping.name = destinationRelationship.name - relationshipMappings.append(propertyMapping) - } - return relationshipMappings - } + entityMapping.attributeMappings = attributeMappings + entityMapping.relationshipMappings = relationshipMappings + entityMappings.append(entityMapping) } + for case .copyEntity(let sourceEntityName, let destinationEntityName) in copyMappings { - let sourceEntity = sourceEntitiesByName[sourceEntityName]! + let sourceAttributes = sourceEntity.cs_resolveAttributeNames() + let sourceRelationships = sourceEntity.cs_resolveRelationshipNames() let destinationEntity = destinationEntitiesByName[destinationEntityName]! + var attributeMappings: [NSPropertyMapping] = [] + var relationshipMappings: [NSPropertyMapping] = [] + + for (renamingIdentifier, destination) in destinationEntity.cs_resolveAttributeRenamingIdentities() { + let sourceAttribute = sourceAttributes[renamingIdentifier]!.attribute + let destinationAttribute = destination.attribute + let propertyMapping = NSPropertyMapping() + propertyMapping.name = destinationAttribute.name + propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceAttribute.name)\")") + attributeMappings.append(propertyMapping) + } + + for (renamingIdentifier, destination) in destinationEntity.cs_resolveRelationshipRenamingIdentities() { + let sourceRelationship = sourceRelationships[renamingIdentifier]!.relationship + let destinationRelationship = destination.relationship + let sourceRelationshipName = sourceRelationship.name + let propertyMapping = NSPropertyMapping() + propertyMapping.name = destinationRelationship.name + propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"destinationInstancesForSourceRelationshipNamed:sourceInstances:\", \"\(sourceRelationshipName)\", FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceRelationshipName)\"))") + relationshipMappings.append(propertyMapping) + } + let entityMapping = NSEntityMapping() + entityMapping.sourceEntityName = sourceEntity.name entityMapping.sourceEntityVersionHash = sourceEntity.versionHash entityMapping.destinationEntityName = destinationEntity.name entityMapping.destinationEntityVersionHash = destinationEntity.versionHash entityMapping.mappingType = .copyEntityMappingType entityMapping.sourceExpression = expression(forSource: sourceEntity) - entityMapping.attributeMappings = autoreleasepool { () -> [NSPropertyMapping] in - - let sourceAttributes = sourceEntity.cs_resolveAttributeNames() - let destinationAttributes = destinationEntity.cs_resolveAttributeRenamingIdentities() - - var attributeMappings: [NSPropertyMapping] = [] - for (renamingIdentifier, destination) in destinationAttributes { - - let sourceAttribute = sourceAttributes[renamingIdentifier]!.attribute - let destinationAttribute = destination.attribute - let propertyMapping = NSPropertyMapping() - propertyMapping.name = destinationAttribute.name - propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceAttribute.name)\")") - attributeMappings.append(propertyMapping) - } - return attributeMappings - } - entityMapping.relationshipMappings = autoreleasepool { () -> [NSPropertyMapping] in - - let sourceRelationships = sourceEntity.cs_resolveRelationshipNames() - let destinationRelationships = destinationEntity.cs_resolveRelationshipRenamingIdentities() - var relationshipMappings: [NSPropertyMapping] = [] - for (renamingIdentifier, destination) in destinationRelationships { - - let sourceRelationship = sourceRelationships[renamingIdentifier]!.relationship - let destinationRelationship = destination.relationship - let sourceRelationshipName = sourceRelationship.name - - let propertyMapping = NSPropertyMapping() - propertyMapping.name = destinationRelationship.name - propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"destinationInstancesForSourceRelationshipNamed:sourceInstances:\", \"\(sourceRelationshipName)\", FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceRelationshipName)\"))") - relationshipMappings.append(propertyMapping) - } - return relationshipMappings - } + entityMapping.attributeMappings = attributeMappings + entityMapping.relationshipMappings = relationshipMappings + entityMappings.append(entityMapping) } + for case .transformEntity(let sourceEntityName, let destinationEntityName, let transformEntity) in transformMappings { - let sourceEntity = sourceEntitiesByName[sourceEntityName]! + let sourceAttributes = sourceEntity.cs_resolveAttributeNames() + let sourceRelationships = sourceEntity.cs_resolveRelationshipNames() let destinationEntity = destinationEntitiesByName[destinationEntityName]! + let destinationAttributes = destinationEntity.cs_resolveAttributeRenamingIdentities() + let destinationRelationships = destinationEntity.cs_resolveRelationshipRenamingIdentities() + + var sourceAttributesByDestinationKey: [KeyPathString: NSAttributeDescription] = [:] + var relationshipMappings: [NSPropertyMapping] = [] + + for renamingIdentifier in Set(destinationAttributes.keys).intersection(sourceAttributes.keys) { + let sourceAttribute = sourceAttributes[renamingIdentifier]!.attribute + let destinationAttribute = destinationAttributes[renamingIdentifier]!.attribute + sourceAttributesByDestinationKey[destinationAttribute.name] = sourceAttribute + } + + for renamingIdentifier in Set(destinationRelationships.keys).intersection(sourceRelationships.keys) { + let sourceRelationship = sourceRelationships[renamingIdentifier]!.relationship + let destinationRelationship = destinationRelationships[renamingIdentifier]!.relationship + let sourceRelationshipName = sourceRelationship.name + let destinationRelationshipName = destinationRelationship.name + let propertyMapping = NSPropertyMapping() + propertyMapping.name = destinationRelationshipName + propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"destinationInstancesForSourceRelationshipNamed:sourceInstances:\", \"\(sourceRelationshipName)\", FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceRelationshipName)\"))") + relationshipMappings.append(propertyMapping) + } let entityMapping = NSEntityMapping() + entityMapping.sourceEntityName = sourceEntity.name entityMapping.sourceEntityVersionHash = sourceEntity.versionHash entityMapping.destinationEntityName = destinationEntity.name @@ -470,61 +477,22 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { entityMapping.mappingType = .customEntityMappingType entityMapping.sourceExpression = expression(forSource: sourceEntity) entityMapping.entityMigrationPolicyClassName = NSStringFromClass(CustomEntityMigrationPolicy.self) + entityMapping.relationshipMappings = relationshipMappings - var userInfo: [AnyHashable: Any] = [ - CustomEntityMigrationPolicy.UserInfoKey.transformer: transformEntity + entityMapping.userInfo = [ + CustomEntityMigrationPolicy.UserInfoKey.transformer: transformEntity, + CustomEntityMigrationPolicy.UserInfoKey.sourceAttributesByDestinationKey: sourceAttributesByDestinationKey ] - autoreleasepool { - - let sourceAttributes = sourceEntity.cs_resolveAttributeNames() - let destinationAttributes = destinationEntity.cs_resolveAttributeRenamingIdentities() - - let transformedRenamingIdentifiers = Set(destinationAttributes.keys) - .intersection(sourceAttributes.keys) - - var sourceAttributesByDestinationKey: [KeyPathString: NSAttributeDescription] = [:] - for renamingIdentifier in transformedRenamingIdentifiers { - - let sourceAttribute = sourceAttributes[renamingIdentifier]!.attribute - let destinationAttribute = destinationAttributes[renamingIdentifier]!.attribute - sourceAttributesByDestinationKey[destinationAttribute.name] = sourceAttribute - } - userInfo[CustomEntityMigrationPolicy.UserInfoKey.sourceAttributesByDestinationKey] = sourceAttributesByDestinationKey - } - entityMapping.relationshipMappings = autoreleasepool { () -> [NSPropertyMapping] in - - let sourceRelationships = sourceEntity.cs_resolveRelationshipNames() - let destinationRelationships = destinationEntity.cs_resolveRelationshipRenamingIdentities() - let transformedRenamingIdentifiers = Set(destinationRelationships.keys) - .intersection(sourceRelationships.keys) - - var relationshipMappings: [NSPropertyMapping] = [] - for renamingIdentifier in transformedRenamingIdentifiers { - - let sourceRelationship = sourceRelationships[renamingIdentifier]!.relationship - let destinationRelationship = destinationRelationships[renamingIdentifier]!.relationship - let sourceRelationshipName = sourceRelationship.name - let destinationRelationshipName = destinationRelationship.name - - let propertyMapping = NSPropertyMapping() - propertyMapping.name = destinationRelationshipName - propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"destinationInstancesForSourceRelationshipNamed:sourceInstances:\", \"\(sourceRelationshipName)\", FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceRelationshipName)\"))") - relationshipMappings.append(propertyMapping) - } - return relationshipMappings - } - entityMapping.userInfo = userInfo + entityMappings.append(entityMapping) } mappingModel.entityMappings = entityMappings - return ( - mappingModel, - .heavyweight( - sourceVersion: self.sourceVersion, - destinationVersion: self.destinationVersion - ) - ) + + return (mappingModel, .heavyweight( + sourceVersion: self.sourceVersion, + destinationVersion: self.destinationVersion + )) } From 790378f7dc63e30da30f801dfd35ee02020a5129 Mon Sep 17 00:00:00 2001 From: Ian Bytchek Date: Mon, 29 Apr 2019 12:09:43 +0100 Subject: [PATCH 4/6] Remove try from asynchronous add storage example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43a5eae7..55117b80 100644 --- a/README.md +++ b/README.md @@ -463,7 +463,7 @@ if you do so, any model mismatch will be thrown as an error. In general though, if migrations are expected the asynchronous variant `addStorage(_:completion:)` method is recommended instead: ```swift -let migrationProgress: Progress? = try dataStack.addStorage( +let migrationProgress: Progress? = dataStack.addStorage( SQLiteStore( fileName: "MyStore.sqlite", configuration: "Config2" From 4790053475cbdd26c9c52609ea84b9c8259e82ef Mon Sep 17 00:00:00 2001 From: Ian Bytchek Date: Mon, 29 Apr 2019 16:14:48 +0100 Subject: [PATCH 5/6] Remove source expression for deleted entities 1. This is causing a crash. 2. Based on the documentation it's not clear what it's doing here anyway. --- Sources/CustomSchemaMappingProvider.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/CustomSchemaMappingProvider.swift b/Sources/CustomSchemaMappingProvider.swift index 1ac33809..e5e79d97 100644 --- a/Sources/CustomSchemaMappingProvider.swift +++ b/Sources/CustomSchemaMappingProvider.swift @@ -365,7 +365,6 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { entityMapping.sourceEntityName = sourceEntity.name entityMapping.sourceEntityVersionHash = sourceEntity.versionHash entityMapping.mappingType = .removeEntityMappingType - entityMapping.sourceExpression = expression(forSource: sourceEntity) entityMappings.append(entityMapping) } From 6c7778f32e02182e46debb7ef79d7b167fb0c84d Mon Sep 17 00:00:00 2001 From: Ian Bytchek Date: Tue, 7 May 2019 07:15:34 +0100 Subject: [PATCH 6/6] Improve custom schema mapping provider 1. Improve property and relationship mapping. 2. Throw errors when property or relationship cannot be mapped, don't explicitly unwrap. 3. Add extensive schema mapping and migration tests. --- CoreStoreTests/MigrationTests.swift | 209 +++++++++++++++++--- Sources/CustomSchemaMappingProvider.swift | 60 ++---- Sources/NSEntityDescription+Migration.swift | 51 ++--- 3 files changed, 234 insertions(+), 86 deletions(-) diff --git a/CoreStoreTests/MigrationTests.swift b/CoreStoreTests/MigrationTests.swift index 0f4134cd..9374c911 100644 --- a/CoreStoreTests/MigrationTests.swift +++ b/CoreStoreTests/MigrationTests.swift @@ -29,46 +29,207 @@ import XCTest import CoreStore +// MARK: - MigrationTests + final class MigrationTests: BaseTestCase { - func test_ThatCustomSchemaMappingProvider_CanInferTransformation() { - struct V1 { - class Animal: CoreStoreObject { - var name = Value.Required("name", initial: "") - } + + func test_ThatEntityDescriptionExtension_CanMapAttributes() { + + // Should match attributes by renaming identifier. + do { + let src = NSEntityDescription([NSAttributeDescription("foo")]) + let dst = NSEntityDescription([NSAttributeDescription("bar", renamingIdentifier: "foo")]) + + var map: [NSAttributeDescription: NSAttributeDescription] = [:] + XCTAssertNoThrow(map = try dst.mapAttributes(in: src)) + XCTAssertEqual(map.count, 1) + XCTAssertEqual(map.keys.first?.renamingIdentifier, "foo") + XCTAssertEqual(map.values.first?.name, "foo") + } + + // Should match attributes by name when matching by renaming identifier fails. + do { + let src = NSEntityDescription([NSAttributeDescription("bar")]) + let dst = NSEntityDescription([NSAttributeDescription("bar", renamingIdentifier: "foo")]) + + var map: [NSAttributeDescription: NSAttributeDescription] = [:] + XCTAssertNoThrow(map = try dst.mapAttributes(in: src)) + XCTAssertEqual(map.count, 1) + XCTAssertEqual(map.keys.first?.renamingIdentifier, "foo") + XCTAssertEqual(map.keys.first?.name, "bar") + XCTAssertEqual(map.values.first?.name, "bar") + } + + // Should not throw exception when optional attributes cannot be matched. + do { + let src = NSEntityDescription([NSAttributeDescription("foo")]) + let dst = NSEntityDescription([NSAttributeDescription("bar")]) + + var map: [NSAttributeDescription: NSAttributeDescription] = [:] + XCTAssertNoThrow(map = try dst.mapAttributes(in: src)) + XCTAssertEqual(map.count, 0) + } + + // Should not throw exception when required attributes with default value cannot be matched. + do { + let src = NSEntityDescription([NSAttributeDescription("foo")]) + let dst = NSEntityDescription([NSAttributeDescription("bar", optional: false, defaultValue: "baz")]) + + var map: [NSAttributeDescription: NSAttributeDescription] = [:] + XCTAssertNoThrow(map = try dst.mapAttributes(in: src)) + XCTAssertEqual(map.count, 0) + } + + // Should throw exception when required attributes without default value cannot be matched. + do { + let src = NSEntityDescription([NSAttributeDescription("foo")]) + let dst = NSEntityDescription([NSAttributeDescription("bar", optional: false)]) + XCTAssertThrowsError(try dst.mapAttributes(in: src)) + } + } + + func test_ThatCustomSchemaMappingProvider_CanDeleteAndInsertEntitiesWithCustomEntityMapping() { + class Foo: CoreStoreObject { + var name = Value.Optional("name") + } + + class Bar: CoreStoreObject { + var nickname = Value.Optional("nickname", renamingIdentifier: "name") } - struct V2 { - class Animal: CoreStoreObject { - var nickname = Value.Required("nickname", initial: "", renamingIdentifier: "name") - } + let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity("Foo")]) + let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity("Bar")]) + + let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [ + .deleteEntity(sourceEntity: "Foo"), + .insertEntity(destinationEntity: "Bar") + ]) + + /// Create the source store and data set. + withExtendedLifetime(DataStack(src), { stack in + try! stack.addStorageAndWait(SQLiteStore()) + try! stack.perform(synchronous: { $0.create(Into()).name.value = "Willy" }) + }) + + let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete") + + withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in + _ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: { + switch $0 { + case .success(_): + XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1) + try! stack.perform(synchronous: { $0.create(Into()).nickname.value = "Bobby" }) + case .failure(let error): + XCTFail("\(error)") + } + expectation.fulfill() + }) + }) + + self.waitAndCheckExpectations() + } + + func test_ThatCustomSchemaMappingProvider_CanCopyEntityWithCustomEntityMapping() { + class Foo: CoreStoreObject { + var name = Value.Required("name", initial: "") } - let schemaV1: CoreStoreSchema = CoreStoreSchema(modelVersion: "V1", entities: [Entity("Animal")]) - let schemaV2: CoreStoreSchema = CoreStoreSchema(modelVersion: "V2", entities: [Entity("Animal")]) - let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "V1", to: "V2", entityMappings: []) + // Todo: The way this handles different version locks is flaky… It fails face on the ground in debug, but seems + // todo: to work fine in production, yet it's not clear if it transforms everything as expected. + + let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity("Foo")]) + let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity("Foo")]) + + XCTAssertEqual(dst.rawModel().entities.first!.versionHash, src.rawModel().entities.first!.versionHash) + + let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [ + .copyEntity(sourceEntity: "Foo", destinationEntity: "Foo") + ]) /// Create the source store and data set. - withExtendedLifetime(DataStack(schemaV1), { stack in + withExtendedLifetime(DataStack(src), { stack in try! stack.addStorageAndWait(SQLiteStore()) - try! stack.perform(synchronous: { $0.create(Into()).name.value = "Willy" }) - try! stack.perform(synchronous: { XCTAssertEqual(try! $0.fetchOne(From())?.name.value, "Willy") }) + try! stack.perform(synchronous: { $0.create(Into()).name.value = "Willy" }) }) - let stack: DataStack = DataStack(schemaV1, schemaV2, migrationChain: ["V1", "V2"]) - let store: SQLiteStore = SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]) + let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete") + + withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in + _ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: { + switch $0 { + case .success(_): + XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1) + XCTAssertEqual(try! stack.fetchCount(From()), 1) + try! stack.perform(synchronous: { $0.create(Into()).name.value = "Bobby" }) + case .failure(let error): + XCTFail("\(error)") + } + expectation.fulfill() + }) + }) + + self.waitAndCheckExpectations() + } + + func test_ThatCustomSchemaMappingProvider_CanTransformEntityWithCustomEntityMapping() { + class Foo: CoreStoreObject { + var name = Value.Required("name", initial: "") + var futile = Value.Required("futile", initial: "") + } + + class Bar: CoreStoreObject { + var firstName = Value.Required("firstName", initial: "", renamingIdentifier: "name") + var lastName = Value.Required("lastName", initial: "", renamingIdentifier: "placeholder") + var age = Value.Required("age", initial: 18) + var gender = Value.Optional("gender") + } + + let src: CoreStoreSchema = CoreStoreSchema(modelVersion: "1", entities: [Entity("Foo")]) + let dst: CoreStoreSchema = CoreStoreSchema(modelVersion: "2", entities: [Entity("Bar")]) + + let migration: CustomSchemaMappingProvider = CustomSchemaMappingProvider(from: "1", to: "2", entityMappings: [ + .transformEntity(sourceEntity: "Foo", destinationEntity: "Bar", transformer: CustomSchemaMappingProvider.CustomMapping.inferredTransformation) + ]) + + /// Create the source store and data set. + withExtendedLifetime(DataStack(src), { stack in + try! stack.addStorageAndWait(SQLiteStore()) + try! stack.perform(synchronous: { $0.create(Into()).name.value = "Willy" }) + }) let expectation: XCTestExpectation = self.expectation(description: "migration-did-complete") - _ = stack.addStorage(store, completion: { - switch $0 { - case .success(_): - XCTAssertEqual(try! stack.perform(synchronous: { try $0.fetchOne(From())?.nickname.value }), "Willy") + withExtendedLifetime(DataStack(src, dst, migrationChain: ["1", "2"]), { stack in + _ = stack.addStorage(SQLiteStore(fileURL: SQLiteStore.defaultFileURL, migrationMappingProviders: [migration]), completion: { + switch $0 { + case .success(_): + XCTAssertEqual(stack.modelSchema.rawModel().entities.count, 1) + XCTAssertEqual(try! stack.fetchCount(From()), 1) + try! stack.perform(synchronous: { $0.create(Into()).firstName.value = "Bobby" }) + case .failure(let error): + XCTFail("\(error)") + } expectation.fulfill() - case .failure(let error): - XCTFail("\(error)") - } + }) }) self.waitAndCheckExpectations() } } + +extension NSEntityDescription { + fileprivate convenience init(_ properties: [NSPropertyDescription]) { + self.init() + self.properties = properties + } +} + +extension NSAttributeDescription { + fileprivate convenience init(_ name: String, renamingIdentifier: String? = nil, optional: Bool? = nil, defaultValue: Any? = nil) { + self.init() + self.name = name + self.renamingIdentifier = renamingIdentifier + self.isOptional = optional ?? true + self.defaultValue = defaultValue + } +} diff --git a/Sources/CustomSchemaMappingProvider.swift b/Sources/CustomSchemaMappingProvider.swift index e5e79d97..eb02af97 100644 --- a/Sources/CustomSchemaMappingProvider.swift +++ b/Sources/CustomSchemaMappingProvider.swift @@ -350,6 +350,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { let mappingModel = NSMappingModel() let (deleteMappings, insertMappings, copyMappings, transformMappings) = self.resolveEntityMappings(sourceModel: sourceModel, destinationModel: destinationModel) + // Todo: What is this? Why is it needed? How does it work? func expression(forSource sourceEntity: NSEntityDescription) -> NSExpression { return NSExpression(format: "FETCH(FUNCTION($\(NSMigrationManagerKey), \"fetchRequestForSourceEntityNamed:predicateString:\" , \"\(sourceEntity.name!)\", \"\(NSPredicate(value: true))\"), FUNCTION($\(NSMigrationManagerKey), \"\(#selector(getter: NSMigrationManager.sourceContext))\"), \(false))") } @@ -358,6 +359,8 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { let destinationEntitiesByName = destinationModel.entitiesByName var entityMappings: [NSEntityMapping] = [] + // Todo: Throw errors, don't do explicit unwraps!!! + for case .deleteEntity(let sourceEntityName) in deleteMappings { let sourceEntity = sourceEntitiesByName[sourceEntityName]! let entityMapping = NSEntityMapping() @@ -399,29 +402,22 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { for case .copyEntity(let sourceEntityName, let destinationEntityName) in copyMappings { let sourceEntity = sourceEntitiesByName[sourceEntityName]! - let sourceAttributes = sourceEntity.cs_resolveAttributeNames() - let sourceRelationships = sourceEntity.cs_resolveRelationshipNames() let destinationEntity = destinationEntitiesByName[destinationEntityName]! var attributeMappings: [NSPropertyMapping] = [] var relationshipMappings: [NSPropertyMapping] = [] - for (renamingIdentifier, destination) in destinationEntity.cs_resolveAttributeRenamingIdentities() { - let sourceAttribute = sourceAttributes[renamingIdentifier]!.attribute - let destinationAttribute = destination.attribute + for (destinationAttribute, sourceAttribute) in try destinationEntity.mapAttributes(in: sourceEntity) { let propertyMapping = NSPropertyMapping() propertyMapping.name = destinationAttribute.name propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceAttribute.name)\")") attributeMappings.append(propertyMapping) } - for (renamingIdentifier, destination) in destinationEntity.cs_resolveRelationshipRenamingIdentities() { - let sourceRelationship = sourceRelationships[renamingIdentifier]!.relationship - let destinationRelationship = destination.relationship - let sourceRelationshipName = sourceRelationship.name + for (destinationRelationship, sourceRelationship) in try destinationEntity.mapRelationships(in: sourceEntity) { let propertyMapping = NSPropertyMapping() propertyMapping.name = destinationRelationship.name - propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"destinationInstancesForSourceRelationshipNamed:sourceInstances:\", \"\(sourceRelationshipName)\", FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceRelationshipName)\"))") + propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"destinationInstancesForSourceRelationshipNamed:sourceInstances:\", \"\(sourceRelationship.name)\", FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceRelationship.name)\"))") relationshipMappings.append(propertyMapping) } @@ -441,29 +437,19 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { for case .transformEntity(let sourceEntityName, let destinationEntityName, let transformEntity) in transformMappings { let sourceEntity = sourceEntitiesByName[sourceEntityName]! - let sourceAttributes = sourceEntity.cs_resolveAttributeNames() - let sourceRelationships = sourceEntity.cs_resolveRelationshipNames() let destinationEntity = destinationEntitiesByName[destinationEntityName]! - let destinationAttributes = destinationEntity.cs_resolveAttributeRenamingIdentities() - let destinationRelationships = destinationEntity.cs_resolveRelationshipRenamingIdentities() - var sourceAttributesByDestinationKey: [KeyPathString: NSAttributeDescription] = [:] + var attributeMappings: [KeyPathString: NSAttributeDescription] = [:] var relationshipMappings: [NSPropertyMapping] = [] - for renamingIdentifier in Set(destinationAttributes.keys).intersection(sourceAttributes.keys) { - let sourceAttribute = sourceAttributes[renamingIdentifier]!.attribute - let destinationAttribute = destinationAttributes[renamingIdentifier]!.attribute - sourceAttributesByDestinationKey[destinationAttribute.name] = sourceAttribute + for (destinationAttribute, sourceAttribute) in try destinationEntity.mapAttributes(in: sourceEntity) { + attributeMappings[destinationAttribute.name] = sourceAttribute } - for renamingIdentifier in Set(destinationRelationships.keys).intersection(sourceRelationships.keys) { - let sourceRelationship = sourceRelationships[renamingIdentifier]!.relationship - let destinationRelationship = destinationRelationships[renamingIdentifier]!.relationship - let sourceRelationshipName = sourceRelationship.name - let destinationRelationshipName = destinationRelationship.name + for (destinationRelationship, sourceRelationship) in try destinationEntity.mapRelationships(in: sourceEntity) { let propertyMapping = NSPropertyMapping() - propertyMapping.name = destinationRelationshipName - propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"destinationInstancesForSourceRelationshipNamed:sourceInstances:\", \"\(sourceRelationshipName)\", FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceRelationshipName)\"))") + propertyMapping.name = destinationRelationship.name + propertyMapping.valueExpression = NSExpression(format: "FUNCTION($\(NSMigrationManagerKey), \"destinationInstancesForSourceRelationshipNamed:sourceInstances:\", \"\(sourceRelationship.name)\", FUNCTION($\(NSMigrationSourceObjectKey), \"\(#selector(NSManagedObject.value(forKey:)))\", \"\(sourceRelationship.name)\"))") relationshipMappings.append(propertyMapping) } @@ -480,7 +466,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { entityMapping.userInfo = [ CustomEntityMigrationPolicy.UserInfoKey.transformer: transformEntity, - CustomEntityMigrationPolicy.UserInfoKey.sourceAttributesByDestinationKey: sourceAttributesByDestinationKey + CustomEntityMigrationPolicy.UserInfoKey.sourceAttributesByDestinationKey: attributeMappings ] entityMappings.append(entityMapping) @@ -552,7 +538,6 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { private let entityMappings: Set private func resolveEntityMappings(sourceModel: NSManagedObjectModel, destinationModel: NSManagedObjectModel) -> (delete: Set, insert: Set, copy: Set, transform: Set) { - var deleteMappings: Set = [] var insertMappings: Set = [] var copyMappings: Set = [] @@ -565,19 +550,13 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { let destinationRenamingIdentifiers = destinationModel.cs_resolveRenamingIdentities() let destinationEntityNames = destinationModel.entitiesByName - let removedRenamingIdentifiers = Set(sourceRenamingIdentifiers.keys) - .subtracting(destinationRenamingIdentifiers.keys) - let addedRenamingIdentifiers = Set(destinationRenamingIdentifiers.keys) - .subtracting(sourceRenamingIdentifiers.keys) - let transformedRenamingIdentifiers = Set(destinationRenamingIdentifiers.keys) - .subtracting(addedRenamingIdentifiers) - .subtracting(removedRenamingIdentifiers) + let removedRenamingIdentifiers = Set(sourceRenamingIdentifiers.keys).subtracting(destinationRenamingIdentifiers.keys) + let addedRenamingIdentifiers = Set(destinationRenamingIdentifiers.keys).subtracting(sourceRenamingIdentifiers.keys) + let transformedRenamingIdentifiers = Set(destinationRenamingIdentifiers.keys).subtracting(addedRenamingIdentifiers).subtracting(removedRenamingIdentifiers) // First pass: resolve source-destination entities for mapping in self.entityMappings { - switch mapping { - case .deleteEntity(let sourceEntity): CoreStore.assert( sourceEntityNames[sourceEntity] != nil, @@ -649,8 +628,8 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { allMappedDestinationKeys[destinationEntity] = sourceEntity } } + for renamingIdentifier in transformedRenamingIdentifiers { - let sourceEntity = sourceRenamingIdentifiers[renamingIdentifier]!.entity let destinationEntity = destinationRenamingIdentifiers[renamingIdentifier]!.entity let sourceEntityName = sourceEntity.name! @@ -691,8 +670,8 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { continue } } + for renamingIdentifier in removedRenamingIdentifiers { - let sourceEntity = sourceRenamingIdentifiers[renamingIdentifier]!.entity let sourceEntityName = sourceEntity.name! switch allMappedSourceKeys[sourceEntityName] { @@ -705,8 +684,8 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { continue } } + for renamingIdentifier in addedRenamingIdentifiers { - let destinationEntity = destinationRenamingIdentifiers[renamingIdentifier]!.entity let destinationEntityName = destinationEntity.name! switch allMappedDestinationKeys[destinationEntityName] { @@ -719,6 +698,7 @@ public class CustomSchemaMappingProvider: Hashable, SchemaMappingProvider { continue } } + return (deleteMappings, insertMappings, copyMappings, transformMappings) } } diff --git a/Sources/NSEntityDescription+Migration.swift b/Sources/NSEntityDescription+Migration.swift index 801ce25c..37dfe63c 100644 --- a/Sources/NSEntityDescription+Migration.swift +++ b/Sources/NSEntityDescription+Migration.swift @@ -31,31 +31,38 @@ import Foundation extension NSEntityDescription { - @nonobjc - internal func cs_resolveAttributeNames() -> [String: (attribute: NSAttributeDescription, versionHash: Data)] { - return self.attributesByName.reduce(into: [:], { (result, attribute: (name: String, description: NSAttributeDescription)) in - result[attribute.name] = (attribute.description, attribute.description.versionHash) + /// Maps attributes of the current entity with the earlier-version source entity. Todo: Log a warning if the renaming identifier is used but not found in source. + internal func mapAttributes(in sourceEntity: NSEntityDescription) throws -> [NSAttributeDescription: NSAttributeDescription] { + let sourceAttributes: [String: NSAttributeDescription] = sourceEntity.attributesByName + return try self.properties.lazy.compactMap({ $0 as? NSAttributeDescription }).reduce(into: [:], { (result, attribute: NSAttributeDescription) in + if let sourceAttribute = attribute.renamingIdentifier.flatMap({ sourceAttributes[$0] }) ?? sourceAttributes[attribute.name] { + result[attribute] = sourceAttribute + } else if !attribute.isOptional && attribute.defaultValue == nil { + throw MappingError.cannotMapAttribute(attribute: attribute) + } }) } - @nonobjc - internal func cs_resolveAttributeRenamingIdentities() -> [String: (attribute: NSAttributeDescription, versionHash: Data)] { - return self.attributesByName.reduce(into: [:], { (result, attribute: (name: String, description: NSAttributeDescription)) in - result[attribute.description.renamingIdentifier ?? attribute.name] = (attribute.description, attribute.description.versionHash) - }) - } - - @nonobjc - internal func cs_resolveRelationshipNames() -> [String: (relationship: NSRelationshipDescription, versionHash: Data)] { - return self.relationshipsByName.reduce(into: [:], { (result, relationship: (name: String, description: NSRelationshipDescription)) in - result[relationship.name] = (relationship.description, relationship.description.versionHash) - }) - } - - @nonobjc - internal func cs_resolveRelationshipRenamingIdentities() -> [String: (relationship: NSRelationshipDescription, versionHash: Data)] { - return self.relationshipsByName.reduce(into: [:], { (result, relationship: (name: String, description: NSRelationshipDescription)) in - result[relationship.description.renamingIdentifier ?? relationship.name] = (relationship.description, relationship.description.versionHash) + /// Maps relationships of the current entity with the earlier-version source entity. Todo: Log a warning if the renaming identifier is used but not found in source. + internal func mapRelationships(in sourceEntity: NSEntityDescription) throws -> [NSRelationshipDescription: NSRelationshipDescription] { + let sourceRelationships: [String: NSRelationshipDescription] = sourceEntity.relationshipsByName + return try self.properties.lazy.compactMap({ $0 as? NSRelationshipDescription }).reduce(into: [:], { (result, relationship: NSRelationshipDescription) in + if let sourceRelationship = relationship.renamingIdentifier.flatMap({ sourceRelationships[$0] }) ?? sourceRelationships[relationship.name] { + result[relationship] = sourceRelationship + } else if !relationship.isOptional { + throw MappingError.cannotMapRelationship(relationship: relationship) + } }) } } + +extension NSEntityDescription { + internal enum MappingError: Swift.Error { + + /// The required attribute without default value could not be mapped in source entity. + case cannotMapAttribute(attribute: NSAttributeDescription) + + /// The required relationship could not be mapped in source entity. + case cannotMapRelationship(relationship: NSRelationshipDescription) + } +} \ No newline at end of file