Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@
configuration: configuration
)
}
try migrate(metadatabase: metadatabase)
return metadatabase
Comment on lines +46 to 47
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now migrate on tear down instead of set up so that we can perform the rest of set up within the same transaction that deletes from the user's tables.

}

func metadatabaseMigrator() -> DatabaseMigrator {
func migrate(metadatabase: some DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("Create Metadata Tables") { db in
try #sql(
Expand Down Expand Up @@ -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
129 changes: 61 additions & 68 deletions Sources/SQLiteData/CloudKit/SyncEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#endif

/// An object that manages the synchronization of local and remote SQLite data.
///
/// See <doc:CloudKit> for more information.
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
public final class SyncEngine: Observable, Sendable {
package let userDatabase: UserDatabase
Expand Down Expand Up @@ -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
)
}
}

Expand Down Expand Up @@ -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: PrimaryKeyedTable>(_: T.Type) {
withErrorReporting(.sqliteDataCloudKitFailure) {
Expand All @@ -584,9 +576,10 @@
}
open(table)
}
try setUpSyncEngine(writableDB: db)
}
}
try setUpSyncEngine()
try await start()
}

@DatabaseFunction(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down
16 changes: 14 additions & 2 deletions Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down