diff --git a/CHANGELOG.md b/CHANGELOG.md index b9042dc45c..39df230dbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,22 @@ x.y.z Release notes (yyyy-MM-dd) ============================================================= ### Enhancements -* None. - -### Fixed -* Removed warnings for deprecated APIs internal use. - ([#8251](https://github.com/realm/realm-swift/issues/8251), since v10.39.0) +* Filesystem errors now include more information in the error message. +* Sync connection and session reconnect timing/backoff logic has been reworked + and unified into a single implementation. Previously some categories of errors + would cause an hour-long wait before attempting to reconnect, while others + would use an exponential backoff strategy. All errors now result in the sync + client waiting for 1 second before retrying, doubling the wait after each + subsequent failure up to a maximum of five minutes. If the cause of the error + changes, the backoff will be reset. If the sync client voluntarily disconnects, + no backoff will be used. ([Core #6526]((https://github.com/realm/realm-core/pull/6526))) + +### Fixed +* Removed warnings for deprecated APIs internal use. + ([#8251](https://github.com/realm/realm-swift/issues/8251), since v10.39.0) +* Fix an error during async open and client reset if properties have been added + to the schema. This fix also applies to partition-based to flexible sync + migration if async open is used. ([Core #6707](https://github.com/realm/realm-core/issues/6707), since v10.28.2) @@ -14,10 +25,16 @@ x.y.z Release notes (yyyy-MM-dd) * APIs are backwards compatible with all previous releases in the 10.x.y series. * Carthage release for Swift is built with Xcode 14.3.1. * CocoaPods: 1.10 or later. -* Xcode: 14.1-15 beta 1. +* Xcode: 14.1-15 beta 4. ### Internal -* Upgraded realm-core from ? to ? +* Upgraded realm-core from 13.15.1 to 13.17.0 +* The location where prebuilt core binaries are published has changed slightly. + If you are using `REALM_BASE_URL` to mirror the binaries, you may need to + adjust your mirroring logic. +* Release packages were being uploaded to several static.realm.io URLs which + are no longer linked to anywhere. These are no longer being updated, and + release packages are now only being uploaded to Github. 10.41.0 Release notes (2023-06-26) ============================================================= @@ -3928,10 +3945,6 @@ This release also contains all changes from 5.3.2. * Realm Studio: 10.0.0 or later. * Carthage release for Swift is built with Xcode 11.5. -### Internal -* Upgraded realm-core from ? to ? -* Upgraded realm-sync from ? to ? - 10.0.0-beta.2 Release notes (2020-06-09) ============================================================= Xcode 11.3 and iOS 9 are now the minimum supported versions. diff --git a/Package.swift b/Package.swift index 6b88afdaae..75efaf043c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,20 +1,18 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.7 import PackageDescription import Foundation -let coreVersionStr = "13.15.1" -let cocoaVersionStr = "10.41.0" +let coreVersion = Version("13.17.0") +let cocoaVersion = Version("10.41.0") -let coreVersionPieces = coreVersionStr.split(separator: ".") -let coreVersionExtra = coreVersionPieces[2].split(separator: "-") let cxxSettings: [CXXSetting] = [ .headerSearchPath("."), .headerSearchPath("include"), .define("REALM_SPM", to: "1"), .define("REALM_ENABLE_SYNC", to: "1"), - .define("REALM_COCOA_VERSION", to: "@\"\(cocoaVersionStr)\""), - .define("REALM_VERSION", to: "\"\(coreVersionStr)\""), + .define("REALM_COCOA_VERSION", to: "@\"\(cocoaVersion)\""), + .define("REALM_VERSION", to: "\"\(coreVersion)\""), .define("REALM_IOPLATFORMUUID", to: "@\"\(runCommand())\""), .define("REALM_DEBUG", .when(configuration: .debug)), @@ -23,11 +21,11 @@ let cxxSettings: [CXXSetting] = [ .define("REALM_ENABLE_ASSERTIONS", to: "1"), .define("REALM_ENABLE_ENCRYPTION", to: "1"), - .define("REALM_VERSION_MAJOR", to: String(coreVersionPieces[0])), - .define("REALM_VERSION_MINOR", to: String(coreVersionPieces[1])), - .define("REALM_VERSION_PATCH", to: String(coreVersionExtra[0])), - .define("REALM_VERSION_EXTRA", to: "\"\(coreVersionExtra.count > 1 ? String(coreVersionExtra[1]) : "")\""), - .define("REALM_VERSION_STRING", to: "\"\(coreVersionStr)\""), + .define("REALM_VERSION_MAJOR", to: String(coreVersion.major)), + .define("REALM_VERSION_MINOR", to: String(coreVersion.minor)), + .define("REALM_VERSION_PATCH", to: String(coreVersion.patch)), + .define("REALM_VERSION_EXTRA", to: "\"\(coreVersion.prereleaseIdentifiers.first ?? "")\""), + .define("REALM_VERSION_STRING", to: "\"\(coreVersion)\""), ] let testCxxSettings: [CXXSetting] = cxxSettings + [ // Command-line `swift build` resolves header search paths @@ -144,12 +142,12 @@ let package = Package( targets: ["Realm", "RealmSwift"]), ], dependencies: [ - .package(name: "RealmDatabase", url: "https://github.com/realm/realm-core.git", .exact(Version(coreVersionStr)!)) + .package(url: "https://github.com/realm/realm-core.git", exact: coreVersion) ], targets: [ .target( name: "Realm", - dependencies: [.product(name: "RealmCore", package: "RealmDatabase")], + dependencies: [.product(name: "RealmCore", package: "realm-core")], path: ".", exclude: [ "CHANGELOG.md", diff --git a/Realm/ObjectServerTests/SwiftObjectServerTests.swift b/Realm/ObjectServerTests/SwiftObjectServerTests.swift index 003a9f1bf2..4728fcc40e 100644 --- a/Realm/ObjectServerTests/SwiftObjectServerTests.swift +++ b/Realm/ObjectServerTests/SwiftObjectServerTests.swift @@ -54,14 +54,6 @@ func assertSyncError(_ error: Error, _ code: SyncError.Code, _ message: String, @available(OSX 10.14, *) @objc(SwiftObjectServerTests) class SwiftObjectServerTests: SwiftSyncTestCase { - func setupMongoCollection(user: User, collectionName: String) -> MongoCollection { - let mongoClient = user.mongoClient("mongodb1") - let database = mongoClient.database(named: "test_data") - let collection = database.collection(withName: collectionName) - removeAllFromCollection(collection) - return collection - } - /// It should be possible to successfully open a Realm configured for sync. func testBasicSwiftSync() throws { let user = try logInUser(for: basicCredentials()) @@ -423,10 +415,27 @@ class SwiftObjectServerTests: SwiftSyncTestCase { } // After restarting sync, the sync history translator service needs time - // to resynthesize the new history from existing objects on the server - // This method waits for the realm to receive "Paul" from the server - // as confirmation. - func waitForServerHistoryAfterRestart(realm: Realm) { + // to resynthesize the new history from existing objects on the server. + // This method creates a new document on the server and then waits for it to + // be synchronized to a newly created Realm to confirm everything is up-to-date. + func waitForServerHistoryAfterRestart(config: Realm.Configuration, collection: MongoCollection) { + XCTAssertFalse(FileManager.default.fileExists(atPath: config.fileURL!.path)) + let realm = Realm.asyncOpen(configuration: config).await(self) + XCTAssertTrue(realm.isEmpty) + + // Create an object on the server which should be present after client reset + removeAllFromCollection(collection) + let serverObject: Document = [ + "_id": .objectId(ObjectId.generate()), + "firstName": .string("Paul"), + "lastName": .string("M"), + "age": .int32(30), + "realm_id": config.syncConfiguration?.partitionValue + ] + collection.insertOne(serverObject).await(self, timeout: 30.0) + XCTAssertEqual(collection.count(filter: [:]).await(self), 1) + + // Wait for the document to be processed by the translator let start = Date() while realm.isEmpty && start.timeIntervalSinceNow > -60.0 { self.waitForDownloads(for: realm) @@ -441,7 +450,7 @@ class SwiftObjectServerTests: SwiftSyncTestCase { } } - func prepareClientReset(_ partition: String, _ user: User) throws { + func prepareClientReset(_ partition: String, _ user: User, appId: String? = nil) throws { try autoreleasepool { // Initialize the local file so that we have conflicting history var configuration = user.configuration(partitionValue: partition) @@ -449,7 +458,7 @@ class SwiftObjectServerTests: SwiftSyncTestCase { let realm = try Realm(configuration: configuration) waitForUploads(for: realm) realm.syncSession!.suspend() - try RealmServer.shared.triggerClientReset(appId, realm) + try RealmServer.shared.triggerClientReset(appId ?? self.appId, realm) // Add an object to the local realm that won't be synced due to the suspend try realm.write { @@ -458,17 +467,11 @@ class SwiftObjectServerTests: SwiftSyncTestCase { XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) } - // Write a different object in a different Realm which should appear in - // the first one after a client reset - try autoreleasepool { - var config = user.configuration(partitionValue: partition) - config.fileURL = RLMTestRealmURL() - config.objectTypes = [SwiftPerson.self] - let realm = try Realm(configuration: config) - try realm.write { - realm.add(SwiftPerson(firstName: "Paul", lastName: "M")) - } - waitForUploads(for: realm) + var config = user.configuration(partitionValue: partition) + config.fileURL = RLMTestRealmURL() + config.objectTypes = [SwiftPerson.self] + autoreleasepool { + waitForServerHistoryAfterRestart(config: config, collection: user.collection(for: SwiftPerson.self)) } } @@ -476,7 +479,7 @@ class SwiftObjectServerTests: SwiftSyncTestCase { let appId = try RealmServer.shared.createAppWithQueryableFields(["age"]) let app = app(withId: appId) let user = try logInUser(for: basicCredentials(app: app), app: app) - let collection = setupMongoCollection(user: user, collectionName: "SwiftPerson") + let collection = try setupMongoCollection(user: user, for: SwiftPerson.self) if disableRecoveryMode { // Disable recovery mode on the server. @@ -494,50 +497,31 @@ class SwiftObjectServerTests: SwiftSyncTestCase { updateAllPeopleSubscription(subscriptions) } - // Create an object on the server which should be present after client reset - let serverObject: Document = [ - "_id": .objectId(ObjectId.generate()), - "firstName": .string("Paul"), - "lastName": .string("M"), - "age": .int32(30) - ] - collection.insertOne(serverObject).await(self, timeout: 30.0) - // Sync is disabled, block executed, sync re-enabled try executeBlockOffline(flexibleSync: true, appId: appId) { var configuration = user.flexibleSyncConfiguration() configuration.objectTypes = [SwiftPerson.self] let realm = try Realm(configuration: configuration) - realm.syncSession!.suspend() - - // There is enough time between the collection insert and the server - // being turned off for the subscription sync to sync "Paul M". - if realm.objects(SwiftPerson.self).count > 0 { - try realm.write { - realm.deleteAll() - } - } + let syncSession = realm.syncSession! + syncSession.suspend() - // Add an object to the local realm that will not be in the server realm (because sync is disabled). try realm.write { + // Add an object to the local realm that will not be in the server realm (because sync is disabled). realm.add(SwiftPerson(firstName: "John", lastName: "L")) } XCTAssertEqual(realm.objects(SwiftPerson.self).count, 1) } - // After restarting sync, the sync history translator service needs time - // to resynthesize the new history from existing objects on the server - // The following creates a new realm with the same partition and wait for - // downloads to ensure the the new history has been created. - try autoreleasepool { - var newConfig = user.flexibleSyncConfiguration() - newConfig.fileURL = RLMTestRealmURL() - newConfig.objectTypes = [SwiftPerson.self] - let newRealm = try Realm(configuration: newConfig) + // Object created above should not have been synced + XCTAssertEqual(collection.count(filter: [:]).await(self), 0) - let subscriptions = newRealm.subscriptions - updateAllPeopleSubscription(subscriptions) - waitForServerHistoryAfterRestart(realm: newRealm) + autoreleasepool { + var config = user.flexibleSyncConfiguration { subscriptions in + subscriptions.append(QuerySubscription(name: "all_people")) + } + config.fileURL = RLMTestRealmURL() + config.objectTypes = [SwiftPerson.self] + waitForServerHistoryAfterRestart(config: config, collection: collection) } return (user, appId) @@ -918,16 +902,98 @@ class SwiftObjectServerTests: SwiftSyncTestCase { XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") XCTAssertEqual(realm.objects(SwiftPerson.self)[1].firstName, "Paul") } + + // Wait for the recovered object to be processed by the translator as + // otherwise it'll show up in the middle of later tests + waitForCollectionCount(user.collection(for: SwiftPerson.self), 2) + } + + func testClientResetRecoverAsyncOpen() throws { + let user = try logInUser(for: basicCredentials()) + try prepareClientReset(#function, user) + + let (assertBeforeBlock, assertAfterBlock) = assertRecover() + var configuration = user.configuration(partitionValue: #function, clientResetMode: .recoverUnsyncedChanges(beforeReset: assertBeforeBlock, afterReset: assertAfterBlock)) + configuration.objectTypes = [SwiftPerson.self] + + let syncConfig = try XCTUnwrap(configuration.syncConfiguration) + switch syncConfig.clientResetMode { + case .recoverUnsyncedChanges(let before, let after): + XCTAssertNotNil(before) + XCTAssertNotNil(after) + default: + XCTFail("Should be set to recover") + } + autoreleasepool { + let realm = Realm.asyncOpen(configuration: configuration).await(self) + XCTAssertEqual(realm.objects(SwiftPerson.self).count, 2) + // The object created locally (John) and the object created on the server (Paul) + // should both be integrated into the new realm file. + XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "John") + XCTAssertEqual(realm.objects(SwiftPerson.self)[1].firstName, "Paul") + waitForExpectations(timeout: 15.0) + } + + // Wait for the recovered object to be processed by the translator as + // otherwise it'll show up in the middle of later tests + waitForCollectionCount(user.collection(for: SwiftPerson.self), 2) + } + + func testClientResetRecoverWithSchemaChanges() throws { + let user = try logInUser(for: basicCredentials()) + try prepareClientReset(#function, user) + + let beforeCallbackEx = expectation(description: "before reset callback") + @Sendable func beforeClientReset(_ before: Realm) { + let person = before.objects(SwiftPersonWithAdditionalProperty.self).first! + XCTAssertEqual(person.objectSchema.properties.map(\.name), + ["_id", "firstName", "lastName", "age", "newProperty"]) + XCTAssertEqual(person.newProperty, 0) + beforeCallbackEx.fulfill() + } + let afterCallbackEx = expectation(description: "after reset callback") + @Sendable func afterClientReset(_ before: Realm, _ after: Realm) { + let beforePerson = before.objects(SwiftPersonWithAdditionalProperty.self).first! + XCTAssertEqual(beforePerson.objectSchema.properties.map(\.name), + ["_id", "firstName", "lastName", "age", "newProperty"]) + XCTAssertEqual(beforePerson.newProperty, 0) + let afterPerson = after.objects(SwiftPersonWithAdditionalProperty.self).first! + XCTAssertEqual(afterPerson.objectSchema.properties.map(\.name), + ["_id", "firstName", "lastName", "age", "newProperty"]) + XCTAssertEqual(afterPerson.newProperty, 0) + + // Fulfill on the main thread to make it harder to hit a race + // condition where the test completes before the client reset finishes + // unwinding. This does not fully fix the problem. + DispatchQueue.main.async { + afterCallbackEx.fulfill() + } + } + + var configuration = user.configuration(partitionValue: #function, clientResetMode: .recoverUnsyncedChanges(beforeReset: beforeClientReset, afterReset: afterClientReset)) + configuration.objectTypes = [SwiftPersonWithAdditionalProperty.self] + + autoreleasepool { + _ = Realm.asyncOpen(configuration: configuration).await(self) + waitForExpectations(timeout: 15.0) + } + + // Wait for the recovered object to be processed by the translator as + // otherwise it'll show up in the middle of later tests + waitForCollectionCount(user.collection(for: SwiftPerson.self), 2) + } func testClientResetRecoverOrDiscardLocalFailedRecovery() throws { + let appId = try RealmServer.shared.createApp() // Disable recovery mode on the server. // This attempts to simulate a case where recovery mode fails when // using RecoverOrDiscardLocal try waitForEditRecoveryMode(appId: appId, disable: true) - let user = try logInUser(for: basicCredentials()) - try prepareClientReset(#function, user) + let app = app(withId: appId) + let user = try logInUser(for: basicCredentials(app: app), app: app) + try prepareClientReset(#function, user, appId: appId) // Expect the recovery to fail back to discardLocal logic let (assertBeforeBlock, assertAfterBlock) = assertDiscardLocal() @@ -953,7 +1019,8 @@ class SwiftObjectServerTests: SwiftSyncTestCase { // while the one from the server ("Paul") should be present. XCTAssertEqual(realm.objects(SwiftPerson.self)[0].firstName, "Paul") } - try waitForEditRecoveryMode(appId: appId, disable: false) + + try RealmServer.shared.deleteApp(appId) } @available(*, deprecated) // .discardLocal @@ -1355,13 +1422,20 @@ class SwiftObjectServerTests: SwiftSyncTestCase { } waitForUploads(for: realm) - XCTAssertGreaterThan(downloadCount.wrappedValue, 1) - XCTAssertGreaterThan(uploadCount.wrappedValue, 1) - tokenDownload!.invalidate() tokenUpload!.invalidate() RLMSyncSession.notificationsQueue().sync { } + XCTAssertGreaterThan(downloadCount.wrappedValue, 1) + XCTAssertGreaterThan(uploadCount.wrappedValue, 1) + + // There's inherently a race condition here: notification callbacks can + // be called up to one more time after they're invalidated if the sync + // worker thread is in the middle of processing a change at the time + // that the invalidation is requested, and there's no way to wait for that. + // This whole test takes 250ms, so we don't need a very long sleep. + Thread.sleep(forTimeInterval: 0.2) + downloadCount.wrappedValue = 0 uploadCount.wrappedValue = 0 @@ -1372,7 +1446,8 @@ class SwiftObjectServerTests: SwiftSyncTestCase { } waitForUploads(for: realm) - // We check that the notification block is not called after we reset the counters on the notifiers and call invalidated(). + // We check that the notification block is not called after we reset the + // counters on the notifiers and call invalidated(). XCTAssertEqual(downloadCount.wrappedValue, 0) XCTAssertEqual(uploadCount.wrappedValue, 0) } @@ -2223,7 +2298,7 @@ class SwiftObjectServerTests: SwiftSyncTestCase { } func testVerifyDocumentsWithCustomColumnNames() throws { - let collection = try setupMongoCollection(for: "SwiftCustomColumnObject") + let collection = try setupMongoCollection(for: SwiftCustomColumnObject.self) let objectId = ObjectId.generate() let linkedObjectId = ObjectId.generate() @@ -2240,12 +2315,7 @@ class SwiftObjectServerTests: SwiftSyncTestCase { realm.add(object) } waitForUploads(for: realm) - - let waitStart = Date() - while collection.count(filter: [:]).await(self) != 2 && waitStart.timeIntervalSinceNow > -600.0 { - sleep(5) - } - XCTAssertEqual(collection.count(filter: [:]).await(self), 2) + waitForCollectionCount(collection, 2) let filter: Document = ["_id": .objectId(objectId)] collection.findOneDocument(filter: filter) @@ -2354,7 +2424,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { // swiftlint:disable multiple_closures_with_trailing_closure func testWatchCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let watchEx1 = Locked(expectation(description: "Main thread watch")) @@ -2396,7 +2466,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testWatchCombineWithFilterIds() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "cane corso"] let document3: Document = ["name": "john", "breed": "cane corso"] @@ -2466,7 +2536,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testWatchCombineWithMatchFilter() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "cane corso"] let document3: Document = ["name": "john", "breed": "cane corso"] @@ -2648,7 +2718,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionInsertCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "tibetan mastiff"] @@ -2664,7 +2734,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionFindCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "tibetan mastiff"] let document3: Document = ["name": "rex", "breed": "tibetan mastiff", "coat": ["fawn", "brown", "white"]] @@ -2694,7 +2764,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionCountAndAggregateCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] collection.insertMany([document]).await(self) @@ -2709,7 +2779,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionDeleteOneCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "cane corso"] @@ -2723,7 +2793,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionDeleteManyCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "cane corso"] @@ -2737,7 +2807,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionUpdateOneCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "cane corso"] let document3: Document = ["name": "john", "breed": "cane corso"] @@ -2759,7 +2829,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionUpdateManyCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "cane corso"] let document3: Document = ["name": "john", "breed": "cane corso"] @@ -2780,7 +2850,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionFindAndUpdateCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "cane corso"] let document3: Document = ["name": "john", "breed": "cane corso"] @@ -2807,7 +2877,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionFindAndReplaceCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] let document2: Document = ["name": "rex", "breed": "cane corso"] let document3: Document = ["name": "john", "breed": "cane corso"] @@ -2832,7 +2902,7 @@ class CombineObjectServerTests: SwiftSyncTestCase { } func testMongoCollectionFindAndDeleteCombine() throws { - let collection = try setupMongoCollection(for: "Dog") + let collection = try setupMongoCollection(for: Dog.self) let document: Document = ["name": "fido", "breed": "cane corso"] collection.insertMany([document]).await(self) diff --git a/Realm/ObjectServerTests/SwiftServerObjects.swift b/Realm/ObjectServerTests/SwiftServerObjects.swift index 60dbc4a7e2..b1159f328c 100644 --- a/Realm/ObjectServerTests/SwiftServerObjects.swift +++ b/Realm/ObjectServerTests/SwiftServerObjects.swift @@ -33,6 +33,20 @@ public class SwiftPerson: Object { } } +public class SwiftPersonWithAdditionalProperty: SwiftPerson { + @Persisted public var newProperty: Int + + public override class func _realmIgnoreClass() -> Bool { + true + } + public override class func _realmObjectName() -> String { + "SwiftPerson" + } + public override class func className() -> String { + "SwiftPersonWithAdditionalProperty" + } +} + public class LinkToSwiftPerson: Object { @Persisted(primaryKey: true) public var _id: ObjectId @Persisted public var person: SwiftPerson? diff --git a/Realm/ObjectServerTests/SwiftSyncTestCase.swift b/Realm/ObjectServerTests/SwiftSyncTestCase.swift index db9abbdb04..c9200cfadc 100644 --- a/Realm/ObjectServerTests/SwiftSyncTestCase.swift +++ b/Realm/ObjectServerTests/SwiftSyncTestCase.swift @@ -34,6 +34,10 @@ public extension User { config.objectTypes = [SwiftPerson.self, SwiftHugeSyncObject.self, SwiftTypesSyncObject.self, SwiftCustomColumnObject.self] return config } + + func collection(for object: ObjectBase.Type) -> MongoCollection { + mongoClient("mongodb1").database(named: "test_data").collection(withName: object.className()) + } } public func randomString(_ length: Int) -> String { @@ -253,11 +257,9 @@ open class SwiftSyncTestCase: RLMSyncTestCase { // MARK: - Mongo Client - public func setupMongoCollection(for collection: String) throws -> MongoCollection { - let user = try logInUser(for: basicCredentials()) - let mongoClient = user.mongoClient("mongodb1") - let database = mongoClient.database(named: "test_data") - let collection = database.collection(withName: collection) + public func setupMongoCollection(user: User? = nil, for type: ObjectBase.Type) throws -> MongoCollection { + let u = try user ?? logInUser(for: basicCredentials()) + let collection = u.collection(for: type) removeAllFromCollection(collection) return collection } @@ -272,6 +274,14 @@ open class SwiftSyncTestCase: RLMSyncTestCase { } wait(for: [deleteEx], timeout: 30.0) } + + public func waitForCollectionCount(_ collection: MongoCollection, _ count: Int) { + let waitStart = Date() + while collection.count(filter: [:]).await(self) != count && waitStart.timeIntervalSinceNow > -600.0 { + sleep(1) + } + XCTAssertEqual(collection.count(filter: [:]).await(self), count) + } } @available(macOS 12.0, *) diff --git a/Realm/RLMAccessor.hpp b/Realm/RLMAccessor.hpp index c1d1c20d34..30d42492eb 100644 --- a/Realm/RLMAccessor.hpp +++ b/Realm/RLMAccessor.hpp @@ -113,7 +113,7 @@ class RLMAccessorContext : public RLMStatelessAccessorContext { id box(realm::Mixed); void will_change(realm::Obj const&, realm::Property const&); - void will_change(realm::Object& obj, realm::Property const& prop) { will_change(obj.obj(), prop); } + void will_change(realm::Object& obj, realm::Property const& prop) { will_change(obj.get_obj(), prop); } void did_change(); RLMOptionalId value_for_property(id dict, realm::Property const&, size_t prop_index); diff --git a/Realm/RLMAccessor.mm b/Realm/RLMAccessor.mm index 2b98bbd2df..db7598738e 100644 --- a/Realm/RLMAccessor.mm +++ b/Realm/RLMAccessor.mm @@ -919,7 +919,7 @@ void RLMSetSwiftPropertyAny(__unsafe_unretained RLMObjectBase *const obj, uint16 id RLMAccessorContext::box(realm::Object&& o) { REALM_ASSERT(currentProperty); - return RLMCreateObjectAccessor(_info.linkTargetType(currentProperty.index), o.obj()); + return RLMCreateObjectAccessor(_info.linkTargetType(currentProperty.index), o.get_obj()); } id RLMAccessorContext::box(realm::Obj&& r) { @@ -1100,7 +1100,7 @@ static auto toOptional(__unsafe_unretained id const value) { try { realm::Object::create(*this, _realm->_realm, *_info.objectSchema, - (id)value, policy, existingKey, outObj).obj(); + (id)value, policy, existingKey, outObj); } catch (std::exception const& e) { @throw RLMException(e); diff --git a/Realm/RLMMongoDatabase.h b/Realm/RLMMongoDatabase.h index f700cb87aa..177fbc1b8c 100644 --- a/Realm/RLMMongoDatabase.h +++ b/Realm/RLMMongoDatabase.h @@ -44,6 +44,7 @@ RLM_SWIFT_SENDABLE RLM_FINAL // immutable final class /// @param name The name of the collection to return /// @returns The collection - (RLMMongoCollection *)collectionWithName:(NSString *)name; +// NEXT-MAJOR: NS_SWIFT_NAME(collection(named:)) @end diff --git a/Realm/RLMObjectBase.mm b/Realm/RLMObjectBase.mm index 02b2154c71..410a3177c3 100644 --- a/Realm/RLMObjectBase.mm +++ b/Realm/RLMObjectBase.mm @@ -440,7 +440,7 @@ + (instancetype)objectWithThreadSafeReference:(realm::ThreadSafeReference)refere return nil; } NSString *objectClassName = @(object.get_object_schema().name.c_str()); - return RLMCreateObjectAccessor(realm->_info[objectClassName], object.obj()); + return RLMCreateObjectAccessor(realm->_info[objectClassName], object.get_obj()); } @end diff --git a/Realm/RLMObjectStore.h b/Realm/RLMObjectStore.h index 69443c9659..6045f1e8e2 100644 --- a/Realm/RLMObjectStore.h +++ b/Realm/RLMObjectStore.h @@ -92,7 +92,7 @@ RLMObjectBase *RLMObjectFromObjLink(RLMRealm *realm, // Create accessors RLMObjectBase *RLMCreateObjectAccessor(RLMClassInfo& info, int64_t key) NS_RETURNS_RETAINED; -RLMObjectBase *RLMCreateObjectAccessor(RLMClassInfo& info, realm::Obj&& obj) NS_RETURNS_RETAINED; +RLMObjectBase *RLMCreateObjectAccessor(RLMClassInfo& info, const realm::Obj& obj) NS_RETURNS_RETAINED; #endif RLM_HEADER_AUDIT_END(nullability) diff --git a/Realm/RLMObjectStore.mm b/Realm/RLMObjectStore.mm index ff87f4506e..3332b8c7ad 100644 --- a/Realm/RLMObjectStore.mm +++ b/Realm/RLMObjectStore.mm @@ -227,7 +227,7 @@ id RLMGetObject(RLMRealm *realm, NSString *objectClassName, id key) { key ?: NSNull.null); if (!obj.is_valid()) return nil; - return RLMCreateObjectAccessor(info, obj.obj()); + return RLMCreateObjectAccessor(info, obj.get_obj()); } catch (std::exception const& e) { @throw RLMException(e); @@ -239,9 +239,9 @@ id RLMGetObject(RLMRealm *realm, NSString *objectClassName, id key) { } // Create accessor and register with realm -RLMObjectBase *RLMCreateObjectAccessor(RLMClassInfo& info, realm::Obj&& obj) { +RLMObjectBase *RLMCreateObjectAccessor(RLMClassInfo& info, const realm::Obj& obj) { RLMObjectBase *accessor = RLMCreateManagedAccessor(info.rlmObjectSchema.accessorClass, &info); - accessor->_row = std::move(obj); + accessor->_row = obj; RLMInitializeSwiftAccessor(accessor, false); return accessor; } diff --git a/Realm/RLMRealm.mm b/Realm/RLMRealm.mm index 059fa60bc9..9109d0e0ba 100644 --- a/Realm/RLMRealm.mm +++ b/Realm/RLMRealm.mm @@ -332,6 +332,53 @@ + (instancetype)realmWithSharedRealm:(SharedRealm)sharedRealm return autorelease(realm); } ++ (instancetype)realmWithSharedRealm:(std::shared_ptr)osRealm + schema:(RLMSchema *)schema + dynamic:(bool)dynamic + freeze:(bool)freeze { + RLMRealm *realm = [[RLMRealm alloc] initPrivate]; + realm->_realm = osRealm; + realm->_dynamic = dynamic; + + if (dynamic) { + realm->_schema = schema ?: [RLMSchema dynamicSchemaFromObjectStoreSchema:osRealm->schema()]; + } + else @autoreleasepool { + if (auto cachedRealm = RLMGetAnyCachedRealmForPath(osRealm->config().path)) { + realm->_realm->set_schema_subset(cachedRealm->_realm->schema()); + realm->_schema = cachedRealm.schema; + realm->_info = cachedRealm->_info.clone(cachedRealm->_realm->schema(), realm); + } + else if (osRealm->is_frozen()) { + realm->_schema = schema ?: RLMSchema.sharedSchema; + realm->_realm->set_schema_subset(realm->_schema.objectStoreCopy); + } + else { + realm->_schema = schema ?: RLMSchema.sharedSchema; + try { + // No migration function: currently this is only used as part of + // client resets on sync Realms, so none is needed. If that + // changes, this'll need to as well. + realm->_realm->update_schema(realm->_schema.objectStoreCopy, osRealm->config().schema_version); + } + catch (...) { + RLMRealmTranslateException(nil); + REALM_COMPILER_HINT_UNREACHABLE(); + } + } + } + + if (realm->_info.begin() == realm->_info.end()) { + realm->_info = RLMSchemaInfo(realm); + } + + if (freeze && !realm->_realm->is_frozen()) { + realm->_realm = realm->_realm->freeze(); + } + + return realm; +} + + (instancetype)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error { return autorelease([self realmWithConfiguration:configuration confinedTo:RLMScheduler.currentRunLoop diff --git a/Realm/RLMRealm_Private.hpp b/Realm/RLMRealm_Private.hpp index ca4d6093fd..77208078e6 100644 --- a/Realm/RLMRealm_Private.hpp +++ b/Realm/RLMRealm_Private.hpp @@ -20,34 +20,27 @@ #import "RLMClassInfo.hpp" -#import - #import +RLM_HEADER_AUDIT_BEGIN(nullability, sendability) + namespace realm { class Group; class Realm; } -struct RLMResultsSetInfo { - realm::ObjectSchema osObjectSchema; - RLMObjectSchema *rlmObjectSchema; - RLMClassInfo info; - - RLMResultsSetInfo(__unsafe_unretained RLMRealm *const realm); - static RLMClassInfo& get(__unsafe_unretained RLMRealm *const realm); -}; @interface RLMRealm () { @public std::shared_ptr _realm; RLMSchemaInfo _info; - std::unique_ptr _resultsSetInfo; } + (instancetype)realmWithSharedRealm:(std::shared_ptr)sharedRealm - schema:(RLMSchema *)schema - dynamic:(bool)dynamic; + schema:(nullable RLMSchema *)schema + dynamic:(bool)dynamic + freeze:(bool)freeze NS_RETURNS_RETAINED; -// FIXME - group should not be exposed @property (nonatomic, readonly) realm::Group &group; @end + +RLM_HEADER_AUDIT_END(nullability, sendability) diff --git a/Realm/RLMSyncConfiguration.mm b/Realm/RLMSyncConfiguration.mm index cb63e5f3bf..f4b0f73e0a 100644 --- a/Realm/RLMSyncConfiguration.mm +++ b/Realm/RLMSyncConfiguration.mm @@ -32,6 +32,7 @@ #import "RLMUser_Private.hpp" #import "RLMUtil.hpp" +#import #import #import #import @@ -45,18 +46,7 @@ struct CallbackSchema { bool dynamic; - std::string path; RLMSchema *customSchema; - - RLMSchema *getSchema(Realm& realm) { - if (dynamic) { - return [RLMSchema dynamicSchemaFromObjectStoreSchema:realm.schema()]; - } - if (auto cached = RLMGetAnyCachedRealmForPath(path)) { - return cached.schema; - } - return customSchema ?: RLMSchema.sharedSchema; - } }; struct BeforeClientResetWrapper : CallbackSchema { @@ -64,7 +54,7 @@ void operator()(std::shared_ptr local) { @autoreleasepool { if (local->schema_version() != RLMNotVersioned) { - block([RLMRealm realmWithSharedRealm:local schema:getSchema(*local) dynamic:false]); + block([RLMRealm realmWithSharedRealm:local schema:customSchema dynamic:dynamic freeze:true]); } } } @@ -74,17 +64,19 @@ void operator()(std::shared_ptr local) { RLMClientResetAfterBlock block; void operator()(std::shared_ptr local, ThreadSafeReference remote, bool) { @autoreleasepool { - if (local->schema_version() != RLMNotVersioned) { - RLMSchema *schema = getSchema(*local); - RLMRealm *localRealm = [RLMRealm realmWithSharedRealm:local - schema:schema - dynamic:false]; - - RLMRealm *remoteRealm = [RLMRealm realmWithSharedRealm:Realm::get_shared_realm(std::move(remote)) - schema:schema - dynamic:false]; - block(localRealm, remoteRealm); + if (local->schema_version() == RLMNotVersioned) { + return; } + + RLMRealm *localRealm = [RLMRealm realmWithSharedRealm:local + schema:customSchema + dynamic:dynamic + freeze:true]; + RLMRealm *remoteRealm = [RLMRealm realmWithSharedRealm:Realm::get_shared_realm(std::move(remote)) + schema:customSchema + dynamic:dynamic + freeze:false]; + block(localRealm, remoteRealm); } } }; @@ -159,6 +151,7 @@ - (void)setBeforeClientReset:(RLMClientResetBeforeBlock)beforeClientReset { } else if (self.clientResetMode == RLMClientResetModeManual) { @throw RLMException(@"RLMClientResetBeforeBlock reset notifications are not supported in Manual mode. Use RLMSyncConfiguration.manualClientResetHandler or RLMSyncManager.ErrorHandler"); } else { + _config->freeze_before_reset_realm = false; _config->notify_before_client_reset = BeforeClientResetWrapper{.block = beforeClientReset}; } } @@ -201,13 +194,11 @@ void RLMSetConfigInfoForClientResetCallbacks(realm::SyncConfig& syncConfig, RLMR if (syncConfig.notify_before_client_reset) { auto before = syncConfig.notify_before_client_reset.target(); before->dynamic = config.dynamic; - before->path = config.path; before->customSchema = config.customSchema; } if (syncConfig.notify_after_client_reset) { auto after = syncConfig.notify_after_client_reset.target(); after->dynamic = config.dynamic; - after->path = config.path; after->customSchema = config.customSchema; } } diff --git a/build.sh b/build.sh index 42181361fe..be9c271f32 100755 --- a/build.sh +++ b/build.sh @@ -914,13 +914,23 @@ case "$COMMAND" in PlistBuddy -c "Set :CFBundleShortVersionString $realm_version" "$version_file" done sed -i '' "s/^VERSION=.*/VERSION=$realm_version/" dependencies.list - sed -i '' "s/^let coreVersionStr =.*/let coreVersionStr = \"$REALM_CORE_VERSION\"/" Package.swift - sed -i '' "s/^let cocoaVersionStr =.*/let cocoaVersionStr = \"$realm_version\"/" Package.swift + sed -i '' "s/^let cocoaVersion =.*/let cocoaVersion = Version(\"$realm_version\")/" Package.swift sed -i '' "s/x.y.z Release notes (yyyy-MM-dd)/$realm_version Release notes ($(date '+%Y-%m-%d'))/" CHANGELOG.md exit 0 ;; + "set-core-version") + new_version="$2" + old_version="$(sed -n 's/^REALM_CORE_VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")" + + sed -i '' "s/^REALM_CORE_VERSION=.*/REALM_CORE_VERSION=$new_version/" dependencies.list + sed -i '' "s/^let coreVersion =.*/let coreVersion = Version(\"$new_version\")/" Package.swift + sed -i '' "s/Upgraded realm-core from ? to ?/Upgraded realm-core from $old_version to $new_version/" CHANGELOG.md + + exit 0 + ;; + ###################################### # Continuous Integration ###################################### @@ -1209,7 +1219,7 @@ x.y.z Release notes (yyyy-MM-dd) * APIs are backwards compatible with all previous releases in the 10.x.y series. * Carthage release for Swift is built with Xcode 14.3.1. * CocoaPods: 1.10 or later. -* Xcode: 14.1-15 beta 1. +* Xcode: 14.1-15 beta 4. ### Internal * Upgraded realm-core from ? to ? diff --git a/dependencies.list b/dependencies.list index 68d2f1fb29..ef0b6e1d8b 100755 --- a/dependencies.list +++ b/dependencies.list @@ -1,3 +1,3 @@ VERSION=10.41.0 -REALM_CORE_VERSION=13.15.1 +REALM_CORE_VERSION=13.17.0 STITCH_VERSION=1eb31b87154cf7af6cbe50ab2732e2856ca499c7 diff --git a/scripts/download-core.sh b/scripts/download-core.sh index 8a1b58dd1b..514bc8de86 100644 --- a/scripts/download-core.sh +++ b/scripts/download-core.sh @@ -5,7 +5,7 @@ source_root="$(dirname "$0")/.." readonly source_root # override this env variable if you need to download from a private mirror -: "${REALM_BASE_URL:="https://static.realm.io/downloads"}" +: "${REALM_BASE_URL:="https://static.realm.io/downloads/core"}" # set to "current" to always use the current build : "${REALM_CORE_VERSION:=$(sed -n 's/^REALM_CORE_VERSION=\(.*\)$/\1/p' "${source_root}/dependencies.list")}" # Provide a fallback value for TMPDIR, relevant for Xcode Bots @@ -28,11 +28,7 @@ copy_core() { tries_left=3 readonly version="$REALM_CORE_VERSION" -nightly_version="" -if [[ "$REALM_CORE_VERSION" == *"nightly"* ]]; then - nightly_version="v${version}/cocoa/" -fi -readonly url="${REALM_BASE_URL}/core/${nightly_version}realm-monorepo-xcframework-v${version}.tar.xz" +readonly url="${REALM_BASE_URL}/v${version}/cocoa/realm-monorepo-xcframework-v${version}.tar.xz" # First check if we need to do anything if [ -e "$dst" ]; then if [ -e "$dst/version.txt" ]; then