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
8 changes: 5 additions & 3 deletions Sources/SQLiteData/CloudKit/CloudKitSharing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@

public func unshare<T: PrimaryKeyedTable>(record: T) async throws
where T.TableColumns.PrimaryKey.QueryOutput: IdentifierStringConvertible {
let share = try await userDatabase.read { [recordName = record.recordName] db in
let share = try await metadatabase.read { [recordName = record.recordName] db in
try SyncMetadata
.where { $0.recordName.eq(recordName) }
.select(\.share)
Expand Down Expand Up @@ -289,8 +289,10 @@
}

public func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
withErrorReporting(.sqliteDataCloudKitFailure) {
try syncEngine.deleteShare(recordID: share.recordID)
Task {
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await syncEngine.deleteShare(recordID: share.recordID)
}
}
didStopSharing()
}
Expand Down
65 changes: 28 additions & 37 deletions Sources/SQLiteData/CloudKit/SyncEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@
!hasSchemaChanges,
"""
A previously run migration has been removed or edited.

Metadatabase migrations must not be modified after release.
"""
)
Expand Down Expand Up @@ -344,9 +344,9 @@

private func start() throws -> Task<Void, Never> {
guard !isRunning else { return Task {} }
let (privateSyncEngine, sharedSyncEngine) = defaultSyncEngines(metadatabase, self)
observationRegistrar.withMutation(of: self, keyPath: \.isRunning) {
syncEngines.withValue {
let (privateSyncEngine, sharedSyncEngine) = defaultSyncEngines(metadatabase, self)
$0 = SyncEngines(
private: privateSyncEngine,
shared: sharedSyncEngine
Expand Down Expand Up @@ -434,13 +434,11 @@

try await userDatabase.write { db in
try PendingRecordZoneChange.delete().execute(db)
}

let newTableNames = currentRecordTypeByTableName.keys.filter { tableName in
previousRecordTypeByTableName[tableName] == nil
}
let newTableNames = currentRecordTypeByTableName.keys.filter { tableName in
previousRecordTypeByTableName[tableName] == nil
}

try await userDatabase.write { db in
try Self.$_isSynchronizingChanges.withValue(false) {
for tableName in newTableNames {
try self.uploadRecordsToCloudKit(tableName: tableName, db: db)
Expand Down Expand Up @@ -671,7 +669,7 @@
case .accountChange(let changeType):
await handleAccountChange(changeType: changeType, syncEngine: syncEngine)
case .stateUpdate(let stateSerialization):
handleStateUpdate(stateSerialization: stateSerialization, syncEngine: syncEngine)
await handleStateUpdate(stateSerialization: stateSerialization, syncEngine: syncEngine)
case .fetchedDatabaseChanges(let modifications, let deletions):
await handleFetchedDatabaseChanges(
modifications: modifications,
Expand Down Expand Up @@ -987,13 +985,6 @@
switch changeType {
case .signIn:
syncEngine.state.add(pendingDatabaseChanges: [.saveZone(defaultZone)])
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await userDatabase.write { db in
for table in self.tables {
try self.uploadRecordsToCloudKit(table: table, db: db)
}
}
}
case .signOut, .switchAccounts:
withErrorReporting(.sqliteDataCloudKitFailure) {
try deleteLocalData()
Expand All @@ -1006,9 +997,9 @@
package func handleStateUpdate(
stateSerialization: CKSyncEngine.State.Serialization,
syncEngine: any SyncEngineProtocol
) {
withErrorReporting(.sqliteDataCloudKitFailure) {
try userDatabase.write { db in
) async {
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await userDatabase.write { db in
try StateSerialization.upsert {
StateSerialization.Draft(
scope: syncEngine.database.databaseScope,
Expand Down Expand Up @@ -1136,8 +1127,8 @@
open(table)
} else if recordType == CKRecord.SystemType.share {
for recordID in recordIDs {
withErrorReporting(.sqliteDataCloudKitFailure) {
try deleteShare(recordID: recordID)
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await deleteShare(recordID: recordID)
}
}
} else {
Expand Down Expand Up @@ -1249,9 +1240,9 @@
syncEngine.state.add(pendingRecordZoneChanges: newPendingRecordZoneChanges)
}
for (failedRecord, error) in failedRecordSaves {
func clearServerRecord() {
withErrorReporting(.sqliteDataCloudKitFailure) {
try userDatabase.write { db in
func clearServerRecord() async {
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await userDatabase.write { db in
try SyncMetadata
.where { $0.recordName.eq(failedRecord.recordID.recordName) }
.update { $0.setLastKnownServerRecord(nil) }
Expand All @@ -1270,14 +1261,14 @@
let zone = CKRecordZone(zoneID: failedRecord.recordID.zoneID)
newPendingDatabaseChanges.append(.saveZone(zone))
newPendingRecordZoneChanges.append(.saveRecord(failedRecord.recordID))
clearServerRecord()
await clearServerRecord()

case .unknownItem:
newPendingRecordZoneChanges.append(.saveRecord(failedRecord.recordID))
clearServerRecord()
await clearServerRecord()

case .serverRejectedRequest:
clearServerRecord()
await clearServerRecord()

case .referenceViolation:
guard
Expand All @@ -1286,8 +1277,8 @@
foreignKeysByTableName[table.tableName]?.count == 1,
let foreignKey = foreignKeysByTableName[table.tableName]?.first
else { continue }
func open<T: PrimaryKeyedTable>(_: T.Type) throws {
try userDatabase.write { db in
func open<T: PrimaryKeyedTable>(_: T.Type) async throws {
try await userDatabase.write { db in
try Self.$_isSynchronizingChanges.withValue(false) {
switch foreignKey.onDelete {
case .cascade:
Expand Down Expand Up @@ -1333,8 +1324,8 @@
}
}
}
withErrorReporting(.sqliteDataCloudKitFailure) {
try open(table)
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await open(table)
}

case .permissionFailure:
Expand Down Expand Up @@ -1411,8 +1402,8 @@
}
}

func deleteShare(recordID: CKRecord.ID) throws {
try userDatabase.write { db in
func deleteShare(recordID: CKRecord.ID) async throws {
try await userDatabase.write { db in
let shareAndRecordName =
try SyncMetadata
.where(\.isShared)
Expand Down Expand Up @@ -1522,9 +1513,9 @@
private func refreshLastKnownServerRecord(_ record: CKRecord) async {
let metadata = await metadataFor(recordName: record.recordID.recordName)

func updateLastKnownServerRecord() {
withErrorReporting(.sqliteDataCloudKitFailure) {
try userDatabase.write { db in
func updateLastKnownServerRecord() async {
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await userDatabase.write { db in
try SyncMetadata
.where { $0.recordName.eq(record.recordID.recordName) }
.update { $0.setLastKnownServerRecord(record) }
Expand All @@ -1535,10 +1526,10 @@

if let lastKnownDate = metadata?.lastKnownServerRecord?.modificationDate {
if let recordDate = record.modificationDate, lastKnownDate < recordDate {
updateLastKnownServerRecord()
await updateLastKnownServerRecord()
}
} else {
updateLastKnownServerRecord()
await updateLastKnownServerRecord()
}
}

Expand Down
18 changes: 9 additions & 9 deletions Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,13 +658,13 @@ And in preivews you can use it like so:
### Convert Int primary keys to UUID

The most important step for migrating an existing SQLite database to be compatible with CloudKit
synchronization is converting any `Int` primary keys in your tables to UUID, or some other
synchronization is converting any `Int` primary keys in your tables to UUID, or some other
globally unique identifier. This can be done in a new migration that is registered when provisioning
your database, but it does take a few queries to accomplish because SQLite does not support
changing the definition of an existing column.
your database, but it does take a few queries to accomplish because SQLite does not support
changing the definition of an existing column.

The steps are roughly: 1) create a table with the new schema, 2) copy data over from old
table to new table and convert integer IDs to UUIDs, 3) drop the old table, and finally 4) rename
The steps are roughly: 1) create a table with the new schema, 2) copy data over from old
table to new table and convert integer IDs to UUIDs, 3) drop the old table, and finally 4) rename
the new table to have the same name as the old table.

```swift
Expand All @@ -683,7 +683,7 @@ migrator.registerMigration("Convert 'remindersLists' table primary key to UUID")
try #sql("""
INSERT INTO "new_remindersLists"
(
"id",
"id",
-- all other columns from 'remindersLists' table
)
SELECT
Expand All @@ -708,7 +708,7 @@ migrator.registerMigration("Convert 'remindersLists' table primary key to UUID")
}
```

This will need to be done for every table that uses an integer for its primary key. Further,
This will need to be done for every table that uses an integer for its primary key. Further,
for tables with foreign keys, you will need to adapt step 1 to change the types of those
columns to TEXT and will need to perform the integer-to-UUID conversion for those columns in
step 2:
Expand All @@ -730,7 +730,7 @@ migrator.registerMigration("Convert 'reminders' table primary key to UUID") { db
try #sql("""
INSERT INTO "new_reminders"
(
"id",
"id",
"remindersListID",
-- all other columns from 'reminders' table
)
Expand Down Expand Up @@ -866,7 +866,7 @@ from CloudKit.
### Developing in the simulator

It is possible to develop your app with CloudKit synchronization using the iOS simulator, but
you must be aware that simulators do not support push notifications, and so changes do not
you must be aware that simulators do not support push notifications, and so changes do not
synchronize from CloudKit to simulator automatically. Sometimes you can simply close and re-open
the app to have the simulator sync with CloudKit, but the most certain way to force synchronization
is to kill the app and relaunch it fresh.
92 changes: 44 additions & 48 deletions Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@

await signOut()

try {
try userDatabase.userRead { db in
try #expect(RemindersList.count().fetchOne(db) == 0)
try #expect(Reminder.count().fetchOne(db) == 0)
try #expect(RemindersListPrivate.count().fetchOne(db) == 0)
try #expect(UnsyncedModel.count().fetchOne(db) == 1)
try #expect(SyncMetadata.count().fetchOne(db) == 0)
}
}()
try await userDatabase.userRead { db in
try #expect(RemindersList.count().fetchOne(db) == 0)
try #expect(Reminder.count().fetchOne(db) == 0)
try #expect(RemindersListPrivate.count().fetchOne(db) == 0)
try #expect(UnsyncedModel.count().fetchOne(db) == 1)
}

try await syncEngine.metadatabase.read { db in
try #expect(SyncMetadata.count().fetchOne(db) == 0)
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
Expand All @@ -47,15 +48,15 @@
}
}

try {
try userDatabase.read { db in
try #expect(RemindersList.count().fetchOne(db) == 1)
try #expect(Reminder.count().fetchOne(db) == 1)
try #expect(RemindersListPrivate.count().fetchOne(db) == 1)
try #expect(UnsyncedModel.count().fetchOne(db) == 1)
try #expect(SyncMetadata.count().fetchOne(db) == 3)
}
}()
try await userDatabase.read { db in
try #expect(RemindersList.count().fetchOne(db) == 1)
try #expect(Reminder.count().fetchOne(db) == 1)
try #expect(RemindersListPrivate.count().fetchOne(db) == 1)
try #expect(UnsyncedModel.count().fetchOne(db) == 1)
}
try await syncEngine.metadatabase.read { db in
try #expect(SyncMetadata.count().fetchOne(db) == 3)
}

await signIn()

Expand Down Expand Up @@ -106,40 +107,35 @@
}
}

@MainActor
@Suite(.accountStatus(.noAccount))
final class SignedOutTests: BaseCloudKitTests, @unchecked Sendable {
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
init() async throws {
try await super.init { userDatabase in
try await userDatabase.write { db in
try db.seed {
RemindersList(id: 1, title: "Personal")
Reminder(id: 1, title: "Get milk", remindersListID: 1)
RemindersListPrivate(id: 1, remindersListID: 1)
UnsyncedModel(id: 1)
}
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Test(
.accountStatus(.noAccount),
.prepareDatabase { userDatabase in
try await userDatabase.write { db in
try db.seed {
RemindersList(id: 1, title: "Personal")
Reminder(id: 1, title: "Get milk", remindersListID: 1)
RemindersListPrivate(id: 1, remindersListID: 1)
UnsyncedModel(id: 1)
}
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Test func doNotUploadExistingDataToCloudKitWhenSignedOut() {
assertQuery(SyncMetadata.all, database: userDatabase.database)
assertInlineSnapshot(of: container, as: .customDump) {
"""
MockCloudContainer(
privateCloudDatabase: MockCloudDatabase(
databaseScope: .private,
storage: []
),
sharedCloudDatabase: MockCloudDatabase(
databaseScope: .shared,
storage: []
)
)
func doNotUploadExistingDataToCloudKitWhenSignedOut() {
assertQuery(SyncMetadata.all, database: userDatabase.database)
assertInlineSnapshot(of: container, as: .customDump) {
"""
MockCloudContainer(
privateCloudDatabase: MockCloudDatabase(
databaseScope: .private,
storage: []
),
sharedCloudDatabase: MockCloudDatabase(
databaseScope: .shared,
storage: []
)
"""
}
)
"""
}
}
}
Expand Down
Loading