From 05cf2ff30c2c1ecd6514e7f08b6b2ee99b342b11 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 6 Sep 2025 15:41:20 -0500 Subject: [PATCH 1/5] Fix sign out. --- .../CloudKit/Internal/Metadatabase.swift | 19 +++ Sources/SQLiteData/CloudKit/SyncEngine.swift | 119 +++++++++--------- .../CloudKitTests/RecordTypeTests.swift | 15 ++- 3 files changed, 89 insertions(+), 64 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift index e1912ad7..80272ed2 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift @@ -43,9 +43,28 @@ configuration: configuration ) } + try migrate(metadatabase: metadatabase) return metadatabase } + func migrate(metadatabase: some DatabaseWriter) throws { + let migrator = metadatabaseMigrator() + #if DEBUG + try metadatabase.read { db in + let hasSchemaChanges = try migrator.hasSchemaChanges(db) + assert( + !hasSchemaChanges, + """ + A previously run migration has been removed or edited. + Metadatabase migrations must not be modified after release. + """ + ) + } + #endif + try migrator.migrate(metadatabase) + } + +// TODO: merge this into migrate above? func metadatabaseMigrator() -> DatabaseMigrator { var migrator = DatabaseMigrator() migrator.registerMigration("Create Metadata Tables") { db in diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 06450fc1..737572bd 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -11,6 +11,8 @@ import SwiftData /// An object that manages the synchronization of local and remote SQLite data. + /// + /// See for more information. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public final class SyncEngine: Observable, Sendable { package let userDatabase: UserDatabase @@ -253,70 +255,58 @@ @TaskLocal package static var _isSynchronizingChanges = false - nonisolated package func setUpSyncEngine() throws { - let migrator = metadatabaseMigrator() - #if DEBUG - try metadatabase.read { db in - let hasSchemaChanges = try migrator.hasSchemaChanges(db) - assert( - !hasSchemaChanges, - """ - A previously run migration has been removed or edited. - - Metadatabase migrations must not be modified after release. - """ - ) - } - #endif - try migrator.migrate(metadatabase) - + package func setUpSyncEngine() throws { try userDatabase.write { db in - let attachedMetadatabasePath: String? = - try PragmaDatabaseList - .where { $0.name.eq(String.sqliteDataCloudKitSchemaName) } - .select(\.file) - .fetchOne(db) - if let attachedMetadatabasePath { - let attachedMetadatabaseName = URL(filePath: metadatabase.path).lastPathComponent - let metadatabaseName = URL(filePath: attachedMetadatabasePath).lastPathComponent - if attachedMetadatabaseName != metadatabaseName { - throw SchemaError( - reason: .metadatabaseMismatch( - attachedPath: attachedMetadatabasePath, - syncEngineConfiguredPath: metadatabase.path - ), - debugDescription: """ + try setUpSyncEngine(writeableDB: db) + } + } + + package func setUpSyncEngine(writeableDB db: Database) throws { + let attachedMetadatabasePath: String? = + try PragmaDatabaseList + .where { $0.name.eq(String.sqliteDataCloudKitSchemaName) } + .select(\.file) + .fetchOne(db) + if let attachedMetadatabasePath { + let attachedMetadatabaseName = URL(filePath: metadatabase.path).lastPathComponent + let metadatabaseName = URL(filePath: attachedMetadatabasePath).lastPathComponent + if attachedMetadatabaseName != metadatabaseName { + throw SchemaError( + reason: .metadatabaseMismatch( + attachedPath: attachedMetadatabasePath, + syncEngineConfiguredPath: metadatabase.path + ), + debugDescription: """ Metadatabase attached in 'prepareDatabase' does not match metadatabase prepared in \ 'SyncEngine.init'. Are different CloudKit container identifiers being provided? """ - ) - } + ) + } - } else { - try #sql( + } else { + try #sql( """ ATTACH DATABASE \(bind: metadatabase.path) AS \(quote: .sqliteDataCloudKitSchemaName) """ - ) - .execute(db) - } - db.add(function: $datetime) - db.add(function: $syncEngineIsSynchronizingChanges) - db.add(function: $didUpdate) - db.add(function: $didDelete) - db.add(function: $hasPermission) - - for trigger in SyncMetadata.callbackTriggers(for: self) { - try trigger.execute(db) - } + ) + .execute(db) + } + db.add(function: $datetime) + db.add(function: $syncEngineIsSynchronizingChanges) + db.add(function: $didUpdate) + db.add(function: $didDelete) + db.add(function: $hasPermission) - for table in tables { - try table.createTriggers( - foreignKeysByTableName: foreignKeysByTableName, - tablesByName: tablesByName, - db: db - ) - } + for trigger in SyncMetadata.callbackTriggers(for: self) { + try trigger.execute(db) + } + + for table in tables { + try table.createTriggers( + foreignKeysByTableName: foreignKeysByTableName, + tablesByName: tablesByName, + db: db + ) } } @@ -539,12 +529,14 @@ db.remove(function: $datetime) } try metadatabase.erase() + try migrate(metadatabase: metadatabase) } - func deleteLocalData() throws { + func deleteLocalData() async throws { + try stop() try tearDownSyncEngine() - withErrorReporting(.sqliteDataCloudKitFailure) { - try userDatabase.write { db in + await withErrorReporting(.sqliteDataCloudKitFailure) { + try await userDatabase.write { db in for table in tables { func open(_: T.Type) { withErrorReporting(.sqliteDataCloudKitFailure) { @@ -553,9 +545,10 @@ } open(table) } + try setUpSyncEngine(writeableDB: db) } } - try setUpSyncEngine() + try await start() } @DatabaseFunction( @@ -1011,8 +1004,8 @@ case .signIn: syncEngine.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)]) case .signOut, .switchAccounts: - withErrorReporting(.sqliteDataCloudKitFailure) { - try deleteLocalData() + await withErrorReporting(.sqliteDataCloudKitFailure) { + try await deleteLocalData() } @unknown default: break @@ -1875,8 +1868,8 @@ throw SyncEngine.SchemaError( reason: .uniquenessConstraint, debugDescription: """ - Uniqueness constraints are not supported for synchronized tables. - """ + Uniqueness constraints are not supported for synchronized tables. + """ ) } } diff --git a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift index af2e86ca..bd564035 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift @@ -384,10 +384,23 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func tearDown() async throws { + @Test func tearDownErasesMetadata() async throws { + try await userDatabase.userWrite { db in + try db.seed { RemindersList(id: 1, title: "Personal") } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + try await syncEngine.metadatabase.read { db in + try #expect(SyncMetadata.all.fetchCount(db) > 0) + try #expect(RecordType.all.fetchCount(db) > 0) + try #expect(StateSerialization.all.fetchCount(db) == 0) + } + try syncEngine.tearDownSyncEngine() try await syncEngine.metadatabase.read { db in try #expect(SQLiteSchema.all.fetchCount(db) == 0) + try #expect(SyncMetadata.all.fetchCount(db) == 0) + try #expect(RecordType.all.fetchCount(db) == 0) + try #expect(StateSerialization.all.fetchCount(db) == 0) } try syncEngine.setUpSyncEngine() } From 8ee87b8ef31b469f850581d5695e18c9f0c0a2a4 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 6 Sep 2025 16:02:58 -0500 Subject: [PATCH 2/5] wip --- .../CloudKit/Internal/Metadatabase.swift | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift index 80272ed2..0d56e7ba 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift @@ -48,24 +48,6 @@ } func migrate(metadatabase: some DatabaseWriter) throws { - let migrator = metadatabaseMigrator() - #if DEBUG - try metadatabase.read { db in - let hasSchemaChanges = try migrator.hasSchemaChanges(db) - assert( - !hasSchemaChanges, - """ - A previously run migration has been removed or edited. - Metadatabase migrations must not be modified after release. - """ - ) - } - #endif - try migrator.migrate(metadatabase) - } - -// TODO: merge this into migrate above? - func metadatabaseMigrator() -> DatabaseMigrator { var migrator = DatabaseMigrator() migrator.registerMigration("Create Metadata Tables") { db in try #sql( @@ -143,6 +125,18 @@ ) .execute(db) } - return migrator + #if DEBUG + try metadatabase.read { db in + let hasSchemaChanges = try migrator.hasSchemaChanges(db) + assert( + !hasSchemaChanges, + """ + A previously run migration has been removed or edited. + Metadatabase migrations must not be modified after release. + """ + ) + } + #endif + try migrator.migrate(metadatabase) } #endif From 8d9232430226c74fffe855b804a2df962851db42 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 6 Sep 2025 21:38:36 -0500 Subject: [PATCH 3/5] fix --- Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift index bd564035..fd1a0a10 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift @@ -397,7 +397,6 @@ try syncEngine.tearDownSyncEngine() try await syncEngine.metadatabase.read { db in - try #expect(SQLiteSchema.all.fetchCount(db) == 0) try #expect(SyncMetadata.all.fetchCount(db) == 0) try #expect(RecordType.all.fetchCount(db) == 0) try #expect(StateSerialization.all.fetchCount(db) == 0) From 797bda740bf24dd83fbf1a9ff797d41db87c98be Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 8 Sep 2025 12:11:19 -0700 Subject: [PATCH 4/5] Apply suggestions from code review --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 737572bd..b950b6de 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -257,11 +257,11 @@ package func setUpSyncEngine() throws { try userDatabase.write { db in - try setUpSyncEngine(writeableDB: db) + try setUpSyncEngine(writableDB: db) } } - package func setUpSyncEngine(writeableDB db: Database) throws { + package func setUpSyncEngine(writableDB db: Database) throws { let attachedMetadatabasePath: String? = try PragmaDatabaseList .where { $0.name.eq(String.sqliteDataCloudKitSchemaName) } @@ -545,7 +545,7 @@ } open(table) } - try setUpSyncEngine(writeableDB: db) + try setUpSyncEngine(writableDB: db) } } try await start() From 5822d68ca2dced5a9722ab69ccddbf2ab4a7b9e0 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Mon, 8 Sep 2025 14:18:42 -0500 Subject: [PATCH 5/5] merge fix --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 41 ++++++-------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index d8cc7011..de4a04b4 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -253,23 +253,6 @@ try validateSchema() } - nonisolated package func setUpSyncEngine() throws { - let migrator = metadatabaseMigrator() - #if DEBUG - try metadatabase.read { db in - let hasSchemaChanges = try migrator.hasSchemaChanges(db) - assert( - !hasSchemaChanges, - """ - A previously run migration has been removed or edited. - - Metadatabase migrations must not be modified after release. - """ - ) - } - #endif - try migrator.migrate(metadatabase) - package func setUpSyncEngine() throws { try userDatabase.write { db in try setUpSyncEngine(writableDB: db) @@ -278,7 +261,7 @@ package func setUpSyncEngine(writableDB db: Database) throws { let attachedMetadatabasePath: String? = - try PragmaDatabaseList + try PragmaDatabaseList .where { $0.name.eq(String.sqliteDataCloudKitSchemaName) } .select(\.file) .fetchOne(db) @@ -292,17 +275,17 @@ syncEngineConfiguredPath: metadatabase.path ), debugDescription: """ - Metadatabase attached in 'prepareDatabase' does not match metadatabase prepared in \ - 'SyncEngine.init'. Are different CloudKit container identifiers being provided? - """ + Metadatabase attached in 'prepareDatabase' does not match metadatabase prepared in \ + 'SyncEngine.init'. Are different CloudKit container identifiers being provided? + """ ) } } else { try #sql( - """ - ATTACH DATABASE \(bind: metadatabase.path) AS \(quote: .sqliteDataCloudKitSchemaName) - """ + """ + ATTACH DATABASE \(bind: metadatabase.path) AS \(quote: .sqliteDataCloudKitSchemaName) + """ ) .execute(db) } @@ -1503,9 +1486,9 @@ guard let row else { reportIssue( - """ - Local database record could not be found for '\(serverRecord.recordID.recordName)'. - """ + """ + Local database record could not be found for '\(serverRecord.recordID.recordName)'. + """ ) return columnNames } @@ -1514,8 +1497,8 @@ row: T(queryOutput: row), columnNames: &columnNames, parentForeignKey: foreignKeysByTableName[T.tableName]?.count == 1 - ? foreignKeysByTableName[T.tableName]?.first - : nil + ? foreignKeysByTableName[T.tableName]?.first + : nil ) return columnNames }