From 646a9cde55ea8147c4b4bbb2135d1ca7614c10fc Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 16 Sep 2025 14:43:51 -0700 Subject: [PATCH 1/3] wip --- Package.resolved | 11 +- Sources/SQLiteData/CloudKit/SyncEngine.swift | 22 +- .../CloudKitTests/SharingTests.swift | 202 ++++++++++++++++++ 3 files changed, 224 insertions(+), 11 deletions(-) diff --git a/Package.resolved b/Package.resolved index d73ca91b..19053c54 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "32f70dab99ea92a018a46453e9ed1e8bbf1e5d17b2993ba8abc63cae49896bbc", + "originHash" : "32a0b9db128d9e5f29bd4495238ec304eafe64f2e75d45e792189c983fbdc49c", "pins" : [ { "identity" : "combine-schedulers", @@ -136,6 +136,15 @@ "version" : "601.0.1" } }, + { + "identity" : "swift-tagged", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-tagged", + "state" : { + "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", + "version" : "0.10.0" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index cc101c2a..aa8d504e 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1138,8 +1138,6 @@ try await userDatabase.write { db in var defaultZoneDeleted = false for (zoneID, reason) in deletions { - guard zoneID == self.defaultZone.zoneID - else { continue } switch reason { case .deleted, .purged: try deleteRecords(in: zoneID, db: db) @@ -1159,19 +1157,23 @@ } @Sendable func deleteRecords(in zoneID: CKRecordZone.ID, db: Database) throws { - let recordTypes = Set( - try SyncMetadata - .where(\.hasLastKnownServerRecord) - .select(\.lastKnownServerRecord) - .fetchAll(db) - .compactMap { $0?.recordID.zoneID == zoneID ? $0?.recordType : nil } + let recordTypes = Dictionary( + grouping: + try SyncMetadata + .where { $0.zoneName.eq(zoneID.zoneName) && $0.ownerName.eq(zoneID.ownerName) } + .select { ($0.recordType, $0.recordPrimaryKey) } + .fetchAll(db), + by: \.0 ) - for recordType in recordTypes { + .mapValues { + $0.map(\.1) + } + for (recordType, primaryKeys) in recordTypes { guard let table = tablesByName[recordType] else { continue } func open(_: T.Type) { withErrorReporting(.sqliteDataCloudKitFailure) { - try T.delete().execute(db) + try T.where { #sql("\($0.primaryKey)").in(primaryKeys) }.delete().execute(db) } } open(table) diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index 9b4c2f54..f878165e 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -1085,6 +1085,208 @@ } } + /// Syncing deletion of a root shared record that is not owned by current user should delete + /// entire zone. + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func syncDeletedRootSharedRecord_CurrentUserNotOwner() 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 await syncEngine + .acceptShare( + metadata: ShareMetadata( + containerIdentifier: container.containerIdentifier!, + hierarchicalRootRecordID: remindersListRecord.recordID, + rootRecord: remindersListRecord, + share: share + ) + ) + + try await userDatabase.userWrite { db in + try db.seed { + Reminder(id: 1, title: "Get milk", remindersListID: 1) + Reminder(id: 2, title: "Take a walk", remindersListID: 1) + } + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .shared) + + try await syncEngine.modifyRecordZones(scope: .shared, deleting: [externalZone.zoneID]) + .notify() + + assertQuery(Reminder.all, database: userDatabase.database) { + """ + ┌─────────────────────────┐ + │ Reminder( │ + │ id: 1, │ + │ dueDate: nil, │ + │ isCompleted: false, │ + │ priority: nil, │ + │ title: "Get milk", │ + │ remindersListID: 1 │ + │ ) │ + ├─────────────────────────┤ + │ Reminder( │ + │ id: 2, │ + │ dueDate: nil, │ + │ isCompleted: false, │ + │ priority: nil, │ + │ title: "Take a walk", │ + │ remindersListID: 1 │ + │ ) │ + └─────────────────────────┘ + """ + } + assertQuery(RemindersList.all, database: userDatabase.database) { + """ + ┌─────────────────────┐ + │ RemindersList( │ + │ id: 1, │ + │ title: "Personal" │ + │ ) │ + └─────────────────────┘ + """ + } + assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) { + """ + ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ SyncMetadata( │ + │ recordPrimaryKey: "1", │ + │ recordType: "remindersLists", │ + │ zoneName: "external.zone", │ + │ ownerName: "external.owner", │ + │ recordName: "1:remindersLists", │ + │ parentRecordPrimaryKey: nil, │ + │ parentRecordType: 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, │ + │ 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( │ + │ recordPrimaryKey: "1", │ + │ recordType: "reminders", │ + │ zoneName: "external.zone", │ + │ ownerName: "external.owner", │ + │ recordName: "1:reminders", │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "remindersLists", │ + │ parentRecordName: "1:remindersLists", │ + │ lastKnownServerRecord: 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 │ + │ ), │ + │ _lastKnownServerRecordAllFields: 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" │ + │ ), │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: false, │ + │ userModificationTime: 0 │ + │ ) │ + ├─────────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ SyncMetadata( │ + │ recordPrimaryKey: "2", │ + │ recordType: "reminders", │ + │ zoneName: "external.zone", │ + │ ownerName: "external.owner", │ + │ recordName: "2:reminders", │ + │ parentRecordPrimaryKey: "1", │ + │ parentRecordType: "remindersLists", │ + │ parentRecordName: "1:remindersLists", │ + │ lastKnownServerRecord: 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 │ + │ ), │ + │ _lastKnownServerRecordAllFields: 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" │ + │ ), │ + │ share: nil, │ + │ _isDeleted: false, │ + │ hasLastKnownServerRecord: true, │ + │ isShared: false, │ + │ userModificationTime: 0 │ + │ ) │ + └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ + """ + } + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + // NB: Come back to this when we have time to investigate. // /// Deleting a root shared record that is not owned by current user should only delete // /// the CKShare, not delete the actual CloudKit records, but delete all the local records. From 04e25157d4e9b3039ccabc6b2cecf799d3f44c15 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 16 Sep 2025 14:50:00 -0700 Subject: [PATCH 2/3] wip --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 4 +- .../CloudKitTests/SharingTests.swift | 140 +----------------- 2 files changed, 6 insertions(+), 138 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index aa8d504e..4f9a5d29 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -1141,7 +1141,9 @@ switch reason { case .deleted, .purged: try deleteRecords(in: zoneID, db: db) - defaultZoneDeleted = true + if zoneID == self.defaultZone.zoneID { + defaultZoneDeleted = true + } case .encryptedDataReset: try uploadRecords(in: zoneID, db: db) @unknown default: diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index f878165e..7c9646e9 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -1133,143 +1133,9 @@ try await syncEngine.modifyRecordZones(scope: .shared, deleting: [externalZone.zoneID]) .notify() - assertQuery(Reminder.all, database: userDatabase.database) { - """ - ┌─────────────────────────┐ - │ Reminder( │ - │ id: 1, │ - │ dueDate: nil, │ - │ isCompleted: false, │ - │ priority: nil, │ - │ title: "Get milk", │ - │ remindersListID: 1 │ - │ ) │ - ├─────────────────────────┤ - │ Reminder( │ - │ id: 2, │ - │ dueDate: nil, │ - │ isCompleted: false, │ - │ priority: nil, │ - │ title: "Take a walk", │ - │ remindersListID: 1 │ - │ ) │ - └─────────────────────────┘ - """ - } - assertQuery(RemindersList.all, database: userDatabase.database) { - """ - ┌─────────────────────┐ - │ RemindersList( │ - │ id: 1, │ - │ title: "Personal" │ - │ ) │ - └─────────────────────┘ - """ - } - assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) { - """ - ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ SyncMetadata( │ - │ recordPrimaryKey: "1", │ - │ recordType: "remindersLists", │ - │ zoneName: "external.zone", │ - │ ownerName: "external.owner", │ - │ recordName: "1:remindersLists", │ - │ parentRecordPrimaryKey: nil, │ - │ parentRecordType: 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, │ - │ 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( │ - │ recordPrimaryKey: "1", │ - │ recordType: "reminders", │ - │ zoneName: "external.zone", │ - │ ownerName: "external.owner", │ - │ recordName: "1:reminders", │ - │ parentRecordPrimaryKey: "1", │ - │ parentRecordType: "remindersLists", │ - │ parentRecordName: "1:remindersLists", │ - │ lastKnownServerRecord: 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 │ - │ ), │ - │ _lastKnownServerRecordAllFields: 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" │ - │ ), │ - │ share: nil, │ - │ _isDeleted: false, │ - │ hasLastKnownServerRecord: true, │ - │ isShared: false, │ - │ userModificationTime: 0 │ - │ ) │ - ├─────────────────────────────────────────────────────────────────────────────────────────────────────┤ - │ SyncMetadata( │ - │ recordPrimaryKey: "2", │ - │ recordType: "reminders", │ - │ zoneName: "external.zone", │ - │ ownerName: "external.owner", │ - │ recordName: "2:reminders", │ - │ parentRecordPrimaryKey: "1", │ - │ parentRecordType: "remindersLists", │ - │ parentRecordName: "1:remindersLists", │ - │ lastKnownServerRecord: 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 │ - │ ), │ - │ _lastKnownServerRecordAllFields: 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" │ - │ ), │ - │ share: nil, │ - │ _isDeleted: false, │ - │ hasLastKnownServerRecord: true, │ - │ isShared: false, │ - │ userModificationTime: 0 │ - │ ) │ - └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ - """ - } + assertQuery(Reminder.all, database: userDatabase.database) + assertQuery(RemindersList.all, database: userDatabase.database) + assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) assertInlineSnapshot(of: container, as: .customDump) { """ From 3cd58d77fd77dd49b3e8802171b984eeea76a71c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 16 Sep 2025 14:59:00 -0700 Subject: [PATCH 3/3] wip --- Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift index d7198cb2..cba0d65d 100644 --- a/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift @@ -75,6 +75,12 @@ """ ) .execute(db) + try #sql( + """ + CREATE INDEX "\(raw: .sqliteDataCloudKitSchemaName)_metadata_zoneID" + ON "\(raw: .sqliteDataCloudKitSchemaName)_metadata"("ownerName", "zoneName") + """ + ) try #sql( """ CREATE INDEX "\(raw: .sqliteDataCloudKitSchemaName)_metadata_parentRecordName"