diff --git a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift index 2df3bb08..8983cdcd 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift @@ -43,10 +43,11 @@ configuration: configuration ) } + try migrate(metadatabase: metadatabase) return metadatabase } - func metadatabaseMigrator() -> DatabaseMigrator { + func migrate(metadatabase: some DatabaseWriter) throws { var migrator = DatabaseMigrator() migrator.registerMigration("Create Metadata Tables") { db in try #sql( @@ -124,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 diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 7893e616..7f25195b 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -15,6 +15,8 @@ #endif /// 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 @@ -285,69 +287,57 @@ } 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. + try userDatabase.write { db in + try setUpSyncEngine(writableDB: db) + } + } - Metadatabase migrations must not be modified after release. - """ + nonisolated package func setUpSyncEngine(writableDB 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? + """ ) } - #endif - try migrator.migrate(metadatabase) - 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: """ - 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) + """ + ) + .execute(db) + } + db.add(function: $datetime) + db.add(function: $syncEngineIsSynchronizingChanges) + db.add(function: $didUpdate) + db.add(function: $didDelete) + db.add(function: $hasPermission) - } 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) - } + for trigger in SyncMetadata.callbackTriggers(for: self) { + try trigger.execute(db) + } - for table in tables { - try table.createTriggers( - foreignKeysByTableName: foreignKeysByTableName, - tablesByName: tablesByName, - db: db - ) - } + for table in tables { + try table.createTriggers( + foreignKeysByTableName: foreignKeysByTableName, + tablesByName: tablesByName, + db: db + ) } } @@ -570,12 +560,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) { @@ -584,9 +576,10 @@ } open(table) } + try setUpSyncEngine(writableDB: db) } } - try setUpSyncEngine() + try await start() } @DatabaseFunction( @@ -1045,8 +1038,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 @@ -1526,9 +1519,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 } @@ -1537,8 +1530,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 } diff --git a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift index af2e86ca..fd1a0a10 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift @@ -384,10 +384,22 @@ } @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() }