diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3e24940a..55bb94acb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ x.y.z Release notes (yyyy-MM-dd) ============================================================= ### Enhancements -* None. +* Lifted a limitation that would prevent declaring a model with only computed properties. ([#8414](https://github.com/realm/realm-swift/issues/8414)) ### Fixed * ([#????](https://github.com/realm/realm-swift/issues/????), since v?.?.?) diff --git a/Realm/RLMObjectSchema.mm b/Realm/RLMObjectSchema.mm index 774d9c2b33..c935fa7b06 100644 --- a/Realm/RLMObjectSchema.mm +++ b/Realm/RLMObjectSchema.mm @@ -211,8 +211,9 @@ + (instancetype)schemaForObjectClass:(Class)objectClass { if ([objectClass shouldIncludeInDefaultSchema] && schema.isSwiftClass - && schema.properties.count == 0) { - @throw RLMException(@"No properties are defined for '%@'. Did you remember to mark them with '@objc' in your model?", schema.className); + && schema.properties.count == 0 + && schema.computedProperties.count == 0) { + @throw RLMException(@"No properties are defined for '%@'. Did you remember to mark them with '@objc' or '@Persisted' in your model?", schema.className); } return schema; } diff --git a/Realm/Tests/Swift/SwiftSchemaTests.swift b/Realm/Tests/Swift/SwiftSchemaTests.swift index 2099f0d38b..9836dcec0c 100644 --- a/Realm/Tests/Swift/SwiftSchemaTests.swift +++ b/Realm/Tests/Swift/SwiftSchemaTests.swift @@ -132,6 +132,24 @@ class NoProps: FakeObject { // no @objc properties } +class OnlyComputedSource: RLMObject { + @objc dynamic var link: OnlyComputedTarget? +} + +class OnlyComputedTarget: RLMObject { + @objc dynamic var backlinks: RLMLinkingObjects? + + override class func linkingObjectsProperties() -> [String : RLMPropertyDescriptor] { + return ["backlinks": RLMPropertyDescriptor(with: OnlyComputedSource.self, propertyName: "link")] + } +} + +class OnlyComputedNoBacklinksProps: FakeObject { + var computedProperty: String { + return "Test_String" + } +} + @MainActor class RequiresObjcName: RLMObject { static var enable = false @@ -170,7 +188,26 @@ class SwiftRLMSchemaTests: RLMMultiProcessTestCase { func testShouldRaiseObjectWithoutProperties() { assertThrowsWithReasonMatching(RLMObjectSchema(forObjectClass: NoProps.self), - "No properties are defined for 'NoProps'. Did you remember to mark them with '@objc' in your model?") + "No properties are defined for 'NoProps'. Did you remember to mark them with '@objc' or '@Persisted' in your model?") + } + + func testShouldNotThrowForObjectWithOnlyBacklinksProps() { + let config = RLMRealmConfiguration.default() + config.objectClasses = [OnlyComputedTarget.self, OnlyComputedSource.self] + config.inMemoryIdentifier = #function + let r = try! RLMRealm(configuration: config) + try! r.transaction { + _ = OnlyComputedTarget.create(in: r, withValue: []) + } + + let schema = OnlyComputedTarget().objectSchema + XCTAssertEqual(schema.computedProperties.count, 1) + XCTAssertEqual(schema.properties.count, 0) + } + + func testShouldThrowForObjectWithOnlyComputedNoBacklinksProps() { + assertThrowsWithReasonMatching(RLMObjectSchema(forObjectClass: OnlyComputedNoBacklinksProps.self), + "No properties are defined for 'OnlyComputedNoBacklinksProps'. Did you remember to mark them with '@objc' or '@Persisted' in your model?") } func testSchemaInitWithLinkedToObjectUsingInitWithValue() { diff --git a/RealmSwift/Tests/SwiftLinkTests.swift b/RealmSwift/Tests/SwiftLinkTests.swift index 25366c44e5..11322e2d73 100644 --- a/RealmSwift/Tests/SwiftLinkTests.swift +++ b/RealmSwift/Tests/SwiftLinkTests.swift @@ -111,4 +111,44 @@ class SwiftLinkTests: TestCase { XCTAssertEqual(0, owners.count) } + + func testLinkingObjectsWithNoPersistedProps() { + let realm = realmWithTestPath() + + let target = OnlyComputedProps() + + let source1 = LinkToOnlyComputed() + source1.value = 1 + source1.link = target + + XCTAssertEqual(target.backlinks.count, 0, "Linking objects are not available until the object is persisted") + + try! realm.write { + realm.add(source1) + } + + XCTAssertEqual(target.backlinks.count, 1) + XCTAssertEqual(target.backlinks.first!.value, source1.value) + + let source2 = LinkToOnlyComputed() + source2.value = 2 + source2.link = target + + XCTAssertEqual(target.backlinks.count, 1, "Linking objects to an unpersisted object are not available") + try! realm.write { + realm.add(source2) + } + + XCTAssertEqual(target.backlinks.count, 2) + XCTAssertTrue(target.backlinks.contains(where: { $0.value == 2 })) + + let targetWithNoLinks = OnlyComputedProps() + try! realm.write { + // Implicitly verify we can persist a RealmObject with no persisted properties and + // no objects linking to it + realm.add(targetWithNoLinks) + } + + XCTAssertEqual(targetWithNoLinks.backlinks.count, 0, "No object is linking to targetWithNoLinks") + } } diff --git a/RealmSwift/Tests/SwiftTestObjects.swift b/RealmSwift/Tests/SwiftTestObjects.swift index 27cad91675..2613b9c01c 100644 --- a/RealmSwift/Tests/SwiftTestObjects.swift +++ b/RealmSwift/Tests/SwiftTestObjects.swift @@ -868,3 +868,12 @@ class ObjectWithNestedEmbeddedObject: Object { private class PrivateObjectSubclass: Object { @objc dynamic var value = 0 } + +class LinkToOnlyComputed: Object { + @Persisted var value: Int = 0 + @Persisted var link: OnlyComputedProps? +} + +class OnlyComputedProps: Object { + @Persisted(originProperty: "link") var backlinks: LinkingObjects +}