diff --git a/Sources/SQLiteData/CloudKit/CloudKitSharing.swift b/Sources/SQLiteData/CloudKit/CloudKitSharing.swift index 09f4181b..7254ab88 100644 --- a/Sources/SQLiteData/CloudKit/CloudKitSharing.swift +++ b/Sources/SQLiteData/CloudKit/CloudKitSharing.swift @@ -118,24 +118,27 @@ ) } let recordName = record.recordName - let lastKnownServerRecord = - try await metadatabase.read { db in + let lastKnownServerRecord = try await { + let lastKnownServerRecord = try await metadatabase.read { db in try SyncMetadata .where { $0.recordName.eq(recordName) } .select(\._lastKnownServerRecordAllFields) .fetchOne(db) } ?? nil - guard let lastKnownServerRecord - else { - throw SharingError( - recordTableName: T.tableName, - recordPrimaryKey: record.primaryKey.rawIdentifier, - reason: .recordMetadataNotFound, - debugDescription: """ + guard let lastKnownServerRecord + else { + throw SharingError( + recordTableName: T.tableName, + recordPrimaryKey: record.primaryKey.rawIdentifier, + reason: .recordMetadataNotFound, + debugDescription: """ No sync metadata found for record. Has the record been saved to the database? """ - ) - } + ) + } + return try await container.database(for: lastKnownServerRecord.recordID) + .record(for: lastKnownServerRecord.recordID) + }() var existingShare: CKShare? { get async throws { diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift index 21c3c8a0..0c2fa6a0 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift @@ -189,6 +189,18 @@ // TODO: This should merge copy's values to more accurately reflect reality storage[recordToSave.recordID.zoneID]?.records[recordToSave.recordID] = copy saveResults[recordToSave.recordID] = .success(copy) + + // NB: "Touch" parent records when saving a child: + if let parent = recordToSave.parent, + // If the parent isn't also being saved in this batch. + !recordsToSave.contains(where: { $0.recordID == parent.recordID }), + // And if the parent is in the database. + let parentRecord = storage[parent.recordID.zoneID]?.records[parent.recordID]?.copy() + as? CKRecord + { + parentRecord._recordChangeTag = UUID().uuidString + storage[parent.recordID.zoneID]?.records[parent.recordID] = parentRecord + } } switch (existingRecord, recordToSave._recordChangeTag) { diff --git a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift index f74d730f..225f544b 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift @@ -847,6 +847,87 @@ } } + /* + * Create parent record and synchronize. + * Create child record and synchronize. + * Share parent record. + */ + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.bug("https://github.com/pointfreeco/sqlite-data/pull/363")) + func createParentThenChildThenShare() async throws { + let remindersList = RemindersList(id: 1, title: "Personal") + try await userDatabase.userWrite { db in + try db.seed { remindersList } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + let reminder = Reminder(id: 1, title: "Groceries", remindersListID: 1) + try await userDatabase.userWrite { db in + try db.seed { reminder } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + let _ = try await syncEngine.share(record: remindersList, configure: { _ in }) + + assertQuery( + SyncMetadata.select { ($0.share, $0.userModificationTime) }, + database: syncEngine.metadatabase + ) { + """ + ┌────────────────────────────────────────────────────────────────────────┬───┐ + │ CKRecord( │ 0 │ + │ recordID: CKRecord.ID(share-1:remindersLists/zone/__defaultOwner__), │ │ + │ recordType: "cloudkit.share", │ │ + │ parent: nil, │ │ + │ share: nil │ │ + │ ) │ │ + ├────────────────────────────────────────────────────────────────────────┼───┤ + │ nil │ 0 │ + └────────────────────────────────────────────────────────────────────────┴───┘ + """ + } + + 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: "Groceries" + ), + [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: [] + ) + ) + """ + } + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func shareTwice() async throws { let remindersList = RemindersList(id: 1, title: "Personal")