From dbaf5f337b422f7a194db8c2e38557e2324b5a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 16:02:31 +0200 Subject: [PATCH 1/9] Add a test for the circular reference It doesn't actually fail though :-) --- .../ManagedModelMacrosTests.swift | 169 +++++++++++------- 1 file changed, 109 insertions(+), 60 deletions(-) diff --git a/Tests/ManagedModelMacrosTests/ManagedModelMacrosTests.swift b/Tests/ManagedModelMacrosTests/ManagedModelMacrosTests.swift index 39dd66e..a2f0e16 100644 --- a/Tests/ManagedModelMacrosTests/ManagedModelMacrosTests.swift +++ b/Tests/ManagedModelMacrosTests/ManagedModelMacrosTests.swift @@ -23,7 +23,7 @@ import SwiftSyntaxMacroExpansion final class ModelMacroTests: XCTestCase { - #if canImport(ManagedModelMacros) +#if canImport(ManagedModelMacros) let macros : [ String: Macro.Type] = [ "Model" : ModelMacro .self, "Attribute" : AttributeMacro .self, @@ -31,12 +31,12 @@ final class ModelMacroTests: XCTestCase { "Transient" : TransientMacro .self, "_PersistedProperty" : PersistedPropertyMacro.self ] - #endif - +#endif + func testPersonAddressModels() throws { - #if !canImport(ManagedModelMacros) +#if !canImport(ManagedModelMacros) throw XCTSkip("macros are only supported when running tests for the host platform") - #else +#else let explodedFile = parseAndExplode( """ @Model @@ -47,7 +47,7 @@ final class ModelMacroTests: XCTestCase { } @Model - final class Address /*test*/ : NSManagedObject { + final class Address /*test*/ : NSManagedObject { var street : String var appartment : String? var person : Person @@ -58,7 +58,7 @@ final class ModelMacroTests: XCTestCase { // Hm, this doesn't seem to work? let diags = ParseDiagnosticsGenerator.diagnostics(for: explodedFile) XCTAssertTrue(diags.isEmpty) - + let explodedSource = explodedFile.description XCTAssertTrue (explodedSource.contains( "extension Person: ManagedModels.PersistentModel")) @@ -77,18 +77,18 @@ final class ModelMacroTests: XCTestCase { """ )) - #if false +#if false print("Exploded:---\n") print(explodedSource) print("\n-----") - #endif - #endif // canImport(ManagedModelMacros) +#endif +#endif // canImport(ManagedModelMacros) } - + func testPersonModelWithExtras() throws { - #if !canImport(ManagedModelMacros) +#if !canImport(ManagedModelMacros) throw XCTSkip("macros are only supported when running tests for the host platform") - #else +#else let explodedFile = parseAndExplode( """ enum MySchema { @@ -118,7 +118,7 @@ final class ModelMacroTests: XCTestCase { // Hm, this doesn't seem to work? let diags = ParseDiagnosticsGenerator.diagnostics(for: explodedFile) XCTAssertTrue(diags.isEmpty) - + let explodedSource = explodedFile.description XCTAssertTrue(explodedSource.contains( "extension Person: ManagedModels.PersistentModel")) @@ -131,18 +131,18 @@ final class ModelMacroTests: XCTestCase { """ )) - #if false +#if false print("Exploded:---\n") print(explodedSource) print("\n-----") - #endif - #endif // canImport(ManagedModelMacros) +#endif +#endif // canImport(ManagedModelMacros) } func testOwnInit() throws { - #if !canImport(ManagedModelMacros) +#if !canImport(ManagedModelMacros) throw XCTSkip("macros are only supported when running tests for the host platform") - #else +#else let explodedFile = parseAndExplode( """ @Model @@ -172,7 +172,7 @@ final class ModelMacroTests: XCTestCase { if !diags.isEmpty { print("DIAGS:", diags) } - + let explodedSource = explodedFile.description XCTAssertFalse(explodedSource.contains("init() {")) XCTAssertFalse(explodedSource.contains("convenience init(context:")) @@ -186,20 +186,20 @@ final class ModelMacroTests: XCTestCase { override init(entity: CoreData.NSEntityDescription, """ )) - - #if false + +#if false print("Exploded:---\n") print(explodedSource) print("\n-----") - #endif - #endif // canImport(ManagedModelMacros) +#endif +#endif // canImport(ManagedModelMacros) } - + func testNoInit() throws { - #if !canImport(ManagedModelMacros) +#if !canImport(ManagedModelMacros) throw XCTSkip("macros are only supported when running tests for the host platform") - #else +#else let explodedFile = parseAndExplode( """ @Model @@ -217,7 +217,7 @@ final class ModelMacroTests: XCTestCase { if !diags.isEmpty { print("DIAGS:", diags) } - + let explodedSource = explodedFile.description XCTAssertTrue (explodedSource.contains("init() {")) XCTAssertFalse(explodedSource.contains("convenience init(context:")) @@ -231,19 +231,19 @@ final class ModelMacroTests: XCTestCase { override init(entity: CoreData.NSEntityDescription, """ )) - - #if false + +#if false print("Exploded:---\n") print(explodedSource) print("\n-----") - #endif - #endif // canImport(ManagedModelMacros) +#endif +#endif // canImport(ManagedModelMacros) } - + func testComputedProperty() throws { - #if !canImport(ManagedModelMacros) +#if !canImport(ManagedModelMacros) throw XCTSkip("macros are only supported when running tests for the host platform") - #else +#else let explodedFile = parseAndExplode( """ @Model @@ -261,7 +261,7 @@ final class ModelMacroTests: XCTestCase { if !diags.isEmpty { print("DIAGS:", diags) } - + let explodedSource = explodedFile.description XCTAssertTrue (explodedSource.contains("init() {")) XCTAssertFalse(explodedSource.contains("convenience init(context:")) @@ -281,19 +281,19 @@ final class ModelMacroTests: XCTestCase { var fullname : String { firstname + " " + lastname } """ )) - - #if false + +#if false print("Exploded:---\n") print(explodedSource) print("\n-----") - #endif - #endif // canImport(ManagedModelMacros) +#endif +#endif // canImport(ManagedModelMacros) } - + func testForceUnwrapType() throws { - #if !canImport(ManagedModelMacros) +#if !canImport(ManagedModelMacros) throw XCTSkip("macros are only supported when running tests for the host platform") - #else +#else let explodedFile = parseAndExplode( """ @Model @@ -310,7 +310,7 @@ final class ModelMacroTests: XCTestCase { if !diags.isEmpty { print("DIAGS:", diags) } - + let explodedSource = explodedFile.description XCTAssertTrue (explodedSource.contains( "name: \"person\", valueType: Person?.self" @@ -318,19 +318,19 @@ final class ModelMacroTests: XCTestCase { XCTAssertFalse(explodedSource.contains( "name: \"person\", valueType: Person!.self" )) - - #if false + +#if false print("Exploded:---\n") print(explodedSource) print("\n-----") - #endif - #endif // canImport(ManagedModelMacros) +#endif +#endif // canImport(ManagedModelMacros) } - + func testCommentInType() throws { - #if !canImport(ManagedModelMacros) +#if !canImport(ManagedModelMacros) throw XCTSkip("macros are only supported when running tests for the host platform") - #else +#else let explodedFile = parseAndExplode( """ @Model @@ -347,7 +347,7 @@ final class ModelMacroTests: XCTestCase { if !diags.isEmpty { print("DIAGS:", diags) } - + let explodedSource = explodedFile.description XCTAssertTrue (explodedSource.contains( """ @@ -359,22 +359,22 @@ final class ModelMacroTests: XCTestCase { originalName: "CustomerTypeID", name: "typeID", valueType: String // pkey in original.self """ )) - - #if false + +#if false print("Exploded:---\n") print(explodedSource) print("\n-----") - #endif - #endif // canImport(ManagedModelMacros) +#endif +#endif // canImport(ManagedModelMacros) } - + // MARK: - Helper func parseAndExplode(_ source: String) -> Syntax { // Parse the original source file. let sourceFile : SourceFileSyntax = Parser.parse(source: source) - + // Expand all macros in the source. let context = BasicMacroExpansionContext( sourceFiles: [ @@ -384,13 +384,62 @@ final class ModelMacroTests: XCTestCase { ) ] ) - + let explodedFile : Syntax = sourceFile.expand( macros: macros, in: context, indentationWidth: .spaces(2) // what else! ) - + return explodedFile } + + // Note: This does not fail the test, but it does fail the compiler. + // https://github.com/Data-swift/ManagedModels/issues/18 + func testPersonAddressModelsWithInverse() throws { + #if !canImport(ManagedModelMacros) + throw XCTSkip("macros are only supported when running tests for the host platform") + #else + let explodedFile = parseAndExplode( + """ + @Model class Person: NSManagedObject { + @Relationship(inverse: \\Address.person) + var addresses : [ Address ] + } + + @Model class Address: NSManagedObject { + @Relationship(inverse: \\Person.addresses) + var person : Person + } + """ + ) + + // Hm, this doesn't seem to work? + // It should raise the + // > Circular reference resolving attached macro 'Relationship' + // error. Maybe the + let diags = ParseDiagnosticsGenerator.diagnostics(for: explodedFile) + XCTAssertTrue(diags.isEmpty) + + let explodedSource = explodedFile.description + XCTAssertTrue (explodedSource.contains( + "extension Person: ManagedModels.PersistentModel")) + XCTAssertTrue (explodedSource.contains( + """ + metadata: CoreData.NSRelationshipDescription(inverse: \\Address.person + """ + )) + XCTAssertTrue(explodedSource.contains( + """ + metadata: CoreData.NSRelationshipDescription(inverse: \\Person.addresses, name: "person", valueType: Person.self) + """ + )) + + #if false + print("Exploded:---\n") + print(explodedSource) + print("\n-----") + #endif + #endif // canImport(ManagedModelMacros) + } } From 3d566f64471744ae780a0eff4a52a25dca6844e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 17:27:58 +0200 Subject: [PATCH 2/9] Test optional string generation Seems to work. --- .../SchemaGenerationTests.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Tests/ManagedModelTests/SchemaGenerationTests.swift b/Tests/ManagedModelTests/SchemaGenerationTests.swift index c688d3c..bdcb725 100644 --- a/Tests/ManagedModelTests/SchemaGenerationTests.swift +++ b/Tests/ManagedModelTests/SchemaGenerationTests.swift @@ -159,6 +159,34 @@ final class SchemaGenerationTests: XCTestCase { XCTAssertFalse(lastname.isRelationship) } + func testOptionalString() throws { + let cache = SchemaBuilder() + let schema = NSManagedObjectModel( + [ Fixtures.PersonAddressSchema.Person.self ], + schemaCache: cache + ) + + XCTAssertEqual(schema.entities.count, 2) + XCTAssertEqual(schema.entitiesByName.count, 2) + + let address = try XCTUnwrap(schema.entitiesByName["Address"]) + XCTAssertEqual(address.attributes.count, 2) + + let appartment = try XCTUnwrap(address.attributesByName["appartment"]) + XCTAssertFalse(appartment.isTransient) + XCTAssertFalse(appartment.isRelationship) + XCTAssertTrue (appartment.isAttribute) + XCTAssertTrue (appartment.isOptional) + XCTAssertEqual(appartment.attributeType, .stringAttributeType) + + let street = try XCTUnwrap(address.attributesByName["street"]) + XCTAssertFalse(street.isTransient) + XCTAssertFalse(street.isRelationship) + XCTAssertTrue (street.isAttribute) + XCTAssertFalse(street.isOptional) + XCTAssertEqual(street.attributeType, .stringAttributeType) + } + func testMOM() throws { let mom = Fixtures.PersonAddressMOM XCTAssertEqual(mom.entities.count, 2) From 16e4f996386c274c9f05de62b128dbb1210781de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 20:14:28 +0200 Subject: [PATCH 3/9] Potential fix for optional toOne values Maybe a T? is injected as an existential even though `PersistentModel` requires NSManagedObject (i.e. is a class type). Might fix issue #19. --- .../PersistentModel/PersistentModel+KVC.swift | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift b/Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift index 27d712d..efda9f6 100644 --- a/Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift +++ b/Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift @@ -64,11 +64,11 @@ public extension PersistentModel { @inlinable func setValue(forKey key: String, to model: T) where T: PersistentModel { - _setValue(forKey: key, to: model) + _setOptionalToOneValue(forKey: key, to: model) } @inlinable func getValue(forKey key: String) -> T where T: PersistentModel { - guard let value : T = _getValue(forKey: key) else { + guard let value : T = _getOptionalToOneValue(forKey: key) else { fatalError("Non-optional toOne relationship contains nil value?!") } return value @@ -76,11 +76,11 @@ public extension PersistentModel { @inlinable func setValue(forKey key: String, to model: T?) where T: PersistentModel { - _setValue(forKey: key, to: model) + _setOptionalToOneValue(forKey: key, to: model) } @inlinable func getValue(forKey key: String) -> T? where T: PersistentModel { - _getValue(forKey: key) + _getOptionalToOneValue(forKey: key) } // Codable disambiguation @@ -89,13 +89,13 @@ public extension PersistentModel { func setValue(forKey key: String, to model: T) where T: PersistentModel & Encodable { - _setValue(forKey: key, to: model) + _setOptionalToOneValue(forKey: key, to: model) } @inlinable func getValue(forKey key: String) -> T where T: PersistentModel & Encodable { - guard let value : T = _getValue(forKey: key) else { + guard let value : T = _getOptionalToOneValue(forKey: key) else { fatalError("Non-optional toOne relationship contains nil value?!") } return value @@ -105,25 +105,34 @@ public extension PersistentModel { func setValue(forKey key: String, to model: T?) where T: PersistentModel & Encodable { - _setValue(forKey: key, to: model) + _setOptionalToOneValue(forKey: key, to: model) } @inlinable func getValue(forKey key: String) -> T? where T: PersistentModel & Encodable { - _getValue(forKey: key) + _getOptionalToOneValue(forKey: key) } // Primitives @inlinable - func _setValue(forKey key: String, to model: T?) where T: PersistentModel { + func _setOptionalToOneValue(forKey key: String, to model: T?) + where T: PersistentModel + { willChangeValue(forKey: key); defer { didChangeValue(forKey: key) } - setPrimitiveValue(model, forKey: key) + if let model { + setPrimitiveValue(model, forKey: key) + } + else { + setPrimitiveValue(nil, forKey: key) + } } @inlinable - func _getValue(forKey key: String) -> T? where T: PersistentModel { + func _getOptionalToOneValue(forKey key: String) -> T? + where T: PersistentModel + { willAccessValue(forKey: key); defer { didAccessValue(forKey: key) } guard let model = primitiveValue(forKey: key) else { return nil } guard let typed = model as? T else { From 5575eba6b4c31ccea5996afe8a89cc91af2249de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 20:15:46 +0200 Subject: [PATCH 4/9] Properly process frozen entities They just got skipped from the result set, which is wrong. --- .../ManagedModels/SchemaGeneration/SchemaBuilder.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/ManagedModels/SchemaGeneration/SchemaBuilder.swift b/Sources/ManagedModels/SchemaGeneration/SchemaBuilder.swift index 8e48fc0..ccc1b46 100644 --- a/Sources/ManagedModels/SchemaGeneration/SchemaBuilder.swift +++ b/Sources/ManagedModels/SchemaGeneration/SchemaBuilder.swift @@ -119,11 +119,17 @@ public final class SchemaBuilder { entities: inout [ NSEntityDescription ]) { // Note: This is called recursively - var allFrozen = false + var allFrozen = true // Create the basic entity and property data for modelType in modelTypes { - guard !isFrozen(modelType) else { continue } + if isFrozen(modelType) { + if let entity = lookupEntity(modelType) { + entities.append(entity) + continue + } + assertionFailure("Type frozen, but no entity found?") + } allFrozen = false if let newEntity = processModel(modelType) { entities.append(newEntity) From 3f9eb1fd4ca960214af786d1f8e4d9b54e669d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 20:35:34 +0200 Subject: [PATCH 5/9] Another fix for duplicate builder runs Seems to work now, though it can't be used anyways ;-) --- .../SchemaGeneration/SchemaBuilder.swift | 15 ++++++++-- .../SchemaGenerationTests.swift | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Sources/ManagedModels/SchemaGeneration/SchemaBuilder.swift b/Sources/ManagedModels/SchemaGeneration/SchemaBuilder.swift index ccc1b46..c5e0ff5 100644 --- a/Sources/ManagedModels/SchemaGeneration/SchemaBuilder.swift +++ b/Sources/ManagedModels/SchemaGeneration/SchemaBuilder.swift @@ -130,18 +130,20 @@ public final class SchemaBuilder { } assertionFailure("Type frozen, but no entity found?") } + allFrozen = false if let newEntity = processModel(modelType) { entities.append(newEntity) } } - if allFrozen { return } // all have been processed already // TBD: The following does too much work, we might only need the // most of those on the "new models" // This recurses into `process`, if necessary. discoverTargetTypes(in: entities, allEntities: &entities) + + if allFrozen { return } // Collect destination entity names in relships based on the modelType! fillDestinationEntityNamesInRelationships(entities) @@ -168,7 +170,16 @@ public final class SchemaBuilder { continue } // This returns nil if the model is already processed. - guard let newEntity = processModel(targetType) else { continue } + guard let newEntity = processModel(targetType) else { + guard let existingEntity = lookupEntity(targetType) else { + assertionFailure("Type marked as processed, but no entity?") + continue + } + if !allEntities.contains(where: { $0 === existingEntity }) { + allEntities.append(existingEntity) + } + continue + } allEntities.append(newEntity) newEntities.append(newEntity) diff --git a/Tests/ManagedModelTests/SchemaGenerationTests.swift b/Tests/ManagedModelTests/SchemaGenerationTests.swift index bdcb725..4e51543 100644 --- a/Tests/ManagedModelTests/SchemaGenerationTests.swift +++ b/Tests/ManagedModelTests/SchemaGenerationTests.swift @@ -193,4 +193,33 @@ final class SchemaGenerationTests: XCTestCase { XCTAssertNotNil(mom.entitiesByName["Person"]) XCTAssertNotNil(mom.entitiesByName["Address"]) } + + func testDuplicateGeneration() throws { + let cache = SchemaBuilder() + + try autoreleasepool { + let entities = cache.lookupAllEntities(for: [ + Fixtures.PersonAddressSchema.Person.self + ]) + XCTAssertEqual(entities.count, 2) + + let address = try XCTUnwrap( + entities.first(where: { $0.name == "Address" }) + ) + XCTAssertEqual(address.attributes.count, 2) + } + + // second run + try autoreleasepool { + let entities = cache.lookupAllEntities(for: [ + Fixtures.PersonAddressSchema.Person.self + ]) + XCTAssertEqual(entities.count, 2) + + let address = try XCTUnwrap( + entities.first(where: { $0.name == "Address" }) + ) + XCTAssertEqual(address.attributes.count, 2) + } + } } From f9eb13485fb0e2da3ac4d8aa9dad507776a7a777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 20:36:31 +0200 Subject: [PATCH 6/9] Deprecate model initializers Too easy to misuse, instead us the `model(for:)` static function, which is a little more secure (but will still fail on different type sets reusing the same types). --- .../NSManagedObjectModel+Data.swift | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift b/Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift index 81e56ff..c8c1863 100644 --- a/Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift +++ b/Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift @@ -11,6 +11,12 @@ public extension NSManagedObjectModel { // - encodingVersion // - version + @available(*, deprecated, renamed: "model(for:)", message: + """ + Entities can only be used in one NSManagedObjectModel, use the `model(for:)` + static function to get access to s ahred, cached model. + """ + ) @inlinable convenience init(_ entities: NSEntityDescription..., version: Schema.Version = Version(1, 0, 0)) @@ -19,6 +25,12 @@ public extension NSManagedObjectModel { self.entities = entities } + @available(*, deprecated, renamed: "model(for:)", message: + """ + Entities can only be used in one NSManagedObjectModel, use the `model(for:)` + static function to get access to s ahred, cached model. + """ + ) convenience init(_ types: [ any PersistentModel.Type ], version: Schema.Version = Version(1, 0, 0)) { @@ -26,6 +38,12 @@ public extension NSManagedObjectModel { self.entities = SchemaBuilder.shared.lookupAllEntities(for: types) } + @available(*, deprecated, renamed: "model(for:)", message: + """ + Entities can only be used in one NSManagedObjectModel, use the `model(for:)` + static function to get access to s ahred, cached model. + """ + ) @inlinable convenience init(versionedSchema: any VersionedSchema.Type) { self.init(versionedSchema.models, @@ -39,8 +57,8 @@ public extension NSManagedObjectModel { private let lock = NSLock() private var map = [ Set : NSManagedObjectModel ]() -extension NSManagedObjectModel { - +public extension NSManagedObjectModel { + /// A cached version of the initializer. static func model(for types: [ any PersistentModel.Type ]) -> NSManagedObjectModel @@ -61,7 +79,8 @@ extension NSManagedObjectModel { let mom : NSManagedObjectModel if let cachedMOM { mom = cachedMOM } else { - mom = NSManagedObjectModel(types) + mom = NSManagedObjectModel() + mom.entities = SchemaBuilder.shared.lookupAllEntities(for: types) map[typeIDs] = mom } lock.unlock() From 78bf67057f9331fb99bf8597dbdd388ce4effb02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 21:36:04 +0200 Subject: [PATCH 7/9] Line up contexts when setting a toOne Might want to have that for toMany as well, not sure whether that gives perf issues. Maybe just check the first. --- .../PersistentModel/PersistentModel+KVC.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift b/Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift index efda9f6..0ae9630 100644 --- a/Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift +++ b/Sources/ManagedModels/PersistentModel/PersistentModel+KVC.swift @@ -120,6 +120,21 @@ public extension PersistentModel { func _setOptionalToOneValue(forKey key: String, to model: T?) where T: PersistentModel { + #if DEBUG + let relship = Self._$entity.relationshipsByName[key]! + assert(!relship.isToMany, "relship: \(relship)") + #endif + if let model { + if model.modelContext != self.modelContext { + if let otherCtx = model.modelContext, self.modelContext == nil { + otherCtx.insert(self) + } + else if let ownCtx = self.modelContext, model.modelContext == nil { + ownCtx.insert(model) + } + } + } + willChangeValue(forKey: key); defer { didChangeValue(forKey: key) } if let model { setPrimitiveValue(model, forKey: key) From cda33e53dc584d0afcef79f0ac9e2335b41c63e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 21:36:58 +0200 Subject: [PATCH 8/9] Add a convenience model(for:) ... that takes a versioned schema. --- .../SchemaCompatibility/NSManagedObjectModel+Data.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift b/Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift index c8c1863..c395e67 100644 --- a/Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift +++ b/Sources/ManagedModels/SchemaCompatibility/NSManagedObjectModel+Data.swift @@ -58,6 +58,12 @@ private let lock = NSLock() private var map = [ Set : NSManagedObjectModel ]() public extension NSManagedObjectModel { + + static func model(for versionedSchema: VersionedSchema.Type) + -> NSManagedObjectModel + { + model(for: versionedSchema.models) + } /// A cached version of the initializer. static func model(for types: [ any PersistentModel.Type ]) From 921d3e76988dfb94fbdb9c84dffee12375456fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Helge=20He=C3=9F?= Date: Sun, 1 Oct 2023 21:37:35 +0200 Subject: [PATCH 9/9] Fix toOne's w/ explicit Relationships This fixes issue #19. --- .../NSEntityDescription+Generation.swift | 15 ++++++- .../SchemaGenerationTests.swift | 41 +++++++++++++++++++ .../Schemas/PersonAddressOptionalToOne.swift | 28 +++++++++++++ .../Schemas/PersonAddressSchema.swift | 10 ----- .../PersonAddressSchemaNoInverse.swift | 5 --- 5 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 Tests/ManagedModelTests/Schemas/PersonAddressOptionalToOne.swift diff --git a/Sources/ManagedModels/SchemaGeneration/NSEntityDescription+Generation.swift b/Sources/ManagedModels/SchemaGeneration/NSEntityDescription+Generation.swift index a5a64b8..a199f03 100644 --- a/Sources/ManagedModels/SchemaGeneration/NSEntityDescription+Generation.swift +++ b/Sources/ManagedModels/SchemaGeneration/NSEntityDescription+Generation.swift @@ -130,7 +130,8 @@ extension NSEntityDescription { case .toMany(collectionType: _, modelType: _): let relationship = CoreData.NSRelationshipDescription() - relationship.valueType = valueType + relationship.valueType = valueType + relationship.isOptional = valueType is any AnyOptional.Type fixup(relationship, targetType: valueType, isToOne: false, meta: propMeta) assert(relationship.maxCount != 1) @@ -174,15 +175,25 @@ extension NSEntityDescription { // TBD: Rather throw? if relationship.name.isEmpty { relationship.name = meta.name } - if !isToOne { + if isToOne { + relationship.maxCount = 1 // toOne marker! + } + else { // Note: In SwiftData arrays are not ordered. relationship.isOrdered = targetType is NSOrderedSet.Type + assert(relationship.maxCount != 1, "toMany w/ maxCount 1?") } if relationship.keypath == nil { relationship.keypath = meta.keypath } if relationship.valueType == Any.self { relationship.valueType = targetType } + if relationship.valueType != Any.self { + relationship.isOptional = relationship.valueType is any AnyOptional.Type + if !isToOne { + relationship.isOrdered = relationship.valueType is NSOrderedSet.Type + } + } } private func fixupOrderedSet(_ relationship: NSRelationshipDescription, diff --git a/Tests/ManagedModelTests/SchemaGenerationTests.swift b/Tests/ManagedModelTests/SchemaGenerationTests.swift index 4e51543..05e1532 100644 --- a/Tests/ManagedModelTests/SchemaGenerationTests.swift +++ b/Tests/ManagedModelTests/SchemaGenerationTests.swift @@ -98,6 +98,7 @@ final class SchemaGenerationTests: XCTestCase { XCTAssertFalse (lastname.isRelationship) XCTAssertTrue (toAddresses.isRelationship) XCTAssertFalse (toAddresses.isToOneRelationship) + XCTAssertTrue (toAddresses.isToMany) XCTAssertEqual (toAddresses.destination, "Address") XCTAssertNotNil(toAddresses.destinationEntity) @@ -106,6 +107,7 @@ final class SchemaGenerationTests: XCTestCase { let toPerson = try XCTUnwrap(address.relationshipsByName["person"]) XCTAssertTrue (toPerson.isRelationship) XCTAssertTrue (toPerson.isToOneRelationship) + XCTAssertFalse(toPerson.isToMany) XCTAssertEqual(toPerson.destination, "Person") XCTAssertTrue(toAddresses.destinationEntity === address) @@ -222,4 +224,43 @@ final class SchemaGenerationTests: XCTestCase { XCTAssertEqual(address.attributes.count, 2) } } + + func testOptionalBackRef() throws { + let cache = SchemaBuilder() + let schema = NSManagedObjectModel( + versionedSchema: Fixtures.PersonAddressOptionalToOneSchema.self, + schemaCache: cache + ) + + XCTAssertEqual(schema.entities.count, 2) + XCTAssertEqual(schema.entitiesByName.count, 2) + + let person = try XCTUnwrap(schema.entitiesByName["Person"]) + let address = try XCTUnwrap(schema.entitiesByName["Address"]) + + XCTAssertTrue(person.attributes.isEmpty) + XCTAssertEqual(person.relationships.count, 1) + let toAddresses = try XCTUnwrap(person.relationshipsByName["addresses"]) + XCTAssertTrue (toAddresses.isRelationship) + XCTAssertFalse (toAddresses.isToOneRelationship) + XCTAssertFalse (toAddresses.isOptional) + XCTAssertEqual (toAddresses.destination, "Address") + XCTAssertNotNil(toAddresses.destinationEntity) + + XCTAssertTrue(address.attributes.isEmpty) + XCTAssertEqual(address.relationships.count, 1) + let toPerson = try XCTUnwrap(address.relationshipsByName["person"]) + XCTAssertTrue (toPerson.isRelationship) + XCTAssertTrue (toPerson.isOptional) + XCTAssertTrue (toPerson.isToOneRelationship) + XCTAssertEqual(toPerson.destination, "Person") + + XCTAssertTrue(toAddresses.destinationEntity === address) + XCTAssertTrue(toPerson .destinationEntity === person) + + XCTAssertEqual(toPerson .inverseName, "addresses") + XCTAssertEqual(toAddresses.inverseName, "person") + XCTAssertTrue (toAddresses.keypath == toPerson.inverseKeyPath) + XCTAssertTrue (toAddresses.inverseKeyPath == toPerson.keypath) + } } diff --git a/Tests/ManagedModelTests/Schemas/PersonAddressOptionalToOne.swift b/Tests/ManagedModelTests/Schemas/PersonAddressOptionalToOne.swift new file mode 100644 index 0000000..b9372b9 --- /dev/null +++ b/Tests/ManagedModelTests/Schemas/PersonAddressOptionalToOne.swift @@ -0,0 +1,28 @@ +// +// Created by Helge Heß. +// Copyright © 2023 ZeeZide GmbH. +// + +import ManagedModels + +extension Fixtures { + + enum PersonAddressOptionalToOneSchema: VersionedSchema { + static var models : [ any PersistentModel.Type ] = [ + Person.self, + Address.self + ] + + public static let versionIdentifier = Schema.Version(0, 1, 0) + + + @Model class Person: NSManagedObject { + var addresses : Set
// [ Address ] + } + + @Model class Address: NSManagedObject { + @Relationship(deleteRule: .nullify, originalName: "PERSON") + var person : Person? + } + } +} diff --git a/Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift b/Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift index 62bd9f7..9fc368f 100644 --- a/Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift +++ b/Tests/ManagedModelTests/Schemas/PersonAddressSchema.swift @@ -21,19 +21,9 @@ extension Fixtures { @Model final class Person: NSManagedObject { - // TBD: Why are the inits required? *** NEED TO FIGURE THIS OUT var firstname : String var lastname : String var addresses : Set
// [ Address ] - - #if false - init(firstname: String, lastname: String, addresses: [ Address ]) { - self.init() // this does not work - self.firstname = firstname - self.lastname = lastname - self.addresses = addresses - } - #endif } @Model diff --git a/Tests/ManagedModelTests/Schemas/PersonAddressSchemaNoInverse.swift b/Tests/ManagedModelTests/Schemas/PersonAddressSchemaNoInverse.swift index 3784d45..efff698 100644 --- a/Tests/ManagedModelTests/Schemas/PersonAddressSchemaNoInverse.swift +++ b/Tests/ManagedModelTests/Schemas/PersonAddressSchemaNoInverse.swift @@ -19,15 +19,10 @@ extension Fixtures { @Model final class Person: NSManagedObject, PersistentModel { - // TBD: Why are the inits required? *** NEED TO FIGURE THIS OUT var firstname : String var lastname : String var addresses : [ Address ] - // init() is a convenience initializer, it looks up the the entity for the - // object? - // Can we generate inits? - init(firstname: String, lastname: String, addresses: [ Address ]) { super.init(entity: Self._$entity, insertInto: nil) self.firstname = firstname