From 3b441f856ac160b798a51e219b52f488287a1026 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 25 Nov 2025 16:48:42 -0600 Subject: [PATCH 01/10] Private tables should always be saved in private db. --- .../CloudKit/Internal/Triggers.swift | 97 +++++++++++++------ Sources/SQLiteData/CloudKit/SyncEngine.swift | 24 ++++- .../CloudKitTests/SharingTests.swift | 87 +++++++++++++++++ 3 files changed, 177 insertions(+), 31 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift index c0aa01b4..0972f13f 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Triggers.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Triggers.swift @@ -6,20 +6,38 @@ extension PrimaryKeyedTable { static func metadataTriggers( parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> [TemporaryTrigger] { [ - afterInsert(parentForeignKey: parentForeignKey, defaultZone: defaultZone), - afterUpdate(parentForeignKey: parentForeignKey, defaultZone: defaultZone), - afterDeleteFromUser(parentForeignKey: parentForeignKey, defaultZone: defaultZone), + afterInsert( + parentForeignKey: parentForeignKey, + defaultZone: defaultZone, + privateTables: privateTables + ), + afterUpdate( + parentForeignKey: parentForeignKey, + defaultZone: defaultZone, + privateTables: privateTables + ), + afterDeleteFromUser( + parentForeignKey: parentForeignKey, + defaultZone: defaultZone, + privateTables: privateTables + ), afterDeleteFromSyncEngine, - afterPrimaryKeyChange(parentForeignKey: parentForeignKey, defaultZone: defaultZone), + afterPrimaryKeyChange( + parentForeignKey: parentForeignKey, + defaultZone: defaultZone, + privateTables: privateTables + ), ] } fileprivate static func afterPrimaryKeyChange( parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> TemporaryTrigger { createTemporaryTrigger( "\(String.sqliteDataCloudKitSchemaName)_after_primary_key_change_on_\(tableName)", @@ -28,7 +46,8 @@ checkWritePermissions( alias: new, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) SyncMetadata .where { @@ -44,7 +63,8 @@ fileprivate static func afterInsert( parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> TemporaryTrigger { createTemporaryTrigger( "\(String.sqliteDataCloudKitSchemaName)_after_insert_on_\(tableName)", @@ -53,12 +73,14 @@ checkWritePermissions( alias: new, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) SyncMetadata.insert( new: new, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) } ) @@ -66,7 +88,8 @@ fileprivate static func afterUpdate( parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> TemporaryTrigger { createTemporaryTrigger( "\(String.sqliteDataCloudKitSchemaName)_after_update_on_\(tableName)", @@ -75,17 +98,20 @@ checkWritePermissions( alias: new, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) SyncMetadata.insert( new: new, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) SyncMetadata.update( new: new, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) } ) @@ -93,7 +119,8 @@ fileprivate static func afterDeleteFromUser( parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> TemporaryTrigger< Self > { @@ -104,7 +131,8 @@ checkWritePermissions( alias: old, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) SyncMetadata .where { @@ -141,12 +169,14 @@ fileprivate static func insert( new: StructuredQueriesCore.TableAlias.TableColumns, parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> some StructuredQueriesCore.Statement { let (parentRecordPrimaryKey, parentRecordType, zoneName, ownerName) = parentFields( alias: new, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) let defaultZoneName = #sql( "\(quote: defaultZone.zoneID.zoneName, delimiter: .text)", @@ -181,12 +211,14 @@ fileprivate static func update( new: StructuredQueriesCore.TableAlias.TableColumns, parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> some StructuredQueriesCore.Statement { let (parentRecordPrimaryKey, parentRecordType, zoneName, ownerName) = parentFields( alias: new, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) return Self.where { $0.recordPrimaryKey.eq(#sql("\(new.primaryKey)")) @@ -322,15 +354,24 @@ private func parentFields( alias: StructuredQueriesCore.TableAlias.TableColumns, parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> ( parentRecordPrimaryKey: SQLQueryExpression?, parentRecordType: SQLQueryExpression?, zoneName: SQLQueryExpression, ownerName: SQLQueryExpression ) { - return - parentForeignKey + let zoneNameOverride: SQLQueryExpression + let ownerNameOverride: SQLQueryExpression + if privateTables.contains(where: { $0.base.tableName == Base.tableName }) { + zoneNameOverride = #sql("\(quote: defaultZone.zoneID.zoneName, delimiter: .text)") + ownerNameOverride = #sql("\(quote: defaultZone.zoneID.ownerName, delimiter: .text)") + } else { + zoneNameOverride = #sql("NULL") + ownerNameOverride = #sql("NULL") + } + return parentForeignKey .map { foreignKey in let parentRecordPrimaryKey = #sql( #"\#(type(of: alias).QueryValue.self).\#(quote: foreignKey.from)"#, @@ -344,8 +385,8 @@ return ( parentRecordPrimaryKey, parentRecordType, - #sql("coalesce(\($currentZoneName()), (\(parentMetadata.select(\.zoneName))))"), - #sql("coalesce(\($currentOwnerName()), (\(parentMetadata.select(\.ownerName))))") + #sql("coalesce(\(zoneNameOverride), \($currentZoneName()), (\(parentMetadata.select(\.zoneName))))"), + #sql("coalesce(\(ownerNameOverride), \($currentOwnerName()), (\(parentMetadata.select(\.ownerName))))") ) } ?? ( @@ -373,12 +414,14 @@ private func checkWritePermissions( alias: StructuredQueriesCore.TableAlias.TableColumns, parentForeignKey: ForeignKey?, - defaultZone: CKRecordZone + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable] ) -> some StructuredQueriesCore.Statement { let (parentRecordPrimaryKey, parentRecordType, _, _) = parentFields( alias: alias, parentForeignKey: parentForeignKey, - defaultZone: defaultZone + defaultZone: defaultZone, + privateTables: privateTables ) return With { diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index c6f29d18..0a985f33 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -359,6 +359,7 @@ foreignKeysByTableName: foreignKeysByTableName, tablesByName: tablesByName, defaultZone: defaultZone, + privateTables: privateTables, db: db ) } @@ -686,7 +687,8 @@ package func tearDownSyncEngine() throws { try userDatabase.write { db in for table in tables.reversed() { - try table.base.dropTriggers(defaultZone: defaultZone, db: db) + try table.base + .dropTriggers(defaultZone: defaultZone, privateTables: privateTables, db: db) } for trigger in SyncMetadata.callbackTriggers(for: self).reversed() { try trigger.drop().execute(db) @@ -886,6 +888,7 @@ foreignKeysByTableName: [String: [ForeignKey]], tablesByName: [String: any SynchronizableTable], defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable], db: Database ) throws { let parentForeignKey = @@ -893,15 +896,28 @@ ? foreignKeysByTableName[tableName]?.first : nil - for trigger in metadataTriggers(parentForeignKey: parentForeignKey, defaultZone: defaultZone) + for trigger in metadataTriggers( + parentForeignKey: parentForeignKey, + defaultZone: defaultZone, + privateTables: privateTables + ) { try trigger.execute(db) } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - fileprivate static func dropTriggers(defaultZone: CKRecordZone, db: Database) throws { - for trigger in metadataTriggers(parentForeignKey: nil, defaultZone: defaultZone).reversed() { + fileprivate static func dropTriggers( + defaultZone: CKRecordZone, + privateTables: [any SynchronizableTable], + db: Database + ) throws { + for trigger in metadataTriggers( + parentForeignKey: nil, + defaultZone: defaultZone, + privateTables: privateTables + ) + .reversed() { try trigger.drop().execute(db) } } diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index ae3b71f4..5b1b5ffa 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -203,6 +203,93 @@ } } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func privateTablesStayInPrivateDatabase() async throws { + let externalZone = CKRecordZone( + zoneID: CKRecordZone.ID( + zoneName: "external.zone", + ownerName: "external.owner" + ) + ) + try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() + + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1, zoneID: externalZone.zoneID) + ) + remindersListRecord.setValue(1, forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) + _ = try syncEngine.modifyRecords(scope: .shared, saving: [share, remindersListRecord]) + let freshShare = try syncEngine.shared.database.record(for: share.recordID) as! CKShare + let freshRemindersListRecord = try syncEngine.shared.database.record( + for: remindersListRecord.recordID + ) + + try await syncEngine + .acceptShare( + metadata: ShareMetadata( + containerIdentifier: container.containerIdentifier!, + hierarchicalRootRecordID: freshRemindersListRecord.recordID, + rootRecord: freshRemindersListRecord, + share: freshShare + ) + ) + + try await userDatabase.userWrite { db in + try RemindersListPrivate.insert { + RemindersListPrivate(remindersListID: 1, position: 42) + } + .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:remindersListPrivates/zone/__defaultOwner__), + recordType: "remindersListPrivates", + parent: nil, + share: nil, + position: 42, + remindersListID: 1 + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + title: "Personal" + ) + ] + ) + ) + """ + } + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func shareRecordBeforeSync() async throws { let error = await #expect(throws: (any Error).self) { From 05580fb9cc8ac6a551726f1e7163ff5dddfbf389 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 25 Nov 2025 16:50:36 -0600 Subject: [PATCH 02/10] snapshots --- .../CloudKitTests/TriggerTests.swift | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift b/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift index c20fc295..bc4c1ec2 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/TriggerTests.swift @@ -404,9 +404,9 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'childWithOnDeleteSetDefaults', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'childWithOnDeleteSetDefaults', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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; @@ -430,9 +430,9 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'childWithOnDeleteSetNulls', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'childWithOnDeleteSetNulls', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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; @@ -478,9 +478,9 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'modelBs', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'modelBs', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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; @@ -504,9 +504,9 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'modelCs', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'modelCs', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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; @@ -574,9 +574,9 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'reminders', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'reminders', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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; @@ -600,9 +600,9 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."remindersListID", 'remindersListAssets', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."remindersListID", 'remindersListAssets', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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; @@ -626,9 +626,9 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."remindersListID", 'remindersListPrivates', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."remindersListID", 'remindersListPrivates', coalesce(coalesce('zone', "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce('__defaultOwner__', "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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; @@ -957,16 +957,16 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'childWithOnDeleteSetDefaults', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'childWithOnDeleteSetDefaults', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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 "zoneName" = coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SET "zoneName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), "sqlitedata_icloud_metadata"."ownerName"), "parentRecordPrimaryKey" = "new"."parentID", "parentRecordType" = 'parents', "userModificationTime" = "sqlitedata_icloud_currentTime"() WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetDefaults')); @@ -990,16 +990,16 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'childWithOnDeleteSetNulls', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'childWithOnDeleteSetNulls', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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 "zoneName" = coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SET "zoneName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."parentID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('parents')))), "sqlitedata_icloud_metadata"."ownerName"), "parentRecordPrimaryKey" = "new"."parentID", "parentRecordType" = 'parents', "userModificationTime" = "sqlitedata_icloud_currentTime"() WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('childWithOnDeleteSetNulls')); @@ -1048,16 +1048,16 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'modelBs', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'modelBs', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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 "zoneName" = coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SET "zoneName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelAID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelAs')))), "sqlitedata_icloud_metadata"."ownerName"), "parentRecordPrimaryKey" = "new"."modelAID", "parentRecordType" = 'modelAs', "userModificationTime" = "sqlitedata_icloud_currentTime"() WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs')); @@ -1081,16 +1081,16 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'modelCs', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'modelCs', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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 "zoneName" = coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SET "zoneName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."modelBID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelBs')))), "sqlitedata_icloud_metadata"."ownerName"), "parentRecordPrimaryKey" = "new"."modelBID", "parentRecordType" = 'modelBs', "userModificationTime" = "sqlitedata_icloud_currentTime"() WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('modelCs')); @@ -1164,16 +1164,16 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."id", 'reminders', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."id", 'reminders', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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 "zoneName" = coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SET "zoneName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."ownerName"), "parentRecordPrimaryKey" = "new"."remindersListID", "parentRecordType" = 'remindersLists', "userModificationTime" = "sqlitedata_icloud_currentTime"() WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."id")) AND (("sqlitedata_icloud_metadata"."recordType") = ('reminders')); @@ -1197,16 +1197,16 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."remindersListID", 'remindersListAssets', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."remindersListID", 'remindersListAssets', coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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 "zoneName" = coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SET "zoneName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce(NULL, "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."ownerName"), "parentRecordPrimaryKey" = "new"."remindersListID", "parentRecordType" = 'remindersLists', "userModificationTime" = "sqlitedata_icloud_currentTime"() WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListAssets')); @@ -1230,16 +1230,16 @@ WHERE ((NOT ("sqlitedata_icloud_syncEngineIsSynchronizingChanges"())) AND (("rootShares"."parentRecordName") IS (NULL))) AND (NOT ("sqlitedata_icloud_hasPermission"("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "zoneName", "ownerName", "parentRecordPrimaryKey", "parentRecordType") - SELECT "new"."remindersListID", 'remindersListPrivates', coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SELECT "new"."remindersListID", 'remindersListPrivates', coalesce(coalesce('zone', "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), 'zone'), coalesce(coalesce('__defaultOwner__', "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" 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 "zoneName" = coalesce(coalesce("sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" + SET "zoneName" = coalesce(coalesce('zone', "sqlitedata_icloud_currentZoneName"(), (SELECT "sqlitedata_icloud_metadata"."zoneName" FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce("sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."zoneName"), "ownerName" = coalesce(coalesce('__defaultOwner__', "sqlitedata_icloud_currentOwnerName"(), (SELECT "sqlitedata_icloud_metadata"."ownerName" FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersLists')))), "sqlitedata_icloud_metadata"."ownerName"), "parentRecordPrimaryKey" = "new"."remindersListID", "parentRecordType" = 'remindersLists', "userModificationTime" = "sqlitedata_icloud_currentTime"() WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey") = ("new"."remindersListID")) AND (("sqlitedata_icloud_metadata"."recordType") = ('remindersListPrivates')); From cc652ece342e334cc91fdbcd08735a39d7868c00 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 26 Nov 2025 14:42:05 -0600 Subject: [PATCH 03/10] Handle shared database more correctly. --- .../CloudKit/Internal/MockCloudDatabase.swift | 28 +++++++++++ .../MockCloudDatabaseTests.swift | 14 +++++- .../SharingPermissionsTests.swift | 47 ++++++++++-------- .../CloudKitTests/SharingTests.swift | 48 +++++++------------ .../SyncEngineLifecycleTests.swift | 14 +++++- 5 files changed, 95 insertions(+), 56 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift index 7edfeccb..67d1d4bc 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift @@ -97,8 +97,18 @@ saveResults[recordToSave.recordID] = .failure(CKError(.invalidArguments)) continue } + } else { + // NB: Emit 'permissionFailure' if saving to shared database with no parent reference. + if databaseScope == .shared, + recordToSave.parent == nil, + recordToSave.share == nil + { + saveResults[recordToSave.recordID] = .failure(CKError(.permissionFailure)) + continue + } } + // NB: Emit 'zoneNotFound' error if saving record with a zone not found in database. guard storage[recordToSave.recordID.zoneID] != nil else { saveResults[recordToSave.recordID] = .failure(CKError(.zoneNotFound)) @@ -231,8 +241,26 @@ deleteResults[recordIDToDelete] = .failure(CKError(.referenceViolation)) continue } + let recordToDelete = storage[recordIDToDelete.zoneID]?[recordIDToDelete] storage[recordIDToDelete.zoneID]?[recordIDToDelete] = nil deleteResults[recordIDToDelete] = .success(()) + + // NB: If deleting a share, delete the shared records and all associated records. + if databaseScope == .shared, let shareToDelete = recordToDelete as? CKShare { + func deleteRecords(referencing recordID: CKRecord.ID) { + for recordToDelete in (storage[recordIDToDelete.zoneID] ?? [:]).values { + guard + recordToDelete.share?.recordID == recordID + || recordToDelete.parent?.recordID == recordID + else { + continue + } + storage[recordIDToDelete.zoneID]?[recordToDelete.recordID] = nil + deleteRecords(referencing: recordToDelete.recordID) + } + } + deleteRecords(referencing: shareToDelete.recordID) + } } return (saveResults: saveResults, deleteResults: deleteResults) diff --git a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift index 01533b59..d61b6a9f 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift @@ -406,7 +406,9 @@ let newShare = try syncEngine.private.database.record(for: CKRecord.ID(recordName: "share")) let (saveResults, _) = try syncEngine.private.database.modifyRecords(saving: [newShare]) - _ = try saveResults.values.first?.get() + #expect(throws: Never.self) { + _ = try saveResults.values.first?.get() + } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @@ -421,6 +423,16 @@ } #expect(error?.code == .unknownItem) } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func saveSharedRecordWithoutParent() async throws { + let record = CKRecord(recordType: "A", recordID: CKRecord.ID(recordName: "1")) + let (saveResults, _) = try syncEngine.shared.database.modifyRecords(saving: [record]) + let error = #expect(throws: CKError.self) { + _ = try saveResults.values.first?.get() + } + #expect(error?.code == .permissionFailure) + } } } #endif diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift index 77507724..c91a178b 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingPermissionsTests.swift @@ -113,25 +113,27 @@ reminderRecord.setValue("Get milk", forKey: "title", at: now) reminderRecord.setValue(1, forKey: "remindersListID", at: now) reminderRecord.parent = CKRecord.Reference(record: remindersListRecord, action: .none) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) + share.publicPermission = .readOnly + share.currentUserParticipant?.permission = .readOnly _ = try syncEngine.modifyRecords( scope: .shared, - saving: [reminderRecord, remindersListRecord] + saving: [reminderRecord, remindersListRecord, share] ) let freshRemindersListRecord = try syncEngine.shared.database.record( for: remindersListRecord.recordID ) - - let share = CKShare( - rootRecord: freshRemindersListRecord, - shareID: CKRecord.ID( - recordName: "share-\(freshRemindersListRecord.recordID.recordName)", - zoneID: freshRemindersListRecord.recordID.zoneID - ) + let freshShare = try #require( + syncEngine.shared.database.record(for: share.recordID) as? CKShare ) - share.publicPermission = .readOnly - share.currentUserParticipant?.permission = .readOnly try await syncEngine .acceptShare( @@ -139,7 +141,7 @@ containerIdentifier: container.containerIdentifier!, hierarchicalRootRecordID: freshRemindersListRecord.recordID, rootRecord: freshRemindersListRecord, - share: share + share: freshShare ) ) @@ -216,23 +218,26 @@ reminderRecord.setValue(1, forKey: "remindersListID", at: now) reminderRecord.setValue(false, forKey: "isCompleted", at: now) reminderRecord.parent = CKRecord.Reference(record: remindersListRecord, action: .none) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) + share.publicPermission = .readOnly + share.currentUserParticipant?.permission = .readOnly _ = try syncEngine.modifyRecords( scope: .shared, - saving: [remindersListRecord, reminderRecord] + saving: [remindersListRecord, reminderRecord, share] ) let freshRemindersListRecord = try syncEngine.shared.database.record( for: remindersListRecord.recordID ) - let share = CKShare( - rootRecord: freshRemindersListRecord, - shareID: CKRecord.ID( - recordName: "share-\(freshRemindersListRecord.recordID.recordName)", - zoneID: freshRemindersListRecord.recordID.zoneID - ) + let freshShare = try #require( + syncEngine.shared.database.record(for: share.recordID) as? CKShare ) - share.publicPermission = .readOnly - share.currentUserParticipant?.permission = .readOnly try await syncEngine .acceptShare( @@ -240,7 +245,7 @@ containerIdentifier: container.containerIdentifier!, hierarchicalRootRecordID: freshRemindersListRecord.recordID, rootRecord: freshRemindersListRecord, - share: share + share: freshShare ) ) diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index 5b1b5ffa..b162240a 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -699,10 +699,17 @@ reminderRecord.setValue("Get milk", forKey: "title", at: now) reminderRecord.setValue(1, forKey: "remindersListID", at: now) reminderRecord.parent = CKRecord.Reference(record: remindersListRecord, action: .none) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) try await syncEngine.modifyRecords( scope: .shared, - saving: [remindersListRecord, reminderRecord] + saving: [remindersListRecord, reminderRecord, share] ).notify() try await withDependencies { @@ -725,10 +732,16 @@ databaseScope: .shared, storage: [ [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), recordType: "remindersLists", parent: nil, - share: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), id: 1, title: "Personal" ) @@ -1313,36 +1326,7 @@ ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, - storage: [ - [0]: CKRecord( - recordID: CKRecord.ID(1:reminders/external.zone/external.owner), - recordType: "reminders", - parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), - share: nil, - id: 1, - isCompleted: 0, - remindersListID: 1, - title: "Get milk" - ), - [1]: CKRecord( - recordID: CKRecord.ID(2:reminders/external.zone/external.owner), - recordType: "reminders", - parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), - share: nil, - id: 2, - isCompleted: 0, - remindersListID: 1, - title: "Take a walk" - ), - [2]: CKRecord( - recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), - recordType: "remindersLists", - parent: nil, - share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), - id: 1, - title: "Personal" - ) - ] + storage: [] ) ) """ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift index b65e984c..19dc7399 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift @@ -389,6 +389,7 @@ ownerName: "external.owner" ) let externalZone = CKRecordZone(zoneID: externalZoneID) + try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() let remindersListRecord = CKRecord( recordType: RemindersList.tableName, @@ -397,9 +398,18 @@ remindersListRecord.setValue(1, forKey: "id", at: now) remindersListRecord.setValue(false, forKey: "isCompleted", at: now) remindersListRecord.setValue("Personal", forKey: "title", at: now) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) - try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() - try await syncEngine.modifyRecords(scope: .shared, saving: [remindersListRecord]).notify() + try await syncEngine.modifyRecords( + scope: .shared, + saving: [remindersListRecord, share] + ).notify() syncEngine.stop() From cdf0b53ec517603f88d344552a2c00c2dc8e12f7 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 26 Nov 2025 14:46:13 -0600 Subject: [PATCH 04/10] wip --- .../CloudKitTests/SharingTests.swift | 256 ++++++++++-------- 1 file changed, 146 insertions(+), 110 deletions(-) diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index b162240a..04f699d6 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -333,9 +333,19 @@ remindersListRecord.setValue(1, forKey: "id", at: now) remindersListRecord.setValue(false, forKey: "isCompleted", at: now) remindersListRecord.setValue("Personal", forKey: "title", at: now) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() - try await syncEngine.modifyRecords(scope: .shared, saving: [remindersListRecord]).notify() + try await syncEngine.modifyRecords( + scope: .shared, + saving: [remindersListRecord, share] + ).notify() try await withDependencies { $0.currentTime.now += 60 @@ -359,6 +369,12 @@ databaseScope: .shared, storage: [ [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( recordID: CKRecord.ID(1:reminders/external.zone/external.owner), recordType: "reminders", parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), @@ -368,11 +384,11 @@ remindersListID: 1, title: "Get milk" ), - [1]: CKRecord( + [2]: CKRecord( recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), recordType: "remindersLists", parent: nil, - share: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), id: 1, isCompleted: 0, title: "Personal" @@ -401,7 +417,6 @@ remindersListRecord.setValue(1, forKey: "id", at: now) remindersListRecord.setValue(false, forKey: "isCompleted", at: now) remindersListRecord.setValue("Personal", forKey: "title", at: now) - let share = CKShare( rootRecord: remindersListRecord, shareID: CKRecord.ID( @@ -509,9 +524,19 @@ ) modelARecord.setValue(1, forKey: "id", at: now) modelARecord.setValue(0, forKey: "count", at: now) + let share = CKShare( + rootRecord: modelARecord, + shareID: CKRecord.ID( + recordName: "share-\(modelARecord.recordID.recordName)", + zoneID: modelARecord.recordID.zoneID + ) + ) try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() - try await syncEngine.modifyRecords(scope: .shared, saving: [modelARecord]).notify() + try await syncEngine.modifyRecords( + scope: .shared, + saving: [modelARecord, share] + ).notify() try await withDependencies { $0.currentTime.now += 60 @@ -527,108 +552,113 @@ try await syncEngine.processPendingRecordZoneChanges(scope: .shared) assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) { """ - ┌─────────────────────────────────────────────────────────────────────────────────────────┐ - │ SyncMetadata( │ - │ id: SyncMetadata.ID( │ - │ recordPrimaryKey: "1", │ - │ recordType: "modelAs" │ - │ ), │ - │ zoneName: "external.zone", │ - │ ownerName: "external.owner", │ - │ recordName: "1:modelAs", │ - │ parentRecordID: nil, │ - │ parentRecordName: nil, │ - │ lastKnownServerRecord: CKRecord( │ - │ recordID: CKRecord.ID(1:modelAs/external.zone/external.owner), │ - │ recordType: "modelAs", │ - │ parent: nil, │ - │ share: nil │ - │ ), │ - │ _lastKnownServerRecordAllFields: CKRecord( │ - │ recordID: CKRecord.ID(1:modelAs/external.zone/external.owner), │ - │ recordType: "modelAs", │ - │ parent: nil, │ - │ share: nil, │ - │ count: 0, │ - │ id: 1 │ - │ ), │ - │ share: nil, │ - │ _isDeleted: false, │ - │ hasLastKnownServerRecord: true, │ - │ isShared: false, │ - │ userModificationTime: 0 │ - │ ) │ - ├─────────────────────────────────────────────────────────────────────────────────────────┤ - │ SyncMetadata( │ - │ id: SyncMetadata.ID( │ - │ recordPrimaryKey: "1", │ - │ recordType: "modelBs" │ - │ ), │ - │ zoneName: "external.zone", │ - │ ownerName: "external.owner", │ - │ recordName: "1:modelBs", │ - │ parentRecordID: SyncMetadata.ParentID( │ - │ parentRecordPrimaryKey: "1", │ - │ parentRecordType: "modelAs" │ - │ ), │ - │ parentRecordName: "1:modelAs", │ - │ lastKnownServerRecord: CKRecord( │ - │ recordID: CKRecord.ID(1:modelBs/external.zone/external.owner), │ - │ recordType: "modelBs", │ - │ parent: CKReference(recordID: CKRecord.ID(1:modelAs/external.zone/external.owner)), │ - │ share: nil │ - │ ), │ - │ _lastKnownServerRecordAllFields: CKRecord( │ - │ recordID: CKRecord.ID(1:modelBs/external.zone/external.owner), │ - │ recordType: "modelBs", │ - │ parent: CKReference(recordID: CKRecord.ID(1:modelAs/external.zone/external.owner)), │ - │ share: nil, │ - │ id: 1, │ - │ isOn: 0, │ - │ modelAID: 1 │ - │ ), │ - │ share: nil, │ - │ _isDeleted: false, │ - │ hasLastKnownServerRecord: true, │ - │ isShared: false, │ - │ userModificationTime: 60 │ - │ ) │ - ├─────────────────────────────────────────────────────────────────────────────────────────┤ - │ SyncMetadata( │ - │ id: SyncMetadata.ID( │ - │ recordPrimaryKey: "1", │ - │ recordType: "modelCs" │ - │ ), │ - │ zoneName: "external.zone", │ - │ ownerName: "external.owner", │ - │ recordName: "1:modelCs", │ - │ parentRecordID: SyncMetadata.ParentID( │ - │ parentRecordPrimaryKey: "1", │ - │ parentRecordType: "modelBs" │ - │ ), │ - │ parentRecordName: "1:modelBs", │ - │ lastKnownServerRecord: CKRecord( │ - │ recordID: CKRecord.ID(1:modelCs/external.zone/external.owner), │ - │ recordType: "modelCs", │ - │ parent: CKReference(recordID: CKRecord.ID(1:modelBs/external.zone/external.owner)), │ - │ share: nil │ - │ ), │ - │ _lastKnownServerRecordAllFields: CKRecord( │ - │ recordID: CKRecord.ID(1:modelCs/external.zone/external.owner), │ - │ recordType: "modelCs", │ - │ parent: CKReference(recordID: CKRecord.ID(1:modelBs/external.zone/external.owner)), │ - │ share: nil, │ - │ id: 1, │ - │ modelBID: 1, │ - │ title: "" │ - │ ), │ - │ share: nil, │ - │ _isDeleted: false, │ - │ hasLastKnownServerRecord: true, │ - │ isShared: false, │ - │ userModificationTime: 60 │ - │ ) │ - └─────────────────────────────────────────────────────────────────────────────────────────┘ + ┌──────────────────────────────────────────────────────────────────────────────────────────────┐ + │ SyncMetadata( │ + │ id: SyncMetadata.ID( │ + │ recordPrimaryKey: "1", │ + │ recordType: "modelAs" │ + │ ), │ + │ zoneName: "external.zone", │ + │ ownerName: "external.owner", │ + │ recordName: "1:modelAs", │ + │ parentRecordID: nil, │ + │ parentRecordName: nil, │ + │ lastKnownServerRecord: CKRecord( │ + │ recordID: CKRecord.ID(1:modelAs/external.zone/external.owner), │ + │ recordType: "modelAs", │ + │ parent: nil, │ + │ share: CKReference(recordID: CKRecord.ID(share-1:modelAs/external.zone/external.owner)) │ + │ ), │ + │ _lastKnownServerRecordAllFields: CKRecord( │ + │ recordID: CKRecord.ID(1:modelAs/external.zone/external.owner), │ + │ recordType: "modelAs", │ + │ parent: nil, │ + │ share: CKReference(recordID: CKRecord.ID(share-1:modelAs/external.zone/external.owner)), │ + │ count: 0, │ + │ id: 1 │ + │ ), │ + │ share: CKRecord( │ + │ recordID: CKRecord.ID(share-1:modelAs/external.zone/external.owner), │ + │ recordType: "cloudkit.share", │ + │ parent: nil, │ + │ share: nil │ + │ ), │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: true, │ + │ userModificationTime: 0 │ + │ ) │ + ├──────────────────────────────────────────────────────────────────────────────────────────────┤ + │ SyncMetadata( │ + │ id: SyncMetadata.ID( │ + │ recordPrimaryKey: "1", │ + │ recordType: "modelBs" │ + │ ), │ + │ zoneName: "external.zone", │ + │ ownerName: "external.owner", │ + │ recordName: "1:modelBs", │ + │ parentRecordID: SyncMetadata.ParentID( │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "modelAs" │ + │ ), │ + │ parentRecordName: "1:modelAs", │ + │ lastKnownServerRecord: CKRecord( │ + │ recordID: CKRecord.ID(1:modelBs/external.zone/external.owner), │ + │ recordType: "modelBs", │ + │ parent: CKReference(recordID: CKRecord.ID(1:modelAs/external.zone/external.owner)), │ + │ share: nil │ + │ ), │ + │ _lastKnownServerRecordAllFields: CKRecord( │ + │ recordID: CKRecord.ID(1:modelBs/external.zone/external.owner), │ + │ recordType: "modelBs", │ + │ parent: CKReference(recordID: CKRecord.ID(1:modelAs/external.zone/external.owner)), │ + │ share: nil, │ + │ id: 1, │ + │ isOn: 0, │ + │ modelAID: 1 │ + │ ), │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: false, │ + │ userModificationTime: 60 │ + │ ) │ + ├──────────────────────────────────────────────────────────────────────────────────────────────┤ + │ SyncMetadata( │ + │ id: SyncMetadata.ID( │ + │ recordPrimaryKey: "1", │ + │ recordType: "modelCs" │ + │ ), │ + │ zoneName: "external.zone", │ + │ ownerName: "external.owner", │ + │ recordName: "1:modelCs", │ + │ parentRecordID: SyncMetadata.ParentID( │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "modelBs" │ + │ ), │ + │ parentRecordName: "1:modelBs", │ + │ lastKnownServerRecord: CKRecord( │ + │ recordID: CKRecord.ID(1:modelCs/external.zone/external.owner), │ + │ recordType: "modelCs", │ + │ parent: CKReference(recordID: CKRecord.ID(1:modelBs/external.zone/external.owner)), │ + │ share: nil │ + │ ), │ + │ _lastKnownServerRecordAllFields: CKRecord( │ + │ recordID: CKRecord.ID(1:modelCs/external.zone/external.owner), │ + │ recordType: "modelCs", │ + │ parent: CKReference(recordID: CKRecord.ID(1:modelBs/external.zone/external.owner)), │ + │ share: nil, │ + │ id: 1, │ + │ modelBID: 1, │ + │ title: "" │ + │ ), │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: false, │ + │ userModificationTime: 60 │ + │ ) │ + └──────────────────────────────────────────────────────────────────────────────────────────────┘ """ } assertInlineSnapshot(of: container, as: .customDump) { @@ -642,14 +672,20 @@ databaseScope: .shared, storage: [ [0]: CKRecord( + recordID: CKRecord.ID(share-1:modelAs/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( recordID: CKRecord.ID(1:modelAs/external.zone/external.owner), recordType: "modelAs", parent: nil, - share: nil, + share: CKReference(recordID: CKRecord.ID(share-1:modelAs/external.zone/external.owner)), count: 0, id: 1 ), - [1]: CKRecord( + [2]: CKRecord( recordID: CKRecord.ID(1:modelBs/external.zone/external.owner), recordType: "modelBs", parent: CKReference(recordID: CKRecord.ID(1:modelAs/external.zone/external.owner)), @@ -658,7 +694,7 @@ isOn: 0, modelAID: 1 ), - [2]: CKRecord( + [3]: CKRecord( recordID: CKRecord.ID(1:modelCs/external.zone/external.owner), recordType: "modelCs", parent: CKReference(recordID: CKRecord.ID(1:modelBs/external.zone/external.owner)), From ec6834883f5a273f1d9d9837043d8cfed10592e1 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 26 Nov 2025 19:44:00 -0600 Subject: [PATCH 05/10] fix test --- .../SyncEngineLifecycleTests.swift | 137 ++++++++++-------- 1 file changed, 79 insertions(+), 58 deletions(-) diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift index 19dc7399..3f82851f 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift @@ -263,9 +263,19 @@ remindersListRecord.setValue(1, forKey: "id", at: now) remindersListRecord.setValue(false, forKey: "isCompleted", at: now) remindersListRecord.setValue("Personal", forKey: "title", at: now) + let share = CKShare( + rootRecord: remindersListRecord, + shareID: CKRecord.ID( + recordName: "share-\(remindersListRecord.recordID.recordName)", + zoneID: remindersListRecord.recordID.zoneID + ) + ) try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify() - try await syncEngine.modifyRecords(scope: .shared, saving: [remindersListRecord]).notify() + try await syncEngine.modifyRecords( + scope: .shared, + saving: [remindersListRecord, share] + ).notify() syncEngine.stop() @@ -282,61 +292,66 @@ try await Task.sleep(for: .seconds(1)) assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) { """ - ┌───────────────────────────────────────────────────────────────────────────┐ - │ SyncMetadata( │ - │ id: SyncMetadata.ID( │ - │ recordPrimaryKey: "1", │ - │ recordType: "remindersLists" │ - │ ), │ - │ zoneName: "external.zone", │ - │ ownerName: "external.owner", │ - │ recordName: "1:remindersLists", │ - │ parentRecordID: nil, │ - │ parentRecordName: nil, │ - │ lastKnownServerRecord: CKRecord( │ - │ recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), │ - │ recordType: "remindersLists", │ - │ parent: nil, │ - │ share: nil │ - │ ), │ - │ _lastKnownServerRecordAllFields: CKRecord( │ - │ recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), │ - │ recordType: "remindersLists", │ - │ parent: nil, │ - │ share: nil, │ - │ id: 1, │ - │ isCompleted: 0, │ - │ title: "Personal" │ - │ ), │ - │ share: nil, │ - │ _isDeleted: false, │ - │ hasLastKnownServerRecord: true, │ - │ isShared: false, │ - │ userModificationTime: 0 │ - │ ) │ - ├───────────────────────────────────────────────────────────────────────────┤ - │ SyncMetadata( │ - │ id: SyncMetadata.ID( │ - │ recordPrimaryKey: "1", │ - │ recordType: "reminders" │ - │ ), │ - │ zoneName: "external.zone", │ - │ ownerName: "external.owner", │ - │ recordName: "1:reminders", │ - │ parentRecordID: SyncMetadata.ParentID( │ - │ parentRecordPrimaryKey: "1", │ - │ parentRecordType: "remindersLists" │ - │ ), │ - │ parentRecordName: "1:remindersLists", │ - │ lastKnownServerRecord: nil, │ - │ _lastKnownServerRecordAllFields: nil, │ - │ share: nil, │ - │ _isDeleted: false, │ - │ hasLastKnownServerRecord: false, │ - │ isShared: false, │ - │ userModificationTime: 60 │ - │ ) │ - └───────────────────────────────────────────────────────────────────────────┘ + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ SyncMetadata( │ + │ id: SyncMetadata.ID( │ + │ recordPrimaryKey: "1", │ + │ recordType: "remindersLists" │ + │ ), │ + │ zoneName: "external.zone", │ + │ ownerName: "external.owner", │ + │ recordName: "1:remindersLists", │ + │ parentRecordID: nil, │ + │ parentRecordName: nil, │ + │ lastKnownServerRecord: CKRecord( │ + │ recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), │ + │ recordType: "remindersLists", │ + │ parent: nil, │ + │ share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)) │ + │ ), │ + │ _lastKnownServerRecordAllFields: CKRecord( │ + │ recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), │ + │ recordType: "remindersLists", │ + │ parent: nil, │ + │ share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), │ + │ id: 1, │ + │ isCompleted: 0, │ + │ title: "Personal" │ + │ ), │ + │ share: CKRecord( │ + │ recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), │ + │ recordType: "cloudkit.share", │ + │ parent: nil, │ + │ share: nil │ + │ ), │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: true, │ + │ userModificationTime: 0 │ + │ ) │ + ├─────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ SyncMetadata( │ + │ id: SyncMetadata.ID( │ + │ recordPrimaryKey: "1", │ + │ recordType: "reminders" │ + │ ), │ + │ zoneName: "external.zone", │ + │ ownerName: "external.owner", │ + │ recordName: "1:reminders", │ + │ parentRecordID: SyncMetadata.ParentID( │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "remindersLists" │ + │ ), │ + │ parentRecordName: "1:remindersLists", │ + │ lastKnownServerRecord: nil, │ + │ _lastKnownServerRecordAllFields: nil, │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: false, │ + │ isShared: false, │ + │ userModificationTime: 60 │ + │ ) │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ """ } @@ -355,6 +370,12 @@ databaseScope: .shared, storage: [ [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( recordID: CKRecord.ID(1:reminders/external.zone/external.owner), recordType: "reminders", parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), @@ -364,11 +385,11 @@ remindersListID: 1, title: "Get milk" ), - [1]: CKRecord( + [2]: CKRecord( recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), recordType: "remindersLists", parent: nil, - share: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), id: 1, isCompleted: 0, title: "Personal" From 52bcdbf9c275ddc049b734353985a87f61ae565e Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Wed, 26 Nov 2025 19:58:56 -0600 Subject: [PATCH 06/10] add another test --- .../MockCloudDatabaseTests.swift | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift index d61b6a9f..d11cad5f 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift @@ -433,6 +433,85 @@ } #expect(error?.code == .permissionFailure) } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deletingShareDeletesSharedRecords() async throws { + let externalZone = CKRecordZone( + zoneID: CKRecordZone.ID(zoneName: "external.zone", ownerName: "external.owner") + ) + _ = try syncEngine.shared.database.modifyRecordZones(saving: [externalZone]) + + let recordA = CKRecord( + recordType: "A", + recordID: CKRecord.ID(recordName: "A1", zoneID: externalZone.zoneID) + ) + let recordB = CKRecord( + recordType: "B", + recordID: CKRecord.ID(recordName: "B1", zoneID: externalZone.zoneID) + ) + recordB.parent = CKRecord.Reference(recordID: recordA.recordID, action: .none) + let share = CKShare( + rootRecord: recordA, + shareID: CKRecord.ID(recordName: "share", zoneID: externalZone.zoneID + ) + ) + let (saveResults, _) = try syncEngine.shared.database.modifyRecords( + saving: [share, recordA, recordB] + ) + + print(saveResults) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(A1/external.zone/external.owner), + recordType: "A", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share/external.zone/external.owner)) + ), + [1]: CKRecord( + recordID: CKRecord.ID(B1/external.zone/external.owner), + recordType: "B", + parent: CKReference(recordID: CKRecord.ID(A1/external.zone/external.owner)), + share: nil + ), + [2]: CKRecord( + recordID: CKRecord.ID(share/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ) + ] + ) + ) + """ + } + + _ = try syncEngine.shared.database.modifyRecords(deleting: [share.recordID]) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } } } #endif From bfe0e8a97f8b8e55b78b62c427e286ee0d643c1f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Thu, 4 Dec 2025 14:56:59 -0800 Subject: [PATCH 07/10] Update Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift --- .../SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift index d11cad5f..8f72828d 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift @@ -459,8 +459,6 @@ saving: [share, recordA, recordB] ) - print(saveResults) - assertInlineSnapshot(of: container, as: .customDump) { """ MockCloudContainer( From 050862c750ace634de22c38b4d344b42a31adf1a Mon Sep 17 00:00:00 2001 From: Robbie Clarken Date: Sat, 6 Dec 2025 02:19:01 +1030 Subject: [PATCH 08/10] Fix id column type in documentation example schema to prevent error (#322) When the `id` column is defined as `INT NOT NULL PRIMARY KEY AUTOINCREMENT` sqlite will throw an error: > AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY --- .../Documentation.docc/Articles/PreparingDatabase.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md index b9c3404d..96e9bf86 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md @@ -170,7 +170,7 @@ code, but we personally feel that it is simpler, more flexible and more powerful migrator.registerMigration("Create tables") { db in try #sql(""" CREATE TABLE "remindersLists"( - "id" INT NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "title" TEXT NOT NULL ) STRICT """) @@ -178,10 +178,10 @@ migrator.registerMigration("Create tables") { db in try #sql(""" CREATE TABLE "reminders"( - "id" INT NOT NULL PRIMARY KEY AUTOINCREMENT, - "isCompleted" INT NOT NULL DEFAULT 0, + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "isCompleted" INTEGER NOT NULL DEFAULT 0, "title" TEXT NOT NULL, - "remindersListID" INT NOT NULL REFERENCES "remindersLists"("id") ON DELETE CASCADE + "remindersListID" INTEGER NOT NULL REFERENCES "remindersLists"("id") ON DELETE CASCADE ) STRICT """) .execute(db) From 59db793b04dbb9f114902dbabbdccb0944139646 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 5 Dec 2025 10:48:50 -0600 Subject: [PATCH 09/10] fixes --- .../CloudKit/Internal/MockCloudDatabase.swift | 9 +- .../MockCloudDatabaseTests.swift | 109 ++++++++++++++- .../CloudKitTests/SharingTests.swift | 124 +++++++++++++++++- .../SyncEngineLifecycleTests.swift | 46 ++++++- 4 files changed, 277 insertions(+), 11 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift index 67d1d4bc..670a1003 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift @@ -245,8 +245,13 @@ storage[recordIDToDelete.zoneID]?[recordIDToDelete] = nil deleteResults[recordIDToDelete] = .success(()) - // NB: If deleting a share, delete the shared records and all associated records. - if databaseScope == .shared, let shareToDelete = recordToDelete as? CKShare { + // NB: If deleting a share that the current user owns, delete the shared records and all + // associated records. + if + databaseScope == .shared, + let shareToDelete = recordToDelete as? CKShare, + shareToDelete.recordID.zoneID.ownerName == CKCurrentUserDefaultName + { func deleteRecords(referencing recordID: CKRecord.ID) { for recordToDelete in (storage[recordIDToDelete.zoneID] ?? [:]).values { guard diff --git a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift index 8f72828d..ac6db0e9 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MockCloudDatabaseTests.swift @@ -435,7 +435,92 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - @Test func deletingShareDeletesSharedRecords() async throws { + @Test func deletingShareOwnedByCurrentUserDeletesShareAndDoesNotDeleteAssociatedData() async throws { + let zone = syncEngine.defaultZone + _ = try syncEngine.private.database.modifyRecordZones(saving: [zone]) + + let recordA = CKRecord( + recordType: "A", + recordID: CKRecord.ID(recordName: "A1", zoneID: zone.zoneID) + ) + let recordB = CKRecord( + recordType: "B", + recordID: CKRecord.ID(recordName: "B1", zoneID: zone.zoneID) + ) + recordB.parent = CKRecord.Reference(recordID: recordA.recordID, action: .none) + let share = CKShare( + rootRecord: recordA, + shareID: CKRecord.ID(recordName: "share", zoneID: zone.zoneID) + ) + _ = try syncEngine.private.database.modifyRecords(saving: [share, recordA, recordB]) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(A1/zone/__defaultOwner__), + recordType: "A", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share/zone/__defaultOwner__)) + ), + [1]: CKRecord( + recordID: CKRecord.ID(B1/zone/__defaultOwner__), + recordType: "B", + parent: CKReference(recordID: CKRecord.ID(A1/zone/__defaultOwner__)), + share: nil + ), + [2]: CKRecord( + recordID: CKRecord.ID(share/zone/__defaultOwner__), + recordType: "cloudkit.share", + parent: nil, + share: nil + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + + _ = try syncEngine.private.database.modifyRecords(deleting: [share.recordID]) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(A1/zone/__defaultOwner__), + recordType: "A", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share/zone/__defaultOwner__)) + ), + [1]: CKRecord( + recordID: CKRecord.ID(B1/zone/__defaultOwner__), + recordType: "B", + parent: CKReference(recordID: CKRecord.ID(A1/zone/__defaultOwner__)), + share: nil + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func deletingShareNotOwnedByCurrentUserDeletesOnlyShareAndNotAssociatedRecords() async throws { let externalZone = CKRecordZone( zoneID: CKRecordZone.ID(zoneName: "external.zone", ownerName: "external.owner") ) @@ -452,12 +537,9 @@ recordB.parent = CKRecord.Reference(recordID: recordA.recordID, action: .none) let share = CKShare( rootRecord: recordA, - shareID: CKRecord.ID(recordName: "share", zoneID: externalZone.zoneID - ) - ) - let (saveResults, _) = try syncEngine.shared.database.modifyRecords( - saving: [share, recordA, recordB] + shareID: CKRecord.ID(recordName: "share", zoneID: externalZone.zoneID) ) + _ = try syncEngine.shared.database.modifyRecords(saving: [share, recordA, recordB]) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -504,7 +586,20 @@ ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, - storage: [] + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(A1/external.zone/external.owner), + recordType: "A", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share/external.zone/external.owner)) + ), + [1]: CKRecord( + recordID: CKRecord.ID(B1/external.zone/external.owner), + recordType: "B", + parent: CKReference(recordID: CKRecord.ID(A1/external.zone/external.owner)), + share: nil + ) + ] ) ) """ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index b5f96494..a02260f0 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -1249,18 +1249,61 @@ } } + // Deleting a root shared record while the owner of that record deletes the associated CKShare + // as well as any other associated records. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func deleteRootSharedRecord_CurrentUserOwnsRecord() async throws { let remindersList = RemindersList(id: 1, title: "Personal") try await userDatabase.userWrite { db in try db.seed { remindersList + Reminder(id: 1, remindersListID: 1) } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) let _ = try await syncEngine.share(record: remindersList, configure: { _ in }) + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/zone/__defaultOwner__), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:reminders/zone/__defaultOwner__), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), + share: nil, + id: 1, + isCompleted: 0, + remindersListID: 1, + title: "" + ), + [2]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/zone/__defaultOwner__)), + id: 1, + title: "Personal" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + try await userDatabase.userWrite { db in try RemindersList.find(1).delete().execute(db) } @@ -1331,6 +1374,56 @@ try await syncEngine.processPendingRecordZoneChanges(scope: .shared) + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:reminders/external.zone/external.owner), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), + share: nil, + id: 1, + isCompleted: 0, + remindersListID: 1, + title: "Get milk" + ), + [2]: CKRecord( + recordID: CKRecord.ID(2:reminders/external.zone/external.owner), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), + share: nil, + id: 2, + isCompleted: 0, + remindersListID: 1, + title: "Take a walk" + ), + [3]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + title: "Personal" + ) + ] + ) + ) + """ + } + try await userDatabase.userWrite { db in try RemindersList.find(1).delete().execute(db) } @@ -1362,7 +1455,36 @@ ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, - storage: [] + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:reminders/external.zone/external.owner), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), + share: nil, + id: 1, + isCompleted: 0, + remindersListID: 1, + title: "Get milk" + ), + [1]: CKRecord( + recordID: CKRecord.ID(2:reminders/external.zone/external.owner), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner)), + share: nil, + id: 2, + isCompleted: 0, + remindersListID: 1, + title: "Take a walk" + ), + [2]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + title: "Personal" + ) + ] ) ) """ diff --git a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift index 3f82851f..6052e15d 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SyncEngineLifecycleTests.swift @@ -401,6 +401,9 @@ } } + // Deleting a root shared record that we do not own while the sync engine is off will + // probably sync (delete share on iCloud but does not delete any records) once the sync + // engine is turned back on. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func externalSharedRecord_StopSyncEngine_DeleteSharedRecord_StartSyncEngine() async throws @@ -432,6 +435,37 @@ saving: [remindersListRecord, share] ).notify() + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner), + recordType: "cloudkit.share", + parent: nil, + share: nil + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + isCompleted: 0, + title: "Personal" + ) + ] + ) + ) + """ + } + syncEngine.stop() try await userDatabase.userWrite { db in @@ -451,7 +485,17 @@ ), sharedCloudDatabase: MockCloudDatabase( databaseScope: .shared, - storage: [] + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner), + recordType: "remindersLists", + parent: nil, + share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)), + id: 1, + isCompleted: 0, + title: "Personal" + ) + ] ) ) """ From 7b77387c4df63665534887600615058a13734aaf Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 5 Dec 2025 10:50:58 -0600 Subject: [PATCH 10/10] format --- .../CloudKit/Internal/MockCloudDatabase.swift | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift index 670a1003..a3a12db5 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift @@ -97,15 +97,14 @@ saveResults[recordToSave.recordID] = .failure(CKError(.invalidArguments)) continue } - } else { - // NB: Emit 'permissionFailure' if saving to shared database with no parent reference. - if databaseScope == .shared, - recordToSave.parent == nil, - recordToSave.share == nil - { - saveResults[recordToSave.recordID] = .failure(CKError(.permissionFailure)) - continue - } + } else if databaseScope == .shared, + recordToSave.parent == nil, + recordToSave.share == nil + { + // NB: Emit 'permissionFailure' if saving to shared database with no parent reference + // or share reference. + saveResults[recordToSave.recordID] = .failure(CKError(.permissionFailure)) + continue } // NB: Emit 'zoneNotFound' error if saving record with a zone not found in database. @@ -247,8 +246,7 @@ // NB: If deleting a share that the current user owns, delete the shared records and all // associated records. - if - databaseScope == .shared, + if databaseScope == .shared, let shareToDelete = recordToDelete as? CKShare, shareToDelete.recordID.zoneID.ownerName == CKCurrentUserDefaultName {