-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RCOCOA-2333: Add tests for sync schema migrations #8542
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -143,8 +143,9 @@ | |
#endif | ||
|
||
private extension ObjectSchema { | ||
func stitchRule(_ partitionKeyType: String?, id: String? = nil, appId: String) -> [String: Json] { | ||
func stitchRule(_ partitionKeyType: String?, id: String? = nil, appId: String, title: String?) -> [String: Json] { | ||
var stitchProperties: [String: Json] = [:] | ||
let title = title ?? className | ||
|
||
// We only add a partition property for pbs | ||
if let partitionKeyType = partitionKeyType { | ||
|
@@ -179,12 +180,12 @@ | |
// The server currently only supports non-optional collections | ||
// but requires them to be marked as optional | ||
"required": properties.compactMap { $0.isOptional || $0.type == .any || $0.isArray || $0.isMap || $0.isSet ? nil : $0.columnName }, | ||
"title": "\(className)" | ||
"title": "\(title)" | ||
], | ||
"metadata": [ | ||
"data_source": "mongodb1", | ||
"database": "test_data", | ||
"collection": "\(className) \(appId)" | ||
"collection": "\(title) \(appId)" | ||
], | ||
"relationships": relationships | ||
] | ||
|
@@ -374,7 +375,7 @@ | |
request(httpMethod: "PUT", completionHandler: completionHandler) | ||
} | ||
|
||
func put(on group: DispatchGroup, data: Json? = nil, | ||
func put(on group: DispatchGroup, data: [String: Json]? = nil, | ||
_ completionHandler: @escaping Completion) { | ||
request(on: group, httpMethod: "PUT", data: data, completionHandler) | ||
} | ||
|
@@ -490,7 +491,7 @@ | |
} | ||
|
||
/// Shared RealmServer. This class only needs to be initialized and torn down once per test suite run. | ||
@objc public static let shared = RealmServer() | ||
Check notice on line 494 in Realm/ObjectServerTests/RealmServer.swift
|
||
|
||
/// Log level for the server and mongo processes. | ||
public var logLevel = LogLevel.none | ||
|
@@ -761,7 +762,7 @@ | |
public typealias AppId = String | ||
|
||
/// Create a new server app | ||
func createApp(syncMode: SyncMode, types: [ObjectBase.Type], persistent: Bool) throws -> AppId { | ||
func createApp(syncMode: SyncMode, types: [ObjectBase.Type], persistent: Bool, typeUpdates: [[ObjectBase.Type]] = []) throws -> AppId { | ||
let session = try XCTUnwrap(session) | ||
|
||
let info = try session.apps.post(["name": "test"]).get() | ||
|
@@ -840,7 +841,7 @@ | |
throw URLError(.badServerResponse) | ||
} | ||
|
||
let schema = types.map { ObjectiveCSupport.convert(object: $0.sharedSchema()!) } | ||
let schema = Dictionary(uniqueKeysWithValues: types.map { ($0._realmObjectName() ?? $0.className(), ObjectiveCSupport.convert(object: $0.sharedSchema()!)) }) | ||
|
||
let partitionKeyType: String? | ||
if case .pbs(let bsonType) = syncMode { | ||
|
@@ -852,12 +853,12 @@ | |
// Creating the schema is a two-step process where we first add all the | ||
// objects with their properties to them so that we can add relationships | ||
let lockedSchemaIds = Locked([String: String]()) | ||
for objectSchema in schema { | ||
app.schemas.post(on: group, objectSchema.stitchRule(partitionKeyType, appId: clientAppId)) { | ||
for (title, objectSchema) in schema { | ||
app.schemas.post(on: group, objectSchema.stitchRule(partitionKeyType, appId: clientAppId, title: title)) { | ||
switch $0 { | ||
case .success(let data): | ||
lockedSchemaIds.withLock { | ||
$0[objectSchema.className] = ((data as! [String: Any])["_id"] as! String) | ||
$0[title] = ((data as! [String: Any])["_id"] as! String) | ||
} | ||
case .failure(let error): | ||
XCTFail(error.localizedDescription) | ||
|
@@ -867,14 +868,14 @@ | |
try group.throwingWait(timeout: .now() + 5.0) | ||
|
||
let schemaIds = lockedSchemaIds.value | ||
for objectSchema in schema { | ||
let schemaId = schemaIds[objectSchema.className]! | ||
app.schemas[schemaId].put(on: group, data: objectSchema.stitchRule(partitionKeyType, id: schemaId, appId: clientAppId), failOnError) | ||
for (title, objectSchema) in schema { | ||
let schemaId = schemaIds[title]! | ||
app.schemas[schemaId].put(on: group, data: objectSchema.stitchRule(partitionKeyType, id: schemaId, appId: clientAppId, title: title), failOnError) | ||
} | ||
try group.throwingWait(timeout: .now() + 5.0) | ||
|
||
let asymmetricTables = schema.compactMap { | ||
$0.isAsymmetric ? $0.className : nil | ||
$0.value.isAsymmetric ? $0.key : nil | ||
} | ||
let serviceConfig: [String: Json] | ||
switch syncMode { | ||
|
@@ -894,6 +895,21 @@ | |
] | ||
] | ||
] | ||
|
||
// We only need to create the userData rule for .pbs since for .flx we | ||
// have a default rule that covers all collections | ||
let userDataRule: [String: Json] = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current baas version will error out if we attempt to insert this rule for a qbs app because it's incompatible with qbs rules. |
||
"database": "test_data", | ||
"collection": "UserData", | ||
"roles": [[ | ||
"name": "default", | ||
"apply_when": [:], | ||
"insert": true, | ||
"delete": true, | ||
"additional_fields": [:] | ||
]] | ||
] | ||
_ = app.services[serviceId].rules.post(userDataRule) | ||
case .flx(let fields): | ||
serviceConfig = [ | ||
"flexible_sync": [ | ||
|
@@ -950,19 +966,6 @@ | |
""" | ||
], failOnError) | ||
|
||
let rules = app.services[serviceId].rules | ||
let userDataRule: [String: Json] = [ | ||
"database": "test_data", | ||
"collection": "UserData", | ||
"roles": [[ | ||
"name": "default", | ||
"apply_when": [:], | ||
"insert": true, | ||
"delete": true, | ||
"additional_fields": [:] | ||
]] | ||
] | ||
_ = rules.post(userDataRule) | ||
app.customUserData.patch(on: group, [ | ||
"mongo_service_id": serviceId, | ||
"enabled": true, | ||
|
@@ -988,25 +991,53 @@ | |
"version": 1 | ||
], failOnError) | ||
|
||
// Disable exponential backoff when the server isn't ready for us to connect | ||
session.privateApps[appId].settings.patch(on: group, [ | ||
"sync": ["disable_client_error_backoff": true] | ||
], failOnError) | ||
Comment on lines
-992
to
-994
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're getting a 403 when we attempt to execute this request. I need to check if it's still necessary and why we're no longer authorized to make it. |
||
|
||
try group.throwingWait(timeout: .now() + 5.0) | ||
|
||
// Wait for initial sync to complete as connecting before that has a lot of problems | ||
try waitForSync(appServerId: appId, expectedCount: schema.count - asymmetricTables.count) | ||
|
||
// Schema updates - if any - need to be applied after sync has been enabled to force the creation | ||
// of different schema versions | ||
let schemaUpdates = typeUpdates.map { | ||
Dictionary(uniqueKeysWithValues: $0.map { ($0._realmObjectName() ?? $0.className(), ObjectiveCSupport.convert(object: $0.sharedSchema()!)) }) | ||
} | ||
|
||
for update in schemaUpdates { | ||
for (title, objectSchema) in update { | ||
let schemaId = schemaIds[title]! | ||
|
||
app.schemas[schemaId].put(on: group, data: objectSchema.stitchRule(partitionKeyType, appId: clientAppId, title: title)) { | ||
switch $0 { | ||
case .failure(let error): | ||
XCTFail(error.localizedDescription) | ||
default: | ||
break; | ||
} | ||
} | ||
} | ||
|
||
try group.throwingWait(timeout: .now() + 5.0) | ||
} | ||
|
||
let expectedSchemaVersion = schemaUpdates.count | ||
while (true) { | ||
let response = try app.sync.schemas.versions.get().get() as! [String: Any?] | ||
let versions = response["versions"] as! [[String: Any?]] | ||
let currentMajor = versions.map({ $0["version_major"] as! Int32 }).max()! | ||
if (currentMajor >= expectedSchemaVersion) { | ||
break | ||
} | ||
} | ||
|
||
if !persistent { | ||
appIds.append(appId) | ||
} | ||
|
||
return clientAppId | ||
} | ||
|
||
@objc public func createApp(fields: [String], types: [ObjectBase.Type], persistent: Bool = false) throws -> AppId { | ||
return try createApp(syncMode: .flx(fields), types: types, persistent: persistent) | ||
@objc public func createApp(fields: [String], types: [ObjectBase.Type], persistent: Bool = false, typeUpdates: [[ObjectBase.Type]] = []) throws -> AppId { | ||
return try createApp(syncMode: .flx(fields), types: types, persistent: persistent, typeUpdates: typeUpdates) | ||
} | ||
|
||
@objc public func createApp(partitionKeyType: String = "string", types: [ObjectBase.Type], persistent: Bool = false) throws -> AppId { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
//////////////////////////////////////////////////////////////////////////// | ||
// | ||
// Copyright 2024 Realm Inc. | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
//////////////////////////////////////////////////////////////////////////// | ||
|
||
#if os(macOS) | ||
import RealmSwift | ||
import XCTest | ||
import Combine | ||
|
||
#if canImport(RealmTestSupport) | ||
import RealmSwiftSyncTestSupport | ||
import RealmSyncTestSupport | ||
import RealmTestSupport | ||
import SwiftUI | ||
import RealmSwiftTestSupport | ||
#endif | ||
|
||
@available(macOS 13.0, *) | ||
class SyncMigrationTests: SwiftSyncTestCase { | ||
override func configuration(user: User) -> Realm.Configuration { | ||
user.flexibleSyncConfiguration() | ||
} | ||
|
||
override func createApp() throws -> String { | ||
try RealmServer.shared.createApp(fields: [], types: [ObjectWithNullablePropsV0.self], typeUpdates: [[ObjectWithNullablePropsV1.self], [ObjectWithNullablePropsV0.self]]) | ||
} | ||
|
||
override var objectTypes: [ObjectBase.Type] { | ||
[ObjectWithNullablePropsV0.self] | ||
} | ||
|
||
func openMigrationRealm(schemaVersion: UInt64, type: ObjectBase.Type) throws -> Realm { | ||
var config = try configuration() | ||
config.schemaVersion = schemaVersion | ||
config.objectTypes = [type] | ||
|
||
let realm = Realm.asyncOpen(configuration: config).await(self) | ||
RLMRealmSubscribeToAll(ObjectiveCSupport.convert(object: realm)) | ||
waitForDownloads(for: realm) | ||
|
||
return realm | ||
} | ||
|
||
func testCanMigratePropertyOptionality() throws { | ||
let realmv0 = try openMigrationRealm(schemaVersion: 0, type: ObjectWithNullablePropsV0.self) | ||
|
||
let oid = ObjectId() | ||
let uuid = UUID() | ||
let date = Date(timeIntervalSince1970: -987) | ||
|
||
try realmv0.write { | ||
realmv0.add(ObjectWithNullablePropsV0(boolCol: true, intCol: 42, doubleCol: 123.456, stringCol: "abc", binaryCol: "foo".data(using: String.Encoding.utf8)!, dateCol: date, longCol: 998877665544332211, decimalCol: Decimal128(1), uuidCol: uuid, objectIdCol: oid)) | ||
} | ||
|
||
waitForUploads(for: realmv0) | ||
|
||
let realmv1 = try openMigrationRealm(schemaVersion: 1, type: ObjectWithNullablePropsV1.self) | ||
let objv1 = realmv1.objects(ObjectWithNullablePropsV1.self).first! | ||
|
||
XCTAssertEqual(objv1.boolCol, true) | ||
XCTAssertEqual(objv1.intCol, 42) | ||
XCTAssertEqual(objv1.doubleCol, 123.456) | ||
XCTAssertEqual(objv1.stringCol, "abc") | ||
XCTAssertEqual(objv1.binaryCol, "foo".data(using: String.Encoding.utf8)!) | ||
XCTAssertEqual(objv1.dateCol, date) | ||
XCTAssertEqual(objv1.longCol, 998877665544332211) | ||
XCTAssertEqual(objv1.decimalCol, Decimal128(1)) | ||
XCTAssertEqual(objv1.uuidCol, uuid) | ||
XCTAssertEqual(objv1.objectIdCol, oid) | ||
|
||
let realmv2 = try openMigrationRealm(schemaVersion: 2, type: ObjectWithNullablePropsV0.self) | ||
let objv2 = realmv2.objects(ObjectWithNullablePropsV0.self).first! | ||
|
||
XCTAssertEqual(objv2.boolCol, true) | ||
XCTAssertEqual(objv2.intCol, 42) | ||
XCTAssertEqual(objv2.doubleCol, 123.456) | ||
XCTAssertEqual(objv2.stringCol, "abc") | ||
XCTAssertEqual(objv2.binaryCol, "foo".data(using: String.Encoding.utf8)!) | ||
XCTAssertEqual(objv2.dateCol, date) | ||
XCTAssertEqual(objv2.longCol, 998877665544332211) | ||
XCTAssertEqual(objv2.decimalCol, Decimal128(1)) | ||
XCTAssertEqual(objv2.uuidCol, uuid) | ||
XCTAssertEqual(objv2.objectIdCol, oid) | ||
} | ||
} | ||
|
||
#endif // os(macOS) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
{ | ||
"events": { | ||
"database": { | ||
"maxCoordinatorChangeStreams": 50000 | ||
"maxCoordinatorChangeStreams": "50000" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Current baas expects this to be a quoted int. |
||
}, | ||
"log_forwarder": { | ||
"enabled": false | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is required to ensure that the title of the collection matches the name of the table whenever we override the table name using
_realmObjectName
.