From bdd291d023f756efbed30d0d8447d74993791356 Mon Sep 17 00:00:00 2001 From: James Sutula Date: Thu, 12 Mar 2026 23:51:02 -0700 Subject: [PATCH 1/5] Fix CloudKit data loss on delete-then-reinsert Fixes #418 --- .../CloudKit/Internal/MockSyncEngine.swift | 22 +- .../CloudKit/Internal/Triggers.swift | 34 ++ Sources/SQLiteData/CloudKit/SyncEngine.swift | 14 + .../ChangeSupersessionTests.swift | 333 ++++++++++++++++++ .../MockSyncEngineStateTests.swift | 98 ++++++ .../CloudKitTests/TriggerTests.swift | 69 +++- 6 files changed, 553 insertions(+), 17 deletions(-) create mode 100644 Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift create mode 100644 Tests/SQLiteDataTests/CloudKitTests/MockSyncEngineStateTests.swift diff --git a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift index e8719d26..fd42047c 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift @@ -173,8 +173,15 @@ } package func add(pendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange]) { - self._pendingRecordZoneChanges.withValue { - $0.append(contentsOf: pendingRecordZoneChanges) + self._pendingRecordZoneChanges.withValue { set in + for change in pendingRecordZoneChanges { + if let id = change.id, + let supersededIndex = set.firstIndex(where: { $0.id == id && $0 != change }) + { + set.remove(at: supersededIndex) + } + set.append(change) + } } } @@ -185,8 +192,15 @@ } package func add(pendingDatabaseChanges: [CKSyncEngine.PendingDatabaseChange]) { - self._pendingDatabaseChanges.withValue { - $0.append(contentsOf: pendingDatabaseChanges) + self._pendingDatabaseChanges.withValue { set in + for change in pendingDatabaseChanges { + if let zoneID = change.zoneID, + let supersededIndex = set.firstIndex(where: { $0.zoneID == zoneID && $0 != change }) + { + set.remove(at: supersededIndex) + } + set.append(change) + } } } diff --git a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift index 93411a74..9e06ebe4 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift @@ -82,6 +82,16 @@ defaultZone: defaultZone, privateTables: privateTables ) + SyncMetadata + .where { + $0.recordPrimaryKey.eq(#sql("\(new.primaryKey)")) + && $0.recordType.eq(tableName) + && $0._isDeleted + } + .update { + $0._isDeleted = false + $0.userModificationTime = $currentTime() + } } ) } @@ -242,6 +252,7 @@ afterZoneUpdateTrigger(), afterUpdateTrigger(for: syncEngine), afterSoftDeleteTrigger(for: syncEngine), + afterUndeleteTrigger(for: syncEngine), ] } @@ -348,6 +359,29 @@ } ) } + + fileprivate static func afterUndeleteTrigger( + for syncEngine: SyncEngine + ) -> TemporaryTrigger { + createTemporaryTrigger( + "\(String.sqliteDataCloudKitSchemaName)_after_undelete_on_sqlitedata_icloud_metadata", + ifNotExists: true, + after: .update(of: \._isDeleted) { _, new in + Values( + syncEngine.$didUpdate( + recordName: new.recordName, + zoneName: new.zoneName, + ownerName: new.ownerName, + oldZoneName: new.zoneName, + oldOwnerName: new.ownerName, + descendantRecordNames: #bind(nil) + ) + ) + } when: { old, new in + old._isDeleted && !new._isDeleted && !SyncEngine.$isSynchronizing + } + ) + } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 871c7999..f2dbdcf7 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -2092,6 +2092,20 @@ } } + @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) + extension CKSyncEngine.PendingDatabaseChange { + var zoneID: CKRecordZone.ID? { + switch self { + case .saveZone(let zone): + return zone.zoneID + case .deleteZone(let zoneID): + return zoneID + @unknown default: + return nil + } + } + } + extension CKRecord.ID { var tableName: String? { guard diff --git a/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift b/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift new file mode 100644 index 00000000..67054c7a --- /dev/null +++ b/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift @@ -0,0 +1,333 @@ +#if canImport(CloudKit) + import CloudKit + import CustomDump + import InlineSnapshotTesting + import OrderedCollections + import SQLiteData + import SnapshotTestingCustomDump + import Testing + + extension BaseCloudKitTests { + @MainActor + final class ChangeSupersessionTests: BaseCloudKitTests, @unchecked Sendable { + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteAndReinsertInSingleWrite() async throws { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Renamed") }.execute(db) + } + + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Renamed" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteAndReinsertInSeparateWrites() async throws { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + } + try await userDatabase.userWrite { db in + try RemindersList.insert { RemindersList(id: 1, title: "Restored") }.execute(db) + } + + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Restored" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteWithoutReinsert_stillDeletes() async throws { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + } + + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteReinsertThenDeleteAgain_deletes() async throws { + try await userDatabase.userWrite { db in + try db.seed { RemindersList(id: 1, title: "Original") } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Middle") }.execute(db) + try RemindersList.find(1).delete().execute(db) + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func balancedDeleteReinsertCycles_savesWithFinalValues() async throws { + try await userDatabase.userWrite { db in + try db.seed { RemindersList(id: 1, title: "Original") } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Middle") }.execute(db) + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Final") }.execute(db) + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Final" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func updateThenDelete_deletes() async throws { + try await userDatabase.userWrite { db in + try db.seed { RemindersList(id: 1, title: "Original") } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) + } + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func updateThenDeleteThenReinsert_savesWithReinsertedValues() async throws { + try await userDatabase.userWrite { db in + try db.seed { RemindersList(id: 1, title: "Original") } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await userDatabase.userWrite { db in + try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) + } + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Reinserted" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + // A second delete+reinsert cycle should propagate the cycle-2 field values to CloudKit, + // not the stale cycle-1 values. Regression test for stale userModificationTime timestamps. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func secondDeleteAndReinsertPropagatesCycle2Values() async throws { + // Seed and sync initial record. + try await userDatabase.userWrite { db in + try db.seed { RemindersList(id: 1, title: "Original") } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + // Cycle 1: delete + reinsert. + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Cycle1") }.execute(db) + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + // Cycle 2: delete + reinsert with new value. + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Cycle2") }.execute(db) + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Cycle2" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + } + } +#endif diff --git a/Tests/SQLiteDataTests/CloudKitTests/MockSyncEngineStateTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MockSyncEngineStateTests.swift new file mode 100644 index 00000000..5c7b9c0b --- /dev/null +++ b/Tests/SQLiteDataTests/CloudKitTests/MockSyncEngineStateTests.swift @@ -0,0 +1,98 @@ +#if canImport(CloudKit) + import CloudKit + import SQLiteData + import Testing + + @Suite struct MockSyncEngineStateTests { + let recordIDa = CKRecord.ID(recordName: "A") + let recordIDb = CKRecord.ID(recordName: "B") + let zoneIDa = CKRecordZone.ID(zoneName: "A", ownerName: CKCurrentUserDefaultName) + let zoneIDb = CKRecordZone.ID(zoneName: "B", ownerName: CKCurrentUserDefaultName) + + @Suite struct PendingRecordZoneChanges { + let state = MockSyncEngineState() + let idA = CKRecord.ID(recordName: "A") + let idB = CKRecord.ID(recordName: "B") + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func sameType_isDeduplicatedAtOriginalPosition() { + state.add(pendingRecordZoneChanges: [.saveRecord(idA), .saveRecord(idB)]) + state.add(pendingRecordZoneChanges: [.saveRecord(idA)]) + #expect(state.pendingRecordZoneChanges == [.saveRecord(idA), .saveRecord(idB)]) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func saveThenDelete_deleteSupersedes() { + state.add(pendingRecordZoneChanges: [.saveRecord(idA)]) + state.add(pendingRecordZoneChanges: [.deleteRecord(idA)]) + #expect(state.pendingRecordZoneChanges == [.deleteRecord(idA)]) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteThenSave_saveSupersedes() { + state.add(pendingRecordZoneChanges: [.deleteRecord(idA)]) + state.add(pendingRecordZoneChanges: [.saveRecord(idA)]) + #expect(state.pendingRecordZoneChanges == [.saveRecord(idA)]) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteThenSaveThenDelete_lastDeleteWins() { + state.add(pendingRecordZoneChanges: [.deleteRecord(idA)]) + state.add(pendingRecordZoneChanges: [.saveRecord(idA)]) + state.add(pendingRecordZoneChanges: [.deleteRecord(idA)]) + #expect(state.pendingRecordZoneChanges == [.deleteRecord(idA)]) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func crossTypeSupersession_doesNotAffectOtherRecords() { + state.add(pendingRecordZoneChanges: [.saveRecord(idA), .saveRecord(idB)]) + state.add(pendingRecordZoneChanges: [.deleteRecord(idA)]) + #expect(state.pendingRecordZoneChanges == [.saveRecord(idB), .deleteRecord(idA)]) + } + } + + @Suite struct PendingDatabaseChanges { + let state = MockSyncEngineState() + let zoneA = CKRecordZone(zoneName: "A") + let zoneB = CKRecordZone(zoneName: "B") + var zoneAID: CKRecordZone.ID { zoneA.zoneID } + var zoneBID: CKRecordZone.ID { zoneB.zoneID } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func sameType_isDeduplicatedAtOriginalPosition() { + state.add(pendingDatabaseChanges: [.saveZone(zoneA), .saveZone(zoneB)]) + state.add(pendingDatabaseChanges: [.saveZone(zoneA)]) + #expect(state.pendingDatabaseChanges == [.saveZone(zoneA), .saveZone(zoneB)]) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func saveThenDelete_deleteSupersedes() { + state.add(pendingDatabaseChanges: [.saveZone(zoneA)]) + state.add(pendingDatabaseChanges: [.deleteZone(zoneAID)]) + #expect(state.pendingDatabaseChanges == [.deleteZone(zoneAID)]) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteThenSave_saveSupersedes() { + state.add(pendingDatabaseChanges: [.deleteZone(zoneAID)]) + state.add(pendingDatabaseChanges: [.saveZone(zoneA)]) + #expect(state.pendingDatabaseChanges == [.saveZone(zoneA)]) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deleteThenSaveThenDelete_lastDeleteWins() { + state.add(pendingDatabaseChanges: [.deleteZone(zoneAID)]) + state.add(pendingDatabaseChanges: [.saveZone(zoneA)]) + state.add(pendingDatabaseChanges: [.deleteZone(zoneAID)]) + #expect(state.pendingDatabaseChanges == [.deleteZone(zoneAID)]) + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func crossTypeSupersession_doesNotAffectOtherZones() { + state.add(pendingDatabaseChanges: [.saveZone(zoneA), .saveZone(zoneB)]) + state.add(pendingDatabaseChanges: [.deleteZone(zoneAID)]) + #expect(state.pendingDatabaseChanges == [.saveZone(zoneB), .deleteZone(zoneAID)]) + } + } + } +#endif diff --git a/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift b/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift index 4a04e1b2..c848b895 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift @@ -410,6 +410,9 @@ FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))))), '__defaultOwner__'), "new"."parentID", 'parents' ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetDefaults'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [26]: """ @@ -436,6 +439,9 @@ FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))))), '__defaultOwner__'), "new"."parentID", 'parents' ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetNulls'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [27]: """ @@ -458,6 +464,9 @@ ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelAs', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [28]: """ @@ -484,6 +493,9 @@ FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))))), '__defaultOwner__'), "new"."modelAID", 'modelAs' ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [29]: """ @@ -510,6 +522,9 @@ FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))))), '__defaultOwner__'), "new"."modelBID", 'modelBs' ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelCs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [30]: """ @@ -532,6 +547,9 @@ ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'parents', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [31]: """ @@ -554,6 +572,9 @@ ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminderTags', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminderTags'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [32]: """ @@ -580,6 +601,9 @@ FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminders'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [33]: """ @@ -606,6 +630,9 @@ FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListAssets'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [34]: """ @@ -632,6 +659,9 @@ FROM "sqlitedata_icloud_metadata" WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListPrivates'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [35]: """ @@ -654,6 +684,9 @@ ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersLists', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [36]: """ @@ -685,6 +718,9 @@ ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."title", 'tags', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."title")) AND (("sqlitedata_icloud_metadata"."recordType") = ('tags'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, [38]: """ @@ -940,6 +976,13 @@ END """, [50]: """ + CREATE TRIGGER "sqlitedata_icloud_after_undelete_on_sqlitedata_icloud_metadata" + AFTER UPDATE OF "_isDeleted" ON "sqlitedata_icloud_metadata" + FOR EACH ROW WHEN (("old"."_isDeleted") AND (NOT ("new"."_isDeleted"))) AND (NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) BEGIN + SELECT "sqlitedata_icloud_didUpdate"("new"."recordName", "new"."zoneName", "new"."ownerName", "new"."zoneName", "new"."ownerName", NULL); + END + """, + [51]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_childWithOnDeleteSetDefaults" AFTER UPDATE ON "childWithOnDeleteSetDefaults" FOR EACH ROW BEGIN @@ -972,7 +1015,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetDefaults'))); END """, - [51]: """ + [52]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_childWithOnDeleteSetNulls" AFTER UPDATE ON "childWithOnDeleteSetNulls" FOR EACH ROW BEGIN @@ -1005,7 +1048,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetNulls'))); END """, - [52]: """ + [53]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_modelAs" AFTER UPDATE ON "modelAs" FOR EACH ROW BEGIN @@ -1030,7 +1073,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))); END """, - [53]: """ + [54]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_modelBs" AFTER UPDATE ON "modelBs" FOR EACH ROW BEGIN @@ -1063,7 +1106,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))); END """, - [54]: """ + [55]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_modelCs" AFTER UPDATE ON "modelCs" FOR EACH ROW BEGIN @@ -1096,7 +1139,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelCs'))); END """, - [55]: """ + [56]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_parents" AFTER UPDATE ON "parents" FOR EACH ROW BEGIN @@ -1121,7 +1164,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))); END """, - [56]: """ + [57]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_reminderTags" AFTER UPDATE ON "reminderTags" FOR EACH ROW BEGIN @@ -1146,7 +1189,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminderTags'))); END """, - [57]: """ + [58]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_reminders" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN @@ -1179,7 +1222,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminders'))); END """, - [58]: """ + [59]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_remindersListAssets" AFTER UPDATE ON "remindersListAssets" FOR EACH ROW BEGIN @@ -1212,7 +1255,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListAssets'))); END """, - [59]: """ + [60]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_remindersListPrivates" AFTER UPDATE ON "remindersListPrivates" FOR EACH ROW BEGIN @@ -1245,7 +1288,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListPrivates'))); END """, - [60]: """ + [61]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_remindersLists" AFTER UPDATE ON "remindersLists" FOR EACH ROW BEGIN @@ -1270,7 +1313,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))); END """, - [61]: """ + [62]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_sqlitedata_icloud_metadata" AFTER UPDATE ON "sqlitedata_icloud_metadata" FOR EACH ROW WHEN (("old"."_isDeleted") = ("new"."_isDeleted")) AND (NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) BEGIN @@ -1292,7 +1335,7 @@ ) END); END """, - [62]: """ + [63]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_tags" AFTER UPDATE ON "tags" FOR EACH ROW BEGIN @@ -1317,7 +1360,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."title")) AND (("sqlitedata_icloud_metadata"."recordType") = ('tags'))); END """, - [63]: """ + [64]: """ CREATE TRIGGER "sqlitedata_icloud_after_zone_update_on_sqlitedata_icloud_metadata" AFTER UPDATE OF "zoneName", "ownerName" ON "sqlitedata_icloud_metadata" FOR EACH ROW WHEN (("new"."zoneName") <> ("old"."zoneName")) OR (("new"."ownerName") <> ("old"."ownerName")) BEGIN From a3049c7a0b13db9aa542eb33dca5ad8144fc55e5 Mon Sep 17 00:00:00 2001 From: James Sutula Date: Fri, 13 Mar 2026 12:03:06 -0700 Subject: [PATCH 2/5] Structurally enforce ID-based deduplication in MockSyncEngineState --- .../CloudKit/Internal/MockSyncEngine.swift | 62 ++++++++++++------- Sources/SQLiteData/CloudKit/SyncEngine.swift | 14 ----- .../MockSyncEngineStateTests.swift | 4 +- .../Internal/CloudKit+CustomDump.swift | 5 +- .../Internal/CloudKitTestHelpers.swift | 4 +- 5 files changed, 47 insertions(+), 42 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift index fd42047c..99e6df77 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift @@ -136,12 +136,12 @@ package final class MockSyncEngineState: CKSyncEngineStateProtocol { package let changeTag = LockIsolated(0) package let _pendingRecordZoneChanges = LockIsolated< - OrderedSet - >([] + OrderedDictionary + >([:] ) package let _pendingDatabaseChanges = LockIsolated< - OrderedSet - >([]) + OrderedDictionary + >([:]) private let fileID: StaticString private let filePath: StaticString private let line: UInt @@ -160,11 +160,11 @@ } package var pendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange] { - _pendingRecordZoneChanges.withValue { Array($0) } + _pendingRecordZoneChanges.withValue { Array($0.values) } } package var pendingDatabaseChanges: [CKSyncEngine.PendingDatabaseChange] { - _pendingDatabaseChanges.withValue { Array($0) } + _pendingDatabaseChanges.withValue { Array($0.values) } } package func removePendingChanges() { @@ -173,40 +173,58 @@ } package func add(pendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange]) { - self._pendingRecordZoneChanges.withValue { set in + self._pendingRecordZoneChanges.withValue { dict in for change in pendingRecordZoneChanges { - if let id = change.id, - let supersededIndex = set.firstIndex(where: { $0.id == id && $0 != change }) - { - set.remove(at: supersededIndex) + switch change { + case .saveRecord(let id), .deleteRecord(let id): + dict.updateValue(change, forKey: id) + @unknown default: + fatalError("Unsupported pendingRecordZoneChange: \(change)") } - set.append(change) } } } package func remove(pendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange]) { - self._pendingRecordZoneChanges.withValue { - $0.subtract(pendingRecordZoneChanges) + self._pendingRecordZoneChanges.withValue { dict in + for change in pendingRecordZoneChanges { + switch change { + case .saveRecord(let id), .deleteRecord(let id): + if dict[id] == change { dict.removeValue(forKey: id) } + @unknown default: + fatalError("Unsupported pendingRecordZoneChange: \(change)") + } + } } } package func add(pendingDatabaseChanges: [CKSyncEngine.PendingDatabaseChange]) { - self._pendingDatabaseChanges.withValue { set in + self._pendingDatabaseChanges.withValue { dict in for change in pendingDatabaseChanges { - if let zoneID = change.zoneID, - let supersededIndex = set.firstIndex(where: { $0.zoneID == zoneID && $0 != change }) - { - set.remove(at: supersededIndex) + switch change { + case .saveZone(let zone): + dict.updateValue(change, forKey: zone.zoneID) + case .deleteZone(let zoneID): + dict.updateValue(change, forKey: zoneID) + @unknown default: + fatalError("Unsupported pendingDatabaseChange: \(change)") } - set.append(change) } } } package func remove(pendingDatabaseChanges: [CKSyncEngine.PendingDatabaseChange]) { - self._pendingDatabaseChanges.withValue { - $0.subtract(pendingDatabaseChanges) + self._pendingDatabaseChanges.withValue { dict in + for change in pendingDatabaseChanges { + switch change { + case .saveZone(let zone): + if dict[zone.zoneID] == change { dict.removeValue(forKey: zone.zoneID) } + case .deleteZone(let zoneID): + if dict[zoneID] == change { dict.removeValue(forKey: zoneID) } + @unknown default: + fatalError("Unsupported pendingDatabaseChange: \(change)") + } + } } } } diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index f2dbdcf7..871c7999 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -2092,20 +2092,6 @@ } } - @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) - extension CKSyncEngine.PendingDatabaseChange { - var zoneID: CKRecordZone.ID? { - switch self { - case .saveZone(let zone): - return zone.zoneID - case .deleteZone(let zoneID): - return zoneID - @unknown default: - return nil - } - } - } - extension CKRecord.ID { var tableName: String? { guard diff --git a/Tests/SQLiteDataTests/CloudKitTests/MockSyncEngineStateTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MockSyncEngineStateTests.swift index 5c7b9c0b..311d7551 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MockSyncEngineStateTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MockSyncEngineStateTests.swift @@ -47,7 +47,7 @@ @Test func crossTypeSupersession_doesNotAffectOtherRecords() { state.add(pendingRecordZoneChanges: [.saveRecord(idA), .saveRecord(idB)]) state.add(pendingRecordZoneChanges: [.deleteRecord(idA)]) - #expect(state.pendingRecordZoneChanges == [.saveRecord(idB), .deleteRecord(idA)]) + #expect(state.pendingRecordZoneChanges == [.deleteRecord(idA), .saveRecord(idB)]) } } @@ -91,7 +91,7 @@ @Test func crossTypeSupersession_doesNotAffectOtherZones() { state.add(pendingDatabaseChanges: [.saveZone(zoneA), .saveZone(zoneB)]) state.add(pendingDatabaseChanges: [.deleteZone(zoneAID)]) - #expect(state.pendingDatabaseChanges == [.saveZone(zoneB), .deleteZone(zoneAID)]) + #expect(state.pendingDatabaseChanges == [.deleteZone(zoneAID), .saveZone(zoneB)]) } } } diff --git a/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift b/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift index 338db9df..29d65b20 100644 --- a/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift +++ b/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift @@ -1,6 +1,7 @@ #if canImport(CloudKit) import CustomDump import CloudKit + import OrderedCollections import SQLiteData extension CKDatabase.Scope: @retroactive CustomDumpStringConvertible { @@ -169,13 +170,13 @@ children: [ ( "pendingRecordZoneChanges", - _pendingRecordZoneChanges.withValue(\.self) + _pendingRecordZoneChanges.withValue { Array($0.values) } .sorted(by: comparePendingRecordZoneChange) as Any ), ( "pendingDatabaseChanges", - _pendingDatabaseChanges.withValue(\.self) + _pendingDatabaseChanges.withValue { Array($0.values) } .sorted(by: comparePendingDatabaseChange) as Any ), ], diff --git a/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift b/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift index 4716be7b..26bd1190 100644 --- a/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift +++ b/Tests/SQLiteDataTests/Internal/CloudKitTestHelpers.swift @@ -164,7 +164,7 @@ extension MockSyncEngineState { _pendingRecordZoneChanges.withValue { expectNoDifference( Set(changes), - Set($0), + Set($0.values), fileID: fileID, filePath: filePath, line: line, @@ -184,7 +184,7 @@ extension MockSyncEngineState { _pendingDatabaseChanges.withValue { expectNoDifference( Set(changes), - Set($0), + Set($0.values), fileID: fileID, filePath: filePath, line: line, From afc96c12284ed2e2fa7e9cd47d499592260d6ab9 Mon Sep 17 00:00:00 2001 From: James Sutula Date: Fri, 13 Mar 2026 19:59:10 -0700 Subject: [PATCH 3/5] Strengthen ChangeSupersessionTests --- .../ChangeSupersessionTests.swift | 183 ++++++++++-------- 1 file changed, 102 insertions(+), 81 deletions(-) diff --git a/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift b/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift index 67054c7a..3409fbae 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift @@ -11,21 +11,14 @@ @MainActor final class ChangeSupersessionTests: BaseCloudKitTests, @unchecked Sendable { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func deleteAndReinsertInSingleWrite() async throws { - try await userDatabase.userWrite { db in - try db.seed { - RemindersList(id: 1, title: "Personal") - } - } - try await syncEngine.processPendingRecordZoneChanges(scope: .private) - + @Test func insertThenDelete_deletes() async throws { try await userDatabase.userWrite { db in + try RemindersList.insert { RemindersList(id: 1, title: "Personal") }.execute(db) try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Renamed") }.execute(db) } let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) try await syncEngine.processPendingRecordZoneChanges(scope: .private) @@ -34,16 +27,7 @@ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( databaseScope: .private, - storage: [ - [0]: CKRecord( - recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), - recordType: "remindersLists", - parent: nil, - share: nil, - id: 1, - title: "Renamed" - ) - ] + storage: [] ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, @@ -55,23 +39,19 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func deleteAndReinsertInSeparateWrites() async throws { + @Test func updateThenDelete_deletes() async throws { try await userDatabase.userWrite { db in - try db.seed { - RemindersList(id: 1, title: "Personal") - } + try db.seed { RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await userDatabase.userWrite { db in + try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) try RemindersList.find(1).delete().execute(db) } - try await userDatabase.userWrite { db in - try RemindersList.insert { RemindersList(id: 1, title: "Restored") }.execute(db) - } let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) try await syncEngine.processPendingRecordZoneChanges(scope: .private) @@ -80,16 +60,7 @@ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( databaseScope: .private, - storage: [ - [0]: CKRecord( - recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), - recordType: "remindersLists", - parent: nil, - share: nil, - id: 1, - title: "Restored" - ) - ] + storage: [] ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, @@ -101,20 +72,21 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func deleteWithoutReinsert_stillDeletes() async throws { + @Test func deleteAndReinsertInSingleWrite_saves() async throws { try await userDatabase.userWrite { db in try db.seed { - RemindersList(id: 1, title: "Personal") + RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await userDatabase.userWrite { db in try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) } let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) try await syncEngine.processPendingRecordZoneChanges(scope: .private) @@ -123,7 +95,16 @@ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( databaseScope: .private, - storage: [] + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Reinserted" + ) + ] ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, @@ -135,17 +116,23 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func deleteReinsertThenDeleteAgain_deletes() async throws { + @Test func deleteAndReinsertInSeparateWrites_saves() async throws { try await userDatabase.userWrite { db in - try db.seed { RemindersList(id: 1, title: "Original") } + try db.seed { + RemindersList(id: 1, title: "Original") + } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await userDatabase.userWrite { db in try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Middle") }.execute(db) - try RemindersList.find(1).delete().execute(db) } + try await userDatabase.userWrite { db in + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + } + + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) try await syncEngine.processPendingRecordZoneChanges(scope: .private) @@ -154,7 +141,16 @@ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( databaseScope: .private, - storage: [] + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Reinserted" + ) + ] ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, @@ -166,19 +162,21 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func balancedDeleteReinsertCycles_savesWithFinalValues() async throws { + @Test func updateThenDeleteThenReinsert_saves() async throws { try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await userDatabase.userWrite { db in + try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Middle") }.execute(db) - try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Final") }.execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) } + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { @@ -193,7 +191,7 @@ parent: nil, share: nil, id: 1, - title: "Final" + title: "Reinserted" ) ] ), @@ -207,19 +205,21 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func updateThenDelete_deletes() async throws { + @Test func deleteReinsertThenDeleteAgain_deletes() async throws { try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - try await userDatabase.userWrite { db in - try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) - } try await userDatabase.userWrite { db in try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + try RemindersList.find(1).delete().execute(db) } + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { @@ -239,22 +239,28 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func updateThenDeleteThenReinsert_savesWithReinsertedValues() async throws { + @Test(.printTimestamps) func twoDeleteReinsertCyclesInSameWrite_propagatesLatestValueAndTimestamp() + async throws + { try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - try await userDatabase.userWrite { db in - try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) - } - try await userDatabase.userWrite { db in - try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + try await withDependencies { + $0.currentTime.now = 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Middle") }.execute(db) + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Final") }.execute(db) + } + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( @@ -267,7 +273,10 @@ parent: nil, share: nil, id: 1, - title: "Reinserted" + idπŸ—“οΈ: 0, + title: "Final", + titleπŸ—“οΈ: 1, + πŸ—“οΈ: 1 ) ] ), @@ -280,29 +289,38 @@ } } - // A second delete+reinsert cycle should propagate the cycle-2 field values to CloudKit, - // not the stale cycle-1 values. Regression test for stale userModificationTime timestamps. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func secondDeleteAndReinsertPropagatesCycle2Values() async throws { - // Seed and sync initial record. + @Test(.printTimestamps) func twoDeleteReinsertCyclesInSeparateBatches_propagatesLatestValueAndTimestamp() + async throws + { try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - // Cycle 1: delete + reinsert. - try await userDatabase.userWrite { db in - try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Cycle1") }.execute(db) + try await withDependencies { + $0.currentTime.now = 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Cycle1") }.execute(db) + } + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - try await syncEngine.processPendingRecordZoneChanges(scope: .private) - // Cycle 2: delete + reinsert with new value. - try await userDatabase.userWrite { db in - try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Cycle2") }.execute(db) + try await withDependencies { + $0.currentTime.now = 2 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Cycle2") }.execute(db) + } + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -316,7 +334,10 @@ parent: nil, share: nil, id: 1, - title: "Cycle2" + idπŸ—“οΈ: 0, + title: "Cycle2", + titleπŸ—“οΈ: 2, + πŸ—“οΈ: 2 ) ] ), From d39ab5ff4e985479ab778daa8598ea3dec1a9123 Mon Sep 17 00:00:00 2001 From: James Sutula Date: Sun, 15 Mar 2026 12:43:26 -0700 Subject: [PATCH 4/5] Ensure all fields get fresh timestamps on reinsertion --- .../CloudKit/Internal/Triggers.swift | 1 + Sources/SQLiteData/CloudKit/SyncEngine.swift | 1 + .../ChangeSupersessionTests.swift | 194 +++++++++++++----- .../CloudKitTests/TriggerTests.swift | 24 +-- 4 files changed, 151 insertions(+), 69 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift index 9e06ebe4..02f05c27 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift @@ -91,6 +91,7 @@ .update { $0._isDeleted = false $0.userModificationTime = $currentTime() + $0._lastKnownServerRecordAllFields = #bind(nil) } } ) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 871c7999..8bb1d5b9 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1198,6 +1198,7 @@ let record = allFields + ?? metadata.lastKnownServerRecord ?? CKRecord( recordType: metadata.recordType, recordID: recordID diff --git a/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift b/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift index 3409fbae..a2321f8e 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift @@ -70,26 +70,66 @@ """ } } - + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func deleteAndReinsertInSingleWrite_saves() async throws { + @Test func deleteThenReinsertThenDelete_deletes() async throws { try await userDatabase.userWrite { db in - try db.seed { - RemindersList(id: 1, title: "Original") - } + try db.seed { RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await userDatabase.userWrite { db in try RemindersList.find(1).delete().execute(db) try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + try RemindersList.find(1).delete().execute(db) } let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.printTimestamps) func deleteThenReinsertInSingleWrite_savesWithUpdatedTimestamps() + async throws + { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Original") + } + } try await syncEngine.processPendingRecordZoneChanges(scope: .private) + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + } + + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + } + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( @@ -102,7 +142,10 @@ parent: nil, share: nil, id: 1, - title: "Reinserted" + idπŸ—“οΈ: 1, + title: "Reinserted", + titleπŸ—“οΈ: 1, + πŸ—“οΈ: 1 ) ] ), @@ -116,7 +159,9 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func deleteAndReinsertInSeparateWrites_saves() async throws { + @Test(.printTimestamps) func deleteThenReinsertInSeparateWrites_savesWithUpdatedTimestamps() + async throws + { try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Original") @@ -124,17 +169,25 @@ } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - try await userDatabase.userWrite { db in - try RemindersList.find(1).delete().execute(db) - } - try await userDatabase.userWrite { db in - try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) - } + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + } + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) - try await syncEngine.processPendingRecordZoneChanges(scope: .private) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + } + } assertInlineSnapshot(of: container, as: .customDump) { """ @@ -148,7 +201,10 @@ parent: nil, share: nil, id: 1, - title: "Reinserted" + idπŸ—“οΈ: 2, + title: "Reinserted", + titleπŸ—“οΈ: 2, + πŸ—“οΈ: 2 ) ] ), @@ -162,22 +218,28 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func updateThenDeleteThenReinsert_saves() async throws { + @Test(.printTimestamps) func updateThenDeleteThenReinsert_savesWithUpdatedTimestamps() + async throws + { try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - try await userDatabase.userWrite { db in - try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) - try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) - } + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) - try await syncEngine.processPendingRecordZoneChanges(scope: .private) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + } assertInlineSnapshot(of: container, as: .customDump) { """ @@ -191,7 +253,10 @@ parent: nil, share: nil, id: 1, - title: "Reinserted" + idπŸ—“οΈ: 1, + title: "Reinserted", + titleπŸ—“οΈ: 1, + πŸ—“οΈ: 1 ) ] ), @@ -203,31 +268,46 @@ """ } } - + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func deleteReinsertThenDeleteAgain_deletes() async throws { + @Test(.printTimestamps) func deleteThenReinsertWithSameValue_savesWithUpdatedTimestamps() + async throws + { try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Original") } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) - try await userDatabase.userWrite { db in - try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) - try RemindersList.find(1).delete().execute(db) + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Original") }.execute(db) + } + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) - - try await syncEngine.processPendingRecordZoneChanges(scope: .private) - assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( databaseScope: .private, - storage: [] + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + idπŸ—“οΈ: 1, + title: "Original", + titleπŸ—“οΈ: 1, + πŸ—“οΈ: 1 + ) + ] ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, @@ -239,7 +319,7 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test(.printTimestamps) func twoDeleteReinsertCyclesInSameWrite_propagatesLatestValueAndTimestamp() + @Test(.printTimestamps) func twoDeleteReinsertCyclesInSameWrite_savesLatestWithUpdatedTimestamps() async throws { try await userDatabase.userWrite { db in @@ -248,7 +328,7 @@ try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await withDependencies { - $0.currentTime.now = 1 + $0.currentTime.now += 1 } operation: { try await userDatabase.userWrite { db in try RemindersList.find(1).delete().execute(db) @@ -273,7 +353,7 @@ parent: nil, share: nil, id: 1, - idπŸ—“οΈ: 0, + idπŸ—“οΈ: 1, title: "Final", titleπŸ—“οΈ: 1, πŸ—“οΈ: 1 @@ -290,7 +370,7 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test(.printTimestamps) func twoDeleteReinsertCyclesInSeparateBatches_propagatesLatestValueAndTimestamp() + @Test(.printTimestamps) func twoDeleteReinsertCyclesInSeparateBatches_savesLatestWithUpdatedTimestamps() async throws { try await userDatabase.userWrite { db in @@ -299,7 +379,7 @@ try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await withDependencies { - $0.currentTime.now = 1 + $0.currentTime.now += 1 } operation: { try await userDatabase.userWrite { db in try RemindersList.find(1).delete().execute(db) @@ -308,18 +388,18 @@ let pending = syncEngine.private.state.pendingRecordZoneChanges #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) try await syncEngine.processPendingRecordZoneChanges(scope: .private) - } - - try await withDependencies { - $0.currentTime.now = 2 - } operation: { - try await userDatabase.userWrite { db in - try RemindersList.find(1).delete().execute(db) - try RemindersList.insert { RemindersList(id: 1, title: "Cycle2") }.execute(db) + + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Cycle2") }.execute(db) + } + let pending = syncEngine.private.state.pendingRecordZoneChanges + #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) - try await syncEngine.processPendingRecordZoneChanges(scope: .private) } assertInlineSnapshot(of: container, as: .customDump) { @@ -334,7 +414,7 @@ parent: nil, share: nil, id: 1, - idπŸ—“οΈ: 0, + idπŸ—“οΈ: 2, title: "Cycle2", titleπŸ—“οΈ: 2, πŸ—“οΈ: 2 diff --git a/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift b/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift index c848b895..0cb651d1 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift @@ -411,7 +411,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))))), '__defaultOwner__'), "new"."parentID", 'parents' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetDefaults'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -440,7 +440,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))))), '__defaultOwner__'), "new"."parentID", 'parents' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetNulls'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -465,7 +465,7 @@ SELECT "new"."id", 'modelAs', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -494,7 +494,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))))), '__defaultOwner__'), "new"."modelAID", 'modelAs' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -523,7 +523,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))))), '__defaultOwner__'), "new"."modelBID", 'modelBs' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelCs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -548,7 +548,7 @@ SELECT "new"."id", 'parents', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -573,7 +573,7 @@ SELECT "new"."id", 'reminderTags', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminderTags'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -602,7 +602,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminders'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -631,7 +631,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListAssets'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -660,7 +660,7 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListPrivates'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -685,7 +685,7 @@ SELECT "new"."id", 'remindersLists', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, @@ -719,7 +719,7 @@ SELECT "new"."title", 'tags', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"() + SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."title")) AND (("sqlitedata_icloud_metadata"."recordType") = ('tags'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); END """, From 08adaa0059579158669b85e49d73bcdc8ea8e928 Mon Sep 17 00:00:00 2001 From: James Sutula Date: Sat, 21 Mar 2026 10:48:33 -0700 Subject: [PATCH 5/5] Replace _isDeleted with enum value _pendingStatus Trigger record rebuilding behavior and cleanup on _pendingStatus == .reinserted --- .../CloudKit/Internal/Metadatabase.swift | 24 ++ .../CloudKit/Internal/Triggers.swift | 29 +- Sources/SQLiteData/CloudKit/SyncEngine.swift | 62 ++- .../SQLiteData/CloudKit/SyncMetadata.swift | 33 +- .../CloudKitTests/AccountLifecycleTests.swift | 16 +- .../AttachedMetadatabaseTests.swift | 2 +- .../ChangeSupersessionTests.swift | 381 ++++++++++++++++-- .../FetchRecordZoneChangesTests.swift | 6 +- .../CloudKitTests/MetadataTests.swift | 24 +- .../CloudKitTests/SchemaChangeTests.swift | 8 +- .../CloudKitTests/SharingTests.swift | 50 +-- .../SyncEngineDelegateTests.swift | 4 +- .../SyncEngineLifecycleTests.swift | 16 +- .../CloudKitTests/TriggerTests.swift | 108 ++--- 14 files changed, 582 insertions(+), 181 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift index c8efa856..9e45d747 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift @@ -125,6 +125,30 @@ ) .execute(db) } + migrator.registerMigration("Replace _isDeleted with _pendingStatus") { db in + try #sql( + """ + ALTER TABLE "\(raw: .sqliteDataCloudKitSchemaName)_metadata" + ADD COLUMN "_pendingStatus" INTEGER + """ + ) + .execute(db) + try #sql( + """ + UPDATE "\(raw: .sqliteDataCloudKitSchemaName)_metadata" + SET "_pendingStatus" = 0 + WHERE "_isDeleted" = 1 + """ + ) + .execute(db) + try #sql( + """ + ALTER TABLE "\(raw: .sqliteDataCloudKitSchemaName)_metadata" + DROP COLUMN "_isDeleted" + """ + ) + .execute(db) + } #if DEBUG try metadatabase.read { db in let hasSchemaChanges = try migrator.hasSchemaChanges(db) diff --git a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift index 02f05c27..6e2ac83a 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift @@ -54,7 +54,7 @@ $0.recordPrimaryKey.eq(#sql("\(old.primaryKey)")) && $0.recordType.eq(tableName) } - .update { $0._isDeleted = true } + .update { $0._pendingStatus = #bind(.deleted) } } when: { old, new in old.primaryKey.neq(new.primaryKey) } @@ -86,12 +86,11 @@ .where { $0.recordPrimaryKey.eq(#sql("\(new.primaryKey)")) && $0.recordType.eq(tableName) - && $0._isDeleted + && $0._pendingStatus.eq(PendingStatus.deleted) } .update { - $0._isDeleted = false + $0._pendingStatus = #bind(.reinserted) $0.userModificationTime = $currentTime() - $0._lastKnownServerRecordAllFields = #bind(nil) } } ) @@ -150,7 +149,7 @@ $0.recordPrimaryKey.eq(#sql("\(old.primaryKey)")) && $0.recordType.eq(tableName) } - .update { $0._isDeleted = true } + .update { $0._pendingStatus = #bind(.deleted) } } when: { _ in !SyncEngine.$isSynchronizing } @@ -253,7 +252,7 @@ afterZoneUpdateTrigger(), afterUpdateTrigger(for: syncEngine), afterSoftDeleteTrigger(for: syncEngine), - afterUndeleteTrigger(for: syncEngine), + afterReinsertTrigger(for: syncEngine), ] } @@ -335,7 +334,7 @@ ) ) } when: { old, new in - old._isDeleted.eq(new._isDeleted) && !SyncEngine.$isSynchronizing + old._pendingStatus.is(new._pendingStatus) && !SyncEngine.$isSynchronizing } ) } @@ -346,7 +345,7 @@ createTemporaryTrigger( "\(String.sqliteDataCloudKitSchemaName)_after_delete_on_sqlitedata_icloud_metadata", ifNotExists: true, - after: .update(of: \._isDeleted) { _, new in + after: .update(of: \._pendingStatus) { _, new in Values( syncEngine.$didDelete( recordName: new.recordName, @@ -356,18 +355,20 @@ ) ) } when: { old, new in - !old._isDeleted && new._isDeleted && !SyncEngine.$isSynchronizing + (old._pendingStatus.is(nil) || old._pendingStatus.neq(PendingStatus.deleted)) + && new._pendingStatus.eq(PendingStatus.deleted) + && !SyncEngine.$isSynchronizing } ) } - fileprivate static func afterUndeleteTrigger( + fileprivate static func afterReinsertTrigger( for syncEngine: SyncEngine ) -> TemporaryTrigger { createTemporaryTrigger( - "\(String.sqliteDataCloudKitSchemaName)_after_undelete_on_sqlitedata_icloud_metadata", + "\(String.sqliteDataCloudKitSchemaName)_after_reinsert_on_sqlitedata_icloud_metadata", ifNotExists: true, - after: .update(of: \._isDeleted) { _, new in + after: .update(of: \._pendingStatus) { _, new in Values( syncEngine.$didUpdate( recordName: new.recordName, @@ -379,7 +380,9 @@ ) ) } when: { old, new in - old._isDeleted && !new._isDeleted && !SyncEngine.$isSynchronizing + old._pendingStatus.eq(PendingStatus.deleted) + && new._pendingStatus.eq(PendingStatus.reinserted) + && !SyncEngine.$isSynchronizing } ) } diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 8bb1d5b9..8cf5ce81 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1125,13 +1125,12 @@ let batch = await syncEngine.recordZoneChangeBatch(pendingChanges: changes) { recordID in guard - let (metadata, allFields) = await withErrorReporting( + let metadata = await withErrorReporting( .sqliteDataCloudKitFailure, catching: { try await metadatabase.read { db in try SyncMetadata .find(recordID) - .select { ($0, $0._lastKnownServerRecordAllFields) } .fetchOne(db) } } @@ -1197,8 +1196,7 @@ } let record = - allFields - ?? metadata.lastKnownServerRecord + metadata.allFieldsRecord ?? CKRecord( recordType: metadata.recordType, recordID: recordID @@ -1221,7 +1219,17 @@ with: T(queryOutput: row), userModificationTime: metadata.userModificationTime ) - await refreshLastKnownServerRecord(record) + await withErrorReporting(.sqliteDataCloudKitFailure) { + try await userDatabase.write { db in + try refreshLastKnownServerRecord(record, db: db) + if metadata._pendingStatus == .reinserted { + try SyncMetadata + .find(record.recordID) + .update { $0._pendingStatus = #bind(nil) } + .execute(db) + } + } + } sentRecord = recordID return record } @@ -1949,9 +1957,12 @@ func open(_ table: some SynchronizableTable) throws { var columnNames: [String] = T.TableColumns.writableColumns.map(\.name) if !force, - let allFields = metadata._lastKnownServerRecordAllFields, + let allFields = metadata.allFieldsRecord, let row = try T.find(#sql("\(bind: metadata.recordPrimaryKey)")).fetchOne(db) { + if metadata._pendingStatus == .reinserted { + allFields.update(with: T(queryOutput: row), userModificationTime: metadata.userModificationTime) + } serverRecord.update( with: allFields, row: T(queryOutput: row), @@ -1971,6 +1982,12 @@ .find(serverRecord.recordID) .update { $0.setLastKnownServerRecord(serverRecord) } .execute(db) + if metadata._pendingStatus == .reinserted { + try SyncMetadata + .find(serverRecord.recordID) + .update { $0._pendingStatus = #bind(nil) } + .execute(db) + } } catch { guard let error = error as? DatabaseError, @@ -1993,22 +2010,25 @@ private func refreshLastKnownServerRecord(_ record: CKRecord) async { await withErrorReporting(.sqliteDataCloudKitFailure) { try await userDatabase.write { db in - let metadata = try SyncMetadata.find(record.recordID).fetchOne(db) - func updateLastKnownServerRecord() throws { - try SyncMetadata - .find(record.recordID) - .update { $0.setLastKnownServerRecord(record) } - .execute(db) - } - - if let lastKnownDate = metadata?.lastKnownServerRecord?.modificationDate { - if let recordDate = record.modificationDate, lastKnownDate < recordDate { - try updateLastKnownServerRecord() - } - } else { - try updateLastKnownServerRecord() - } + try refreshLastKnownServerRecord(record, db: db) + } + } + } + + private func refreshLastKnownServerRecord(_ record: CKRecord, db: Database) throws { + let metadata = try SyncMetadata.find(record.recordID).fetchOne(db) + func updateLastKnownServerRecord() throws { + try SyncMetadata + .find(record.recordID) + .update { $0.setLastKnownServerRecord(record) } + .execute(db) + } + if let lastKnownDate = metadata?.lastKnownServerRecord?.modificationDate { + if let recordDate = record.modificationDate, lastKnownDate < recordDate { + try updateLastKnownServerRecord() } + } else { + try updateLastKnownServerRecord() } } diff --git a/Sources/SQLiteData/CloudKit/SyncMetadata.swift b/Sources/SQLiteData/CloudKit/SyncMetadata.swift index 22990098..5ea95b69 100644 --- a/Sources/SQLiteData/CloudKit/SyncMetadata.swift +++ b/Sources/SQLiteData/CloudKit/SyncMetadata.swift @@ -1,6 +1,17 @@ #if canImport(CloudKit) import CloudKit + /// Represents the pending synchronization state of a record. + public enum PendingStatus: Int, Hashable, Sendable, QueryBindable, QueryDecodable, QueryRepresentable { + /// Indicates the metadata has been "soft" deleted. It will be fully deleted once the + /// next batch of pending changes is processed. + case deleted = 0 + /// Indicates the record has been reinserted after being soft-deleted. This status + /// will be cleared after the next batch of pending changes is processed or server + /// record changes are applied. + case reinserted = 1 + } + /// A table that tracks metadata related to synchronized data. /// /// Each row of this table represents a synchronized record across all tables synchronized with @@ -93,9 +104,23 @@ @Column(as: CKShare?.SystemFieldsRepresentation.self) public let share: CKShare? - /// Determines if the metadata has been "soft" deleted. It will be fully deleted once the - /// next batch of pending changes is processed. - public let _isDeleted: Bool + /// The pending synchronization state of the record which can require special handling. + /// + /// `nil` indicates a normal record with standard sync behavior. + public let _pendingStatus: PendingStatus? + + /// The appropriate all-fields record to use as a base when building a `CKRecord` for upload. + /// + /// For reinserted rows, returns `lastKnownServerRecord` (system fields only) + /// For all others, returns `_lastKnownServerRecordAllFields`. + var allFieldsRecord: CKRecord? { + switch _pendingStatus { + case .reinserted: + lastKnownServerRecord + case nil, .deleted: + _lastKnownServerRecordAllFields + } + } @Column("hasLastKnownServerRecord", generated: .virtual) public let _hasLastKnownServerRecord: Bool @@ -191,7 +216,7 @@ self._hasLastKnownServerRecord = lastKnownServerRecord != nil self._isShared = share != nil self.userModificationTime = userModificationTime - self._isDeleted = false + self._pendingStatus = nil } package static func find(_ recordID: CKRecord.ID) -> Where { diff --git a/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift index c90130db..164f0d46 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift @@ -170,7 +170,7 @@ β”‚ title: "Personal" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -192,7 +192,7 @@ β”‚ lastKnownServerRecord: nil, β”‚ β”‚ _lastKnownServerRecordAllFields: nil, β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: false, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -256,7 +256,7 @@ β”‚ title: "Personal" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -292,7 +292,7 @@ β”‚ title: "Get milk" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -420,7 +420,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -442,7 +442,7 @@ β”‚ lastKnownServerRecord: nil, β”‚ β”‚ _lastKnownServerRecordAllFields: nil, β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: false, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -517,7 +517,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -553,7 +553,7 @@ β”‚ title: "Get milk" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ diff --git a/Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift index 1d13ead2..865128ed 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift @@ -55,7 +55,7 @@ β”‚ β”‚ title: "Personal" β”‚ β”‚ β”‚ ), β”‚ β”‚ β”‚ share: nil, β”‚ - β”‚ β”‚ _isDeleted: false, β”‚ + β”‚ β”‚ _pendingStatus: nil, β”‚ β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ β”‚ _isShared: false, β”‚ β”‚ β”‚ userModificationTime: 0 β”‚ diff --git a/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift b/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift index a2321f8e..fdbf04ee 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/ChangeSupersessionTests.swift @@ -8,6 +8,40 @@ import Testing extension BaseCloudKitTests { + @MainActor + func fetchReminderListMetadata(_ recordID: RemindersList.ID) async throws -> SyncMetadata? { + try await self.syncEngine.metadatabase.read { + try SyncMetadata.find(RemindersList.recordID(for: recordID)).fetchOne($0) + } + } + + @MainActor + func fetchReminderMetadata(_ recordID: Reminder.ID) async throws -> SyncMetadata? { + try await self.syncEngine.metadatabase.read { + try SyncMetadata.find(Reminder.recordID(for: recordID)).fetchOne($0) + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + func expectMetadataServerRecord( + _ metadata: SyncMetadata, + matchesContainerRecord recordID: CKRecord.ID, + fileID: StaticString = #fileID, + filePath: StaticString = #filePath, + line: UInt = #line, + column: UInt = #column + ) throws { + let containerRecord = try container.privateCloudDatabase.record(for: recordID) + var containerRecordDump = "" + var metadataServerRecordDump = "" + customDump(containerRecord, to: &containerRecordDump) + customDump(metadata._lastKnownServerRecordAllFields, to: &metadataServerRecordDump) + expectNoDifference( + metadataServerRecordDump, containerRecordDump, + fileID: fileID, filePath: filePath, line: line, column: column + ) + } + @MainActor final class ChangeSupersessionTests: BaseCloudKitTests, @unchecked Sendable { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @@ -16,11 +50,11 @@ try RemindersList.insert { RemindersList(id: 1, title: "Personal") }.execute(db) try RemindersList.find(1).delete().execute(db) } - - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .deleted) try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + #expect(try await fetchReminderListMetadata(1) == nil) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -49,11 +83,11 @@ try RemindersList.find(1).update { $0.title = "Updated" }.execute(db) try RemindersList.find(1).delete().execute(db) } - - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .deleted) try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + #expect(try await fetchReminderListMetadata(1) == nil) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -83,18 +117,59 @@ try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) try RemindersList.find(1).delete().execute(db) } + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .deleted) + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + #expect(try await fetchReminderListMetadata(1) == nil) - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.deleteRecord(RemindersList.recordID(for: 1))]) + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func insertThenDeleteThenReinsert_saves() async throws { + try await userDatabase.userWrite { db in + try RemindersList.insert { RemindersList(id: 1, title: "Original") }.execute(db) + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + } + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( privateCloudDatabase: MockCloudDatabase( databaseScope: .private, - storage: [] + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Reinserted" + ) + ] ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, @@ -124,12 +199,16 @@ try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) try await syncEngine.processPendingRecordZoneChanges(scope: .private) } + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) + assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( @@ -181,13 +260,17 @@ try await userDatabase.userWrite { db in try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) } - - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) try await syncEngine.processPendingRecordZoneChanges(scope: .private) } } + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -216,6 +299,99 @@ """ } } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.printTimestamps) func deleteThenReinsertThenUpdateInSeparateWrites_allFieldsSavedWithLatestTimestamp() + async throws + { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + Reminder(id: 1, dueDate: Date(timeIntervalSince1970: Double(0)), title: "Get milk", remindersListID: 1) + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try Reminder.find(1).delete().execute(db) + } + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try Reminder.insert { + Reminder(id: 1, dueDate: Date(timeIntervalSince1970: Double(30)), title: "(Reinserted) Get milk", remindersListID: 1) + }.execute(db) + } + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try Reminder.update { + $0.title = "(Updated) Get milk" + }.execute(db) + } + + #expect(try #require(await fetchReminderMetadata(1))._pendingStatus == .reinserted) + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + } + } + } + + let metadata = try #require(await fetchReminderMetadata(1)) + #expect(metadata._pendingStatus == nil) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: Reminder.recordID(for: 1)) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:reminders/zone/__defaultOwner__), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), + share: nil, + dueDate: Date(1970-01-01T00:00:30.000Z), + dueDateπŸ—“οΈ: 3, + id: 1, + idπŸ—“οΈ: 3, + isCompleted: 0, + isCompletedπŸ—“οΈ: 3, + priorityπŸ—“οΈ: 3, + remindersListID: 1, + remindersListIDπŸ—“οΈ: 3, + title: "(Updated) Get milk", + titleπŸ—“οΈ: 3, + πŸ—“οΈ: 3 + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + idπŸ—“οΈ: 0, + title: "Personal", + titleπŸ—“οΈ: 0, + πŸ—“οΈ: 0 + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test(.printTimestamps) func updateThenDeleteThenReinsert_savesWithUpdatedTimestamps() @@ -234,12 +410,16 @@ try RemindersList.find(1).delete().execute(db) try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) } - - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) try await syncEngine.processPendingRecordZoneChanges(scope: .private) } + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -268,7 +448,7 @@ """ } } - + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test(.printTimestamps) func deleteThenReinsertWithSameValue_savesWithUpdatedTimestamps() async throws @@ -285,10 +465,16 @@ try RemindersList.find(1).delete().execute(db) try RemindersList.insert { RemindersList(id: 1, title: "Original") }.execute(db) } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -336,10 +522,16 @@ try RemindersList.find(1).delete().execute(db) try RemindersList.insert { RemindersList(id: 1, title: "Final") }.execute(db) } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -385,8 +577,6 @@ try RemindersList.find(1).delete().execute(db) try RemindersList.insert { RemindersList(id: 1, title: "Cycle1") }.execute(db) } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) try await syncEngine.processPendingRecordZoneChanges(scope: .private) try await withDependencies { @@ -396,11 +586,17 @@ try RemindersList.find(1).delete().execute(db) try RemindersList.insert { RemindersList(id: 1, title: "Cycle2") }.execute(db) } - let pending = syncEngine.private.state.pendingRecordZoneChanges - #expect(pending == [.saveRecord(RemindersList.recordID(for: 1))]) + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } } + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -429,6 +625,139 @@ """ } } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.printTimestamps) func reinsertedRecord_staleServerUpdate_localWins() async throws { + try await userDatabase.userWrite { db in + try db.seed { RemindersList(id: 1, title: "Original") } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + } + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) + + let record = try syncEngine.private.database.record(for: RemindersList.recordID(for: 1)) + record.setValue("Server", forKey: "title", at: 0) + try await syncEngine.modifyRecords(scope: .private, saving: [record]).notify() + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + #expect(metadata.userModificationTime == 1) + + let row = try await userDatabase.read { db in + try RemindersList.find(1).fetchOne(db) + } + #expect(row?.title == "Reinserted") + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + } + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + #expect(metadata.userModificationTime == 1) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + idπŸ—“οΈ: 0, + title: "Reinserted", + titleπŸ—“οΈ: 1, + πŸ—“οΈ: 1 + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.printTimestamps) func reinsertedRecord_freshServerUpdate_serverWins() async throws { + try await userDatabase.userWrite { db in + try db.seed { RemindersList(id: 1, title: "Original") } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersList.find(1).delete().execute(db) + try RemindersList.insert { RemindersList(id: 1, title: "Reinserted") }.execute(db) + } + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == .reinserted) + + let record = try syncEngine.private.database.record(for: RemindersList.recordID(for: 1)) + record.setValue("Server", forKey: "title", at: 2) + try await syncEngine.modifyRecords(scope: .private, saving: [record]).notify() + + #expect(try #require(await fetchReminderListMetadata(1))._pendingStatus == nil) + + let row = try await userDatabase.read { db in + try RemindersList.find(1).fetchOne(db) + } + #expect(row?.title == "Server") + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + } + + let metadata = try #require(await fetchReminderListMetadata(1)) + #expect(metadata._pendingStatus == nil) + + try expectMetadataServerRecord(metadata, matchesContainerRecord: RemindersList.recordID(for: 1)) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + idπŸ—“οΈ: 0, + title: "Server", + titleπŸ—“οΈ: 2, + πŸ—“οΈ: 2 + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } } } #endif + + diff --git a/Tests/SQLiteDataTests/CloudKitTests/FetchRecordZoneChangesTests.swift b/Tests/SQLiteDataTests/CloudKitTests/FetchRecordZoneChangesTests.swift index 60242f49..0bd1fa21 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/FetchRecordZoneChangesTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/FetchRecordZoneChangesTests.swift @@ -545,7 +545,7 @@ β”‚ title: "tag" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -628,7 +628,7 @@ β”‚ title: "tag" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -698,7 +698,7 @@ β”‚ title: "weekend" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ diff --git a/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift index 0f58f0d6..c5a2703f 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MetadataTests.swift @@ -234,7 +234,7 @@ β”‚ tagID: "weekend" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -270,7 +270,7 @@ β”‚ title: "Groceries" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -301,7 +301,7 @@ β”‚ title: "Personal" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -333,7 +333,7 @@ β”‚ tagID: "weekend" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -369,7 +369,7 @@ β”‚ title: "Take a walk" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -400,7 +400,7 @@ β”‚ title: "Business" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -432,7 +432,7 @@ β”‚ tagID: "optional" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -468,7 +468,7 @@ β”‚ title: "Call accountant" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -498,7 +498,7 @@ β”‚ title: "optional" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -528,7 +528,7 @@ β”‚ title: "weekend" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -581,7 +581,7 @@ β”‚ β”‚ title: "Personal" β”‚ β”‚ β”‚ ), β”‚ β”‚ β”‚ share: nil, β”‚ - β”‚ β”‚ _isDeleted: false, β”‚ + β”‚ β”‚ _pendingStatus: nil, β”‚ β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ β”‚ _isShared: false, β”‚ β”‚ β”‚ userModificationTime: 0 β”‚ @@ -612,7 +612,7 @@ β”‚ β”‚ title: "Work" β”‚ β”‚ β”‚ ), β”‚ β”‚ β”‚ share: nil, β”‚ - β”‚ β”‚ _isDeleted: false, β”‚ + β”‚ β”‚ _pendingStatus: nil, β”‚ β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ β”‚ _isShared: false, β”‚ β”‚ β”‚ userModificationTime: 0 β”‚ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift index e2b96c39..6c2ddf7a 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift @@ -162,7 +162,7 @@ β”‚ title: "My Stuff" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -506,7 +506,7 @@ β”‚ title: "My Stuff" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 1 β”‚ @@ -618,7 +618,7 @@ β”‚ title: "Personal" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -665,7 +665,7 @@ β”‚ title: "My Stuff" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index 9d6705f4..dfa01036 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -500,7 +500,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -583,7 +583,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -618,7 +618,7 @@ β”‚ modelAID: 1 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 60 β”‚ @@ -653,7 +653,7 @@ β”‚ title: "" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 60 β”‚ @@ -1249,7 +1249,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -1285,7 +1285,7 @@ β”‚ title: "Get milk" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -1932,7 +1932,7 @@ β”‚ id: 1 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -1968,7 +1968,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2003,7 +2003,7 @@ β”‚ modelAID: 2 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 1 β”‚ @@ -2038,7 +2038,7 @@ β”‚ title: "Blob" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2237,7 +2237,7 @@ β”‚ id: 1 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2273,7 +2273,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2308,7 +2308,7 @@ β”‚ modelAID: 2 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2343,7 +2343,7 @@ β”‚ title: "Blob" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2542,7 +2542,7 @@ β”‚ id: 1 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2578,7 +2578,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2613,7 +2613,7 @@ β”‚ modelAID: 2 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2648,7 +2648,7 @@ β”‚ title: "Blob" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2827,7 +2827,7 @@ β”‚ id: 1 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2863,7 +2863,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -2898,7 +2898,7 @@ β”‚ modelAID: 1 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 1 β”‚ @@ -3070,7 +3070,7 @@ β”‚ id: 1 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -3106,7 +3106,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -3141,7 +3141,7 @@ β”‚ modelAID: 2 β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 1 β”‚ @@ -3176,7 +3176,7 @@ β”‚ title: "Blob" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift index 40c55eaf..eadfe018 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift @@ -63,7 +63,7 @@ β”‚ title: "Personal" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -137,7 +137,7 @@ β”‚ title: "Personal" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift index 7b720c2e..947b6379 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift @@ -48,7 +48,7 @@ β”‚ lastKnownServerRecord: nil, β”‚ β”‚ _lastKnownServerRecordAllFields: nil, β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: false, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -70,7 +70,7 @@ β”‚ lastKnownServerRecord: nil, β”‚ β”‚ _lastKnownServerRecordAllFields: nil, β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: false, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -328,7 +328,7 @@ β”‚ parent: nil, β”‚ β”‚ share: nil β”‚ β”‚ ), β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: true, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -350,7 +350,7 @@ β”‚ lastKnownServerRecord: nil, β”‚ β”‚ _lastKnownServerRecordAllFields: nil, β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: false, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 60 β”‚ @@ -606,7 +606,7 @@ β”‚ lastKnownServerRecord: nil, β”‚ β”‚ _lastKnownServerRecordAllFields: nil, β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: false, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -628,7 +628,7 @@ β”‚ lastKnownServerRecord: nil, β”‚ β”‚ _lastKnownServerRecordAllFields: nil, β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: false, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -684,7 +684,7 @@ β”‚ title: "Personal" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ @@ -720,7 +720,7 @@ β”‚ title: "Get milk" β”‚ β”‚ ), β”‚ β”‚ share: nil, β”‚ - β”‚ _isDeleted: false, β”‚ + β”‚ _pendingStatus: nil, β”‚ β”‚ _hasLastKnownServerRecord: true, β”‚ β”‚ _isShared: false, β”‚ β”‚ userModificationTime: 0 β”‚ diff --git a/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift b/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift index 0cb651d1..e654a956 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift @@ -43,7 +43,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetDefaults'))); END """, @@ -72,7 +72,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetNulls'))); END """, @@ -101,7 +101,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))); END """, @@ -130,7 +130,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))); END """, @@ -159,7 +159,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelCs'))); END """, @@ -188,7 +188,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))); END """, @@ -217,7 +217,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminderTags'))); END """, @@ -246,7 +246,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListAssets'))); END """, @@ -275,7 +275,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListPrivates'))); END """, @@ -304,7 +304,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))); END """, @@ -333,14 +333,14 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminders'))); END """, [22]: """ CREATE TRIGGER "sqlitedata_icloud_after_delete_on_sqlitedata_icloud_metadata" - AFTER UPDATE OF "_isDeleted" ON "sqlitedata_icloud_metadata" - FOR EACH ROW WHEN ((NOT ("old"."_isDeleted")) AND ("new"."_isDeleted")) AND (NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) BEGIN + AFTER UPDATE OF "_pendingStatus" ON "sqlitedata_icloud_metadata" + FOR EACH ROW WHEN (((("old"."_pendingStatus") IS (NULL)) OR (("old"."_pendingStatus") <> (0))) AND (("new"."_pendingStatus") = (0))) AND (NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) BEGIN SELECT "sqlitedata_icloud_didDelete"("new"."recordName", coalesce("new"."lastKnownServerRecord", ( WITH "ancestorMetadatas" AS ( SELECT "sqlitedata_icloud_metadata"."recordName" AS "recordName", "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."lastKnownServerRecord" AS "lastKnownServerRecord" @@ -382,7 +382,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."title")) AND (("sqlitedata_icloud_metadata"."recordType") = ('tags'))); END """, @@ -411,8 +411,8 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))))), '__defaultOwner__'), "new"."parentID", 'parents' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetDefaults'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetDefaults'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [26]: """ @@ -440,8 +440,8 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))))), '__defaultOwner__'), "new"."parentID", 'parents' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetNulls'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetNulls'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [27]: """ @@ -465,8 +465,8 @@ SELECT "new"."id", 'modelAs', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [28]: """ @@ -494,8 +494,8 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))))), '__defaultOwner__'), "new"."modelAID", 'modelAs' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [29]: """ @@ -523,8 +523,8 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))))), '__defaultOwner__'), "new"."modelBID", 'modelBs' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelCs'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelCs'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [30]: """ @@ -548,8 +548,8 @@ SELECT "new"."id", 'parents', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [31]: """ @@ -573,8 +573,8 @@ SELECT "new"."id", 'reminderTags', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminderTags'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminderTags'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [32]: """ @@ -602,8 +602,8 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminders'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminders'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [33]: """ @@ -631,8 +631,8 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListAssets'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListAssets'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [34]: """ @@ -660,8 +660,8 @@ WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))))), '__defaultOwner__'), "new"."remindersListID", 'remindersLists' ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListPrivates'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListPrivates'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [35]: """ @@ -685,8 +685,8 @@ SELECT "new"."id", 'remindersLists', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [36]: """ @@ -719,8 +719,8 @@ SELECT "new"."title", 'tags', coalesce("sqlitedata_icloud_currentZoneName"(), 'zone'), coalesce("sqlitedata_icloud_currentOwnerName"(), '__defaultOwner__'), NULL, NULL ON CONFLICT DO NOTHING; UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 0, "userModificationTime" = "sqlitedata_icloud_currentTime"(), "_lastKnownServerRecordAllFields" = NULL - WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."title")) AND (("sqlitedata_icloud_metadata"."recordType") = ('tags'))) AND ("sqlitedata_icloud_metadata"."_isDeleted")); + SET "_pendingStatus" = 1, "userModificationTime" = "sqlitedata_icloud_currentTime"() + WHERE (((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."title")) AND (("sqlitedata_icloud_metadata"."recordType") = ('tags'))) AND (("sqlitedata_icloud_metadata"."_pendingStatus") = (0))); END """, [38]: """ @@ -740,7 +740,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetDefaults'))); END """, @@ -761,7 +761,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetNulls'))); END """, @@ -782,7 +782,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs'))); END """, @@ -803,7 +803,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs'))); END """, @@ -824,7 +824,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelCs'))); END """, @@ -845,7 +845,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents'))); END """, @@ -866,7 +866,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminderTags'))); END """, @@ -887,7 +887,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminders'))); END """, @@ -908,7 +908,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListAssets'))); END """, @@ -929,7 +929,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListPrivates'))); END """, @@ -950,7 +950,7 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists'))); END """, @@ -971,14 +971,14 @@ FROM "rootShares" WHERE (((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share")))); UPDATE "sqlitedata_icloud_metadata" - SET "_isDeleted" = 1 + SET "_pendingStatus" = 0 WHERE ((("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("old"."title")) AND (("sqlitedata_icloud_metadata"."recordType") = ('tags'))); END """, [50]: """ - CREATE TRIGGER "sqlitedata_icloud_after_undelete_on_sqlitedata_icloud_metadata" - AFTER UPDATE OF "_isDeleted" ON "sqlitedata_icloud_metadata" - FOR EACH ROW WHEN (("old"."_isDeleted") AND (NOT ("new"."_isDeleted"))) AND (NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) BEGIN + CREATE TRIGGER "sqlitedata_icloud_after_reinsert_on_sqlitedata_icloud_metadata" + AFTER UPDATE OF "_pendingStatus" ON "sqlitedata_icloud_metadata" + FOR EACH ROW WHEN ((("old"."_pendingStatus") = (0)) AND (("new"."_pendingStatus") = (1))) AND (NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) BEGIN SELECT "sqlitedata_icloud_didUpdate"("new"."recordName", "new"."zoneName", "new"."ownerName", "new"."zoneName", "new"."ownerName", NULL); END """, @@ -1316,7 +1316,7 @@ [62]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_sqlitedata_icloud_metadata" AFTER UPDATE ON "sqlitedata_icloud_metadata" - FOR EACH ROW WHEN (("old"."_isDeleted") = ("new"."_isDeleted")) AND (NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) BEGIN + FOR EACH ROW WHEN (("old"."_pendingStatus") IS ("new"."_pendingStatus")) AND (NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) BEGIN SELECT RAISE(ABORT, 'co.pointfree.SQLiteData.CloudKit.invalid-record-name-error') WHERE NOT (((substr("new"."recordName", 1, 1)) <> ('_')) AND ((octet_length("new"."recordName")) <= (255))) AND ((octet_length("new"."recordName")) = (length("new"."recordName"))); SELECT "sqlitedata_icloud_didUpdate"("new"."recordName", "new"."zoneName", "new"."ownerName", "old"."zoneName", "old"."ownerName", CASE WHEN (("new"."zoneName") <> ("old"."zoneName")) OR (("new"."ownerName") <> ("old"."ownerName")) THEN (