diff --git a/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift b/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift index 7533a371..a6b216da 100644 --- a/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift +++ b/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift @@ -28,6 +28,7 @@ queryOutput.encodeSystemFields(with: archiver) if isTesting { archiver.encode(queryOutput._recordChangeTag, forKey: "_recordChangeTag") + archiver.encode(queryOutput._modificationDate, forKey: "_modificationDate") if queryOutput is CKShare { archiver.encode(queryOutput[CKShare.SystemFieldKey.title], forKey: CKShare.SystemFieldKey.title) } @@ -58,6 +59,9 @@ queryOutput._recordChangeTag = coder .decodeObject(of: NSNumber.self, forKey: "_recordChangeTag")?.intValue + queryOutput._modificationDate = + coder + .decodeObject(of: NSDate.self, forKey: "_modificationDate") as Date? if queryOutput is CKShare { queryOutput[CKShare.SystemFieldKey.title] = coder .decodeObject(of: NSString.self, forKey: CKShare.SystemFieldKey.title) @@ -77,6 +81,10 @@ queryOutput.encode(with: archiver) if isTesting { archiver.encode(queryOutput._recordChangeTag, forKey: "_recordChangeTag") + archiver.encode(queryOutput._modificationDate, forKey: "_modificationDate") + if queryOutput is CKShare { + archiver.encode(queryOutput[CKShare.SystemFieldKey.title], forKey: CKShare.SystemFieldKey.title) + } } return archiver.encodedData.queryBinding } @@ -104,6 +112,13 @@ queryOutput._recordChangeTag = coder .decodeObject(of: NSNumber.self, forKey: "_recordChangeTag")?.intValue + queryOutput._modificationDate = + coder + .decodeObject(of: NSDate.self, forKey: "_modificationDate") as Date? + if queryOutput is CKShare { + queryOutput[CKShare.SystemFieldKey.title] = coder + .decodeObject(of: NSString.self, forKey: CKShare.SystemFieldKey.title) + } } self.init(queryOutput: queryOutput) } @@ -386,6 +401,11 @@ get { self[#function] } set { self[#function] = newValue } } + + package var _modificationDate: Date? { + get { modificationDate ?? self[#function] } + set { self[#function] = newValue } + } } extension DataProtocol { diff --git a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift index d2b9259c..cd350e8e 100644 --- a/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift +++ b/Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift @@ -10,6 +10,7 @@ package let databaseScope: CKDatabase.Scope let _container = IsolatedWeakVar() let dataManager = Dependency(\.dataManager) + let now = Dependency(\.currentTime.now) let uuid = Dependency(\.uuid) let clock: AnyClock @@ -224,6 +225,9 @@ guard let databaseCopy = recordToSave.copy() as? CKRecord else { fatalError("Could not copy CKRecord.") } + databaseCopy._modificationDate = Date( + timeIntervalSinceReferenceDate: Double(now.wrappedValue) + ) databaseCopy._recordChangeTag = state.nextRecordChangeTag() for key in databaseCopy.allKeys() { @@ -249,6 +253,9 @@ as? CKRecord { parentRecord._recordChangeTag = state.nextRecordChangeTag() + parentRecord._modificationDate = Date( + timeIntervalSinceReferenceDate: Double(now.wrappedValue) + ) state.storage[parent.recordID.zoneID]?.records[parent.recordID] = parentRecord } } diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 39e81739..930843d2 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -2121,8 +2121,8 @@ .execute(db) } - if let lastKnownDate = metadata?.lastKnownServerRecord?.modificationDate { - if let recordDate = record.modificationDate, lastKnownDate < recordDate { + if let lastKnownDate = metadata?.lastKnownServerRecord?._modificationDate { + if let recordDate = record._modificationDate, lastKnownDate <= recordDate { try updateLastKnownServerRecord() } } else { diff --git a/Tests/SQLiteDataTests/CloudKitTests/MergeConflictTests.swift b/Tests/SQLiteDataTests/CloudKitTests/MergeConflictTests.swift index b9840148..a37a8987 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/MergeConflictTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/MergeConflictTests.swift @@ -67,11 +67,13 @@ """ } - let record = try syncEngine.private.database.record(for: Reminder.recordID(for: 1)) - record.setValue("Buy milk", forKey: "title", at: 60) - let modificationCallback = try { - try syncEngine.modifyRecords(scope: .private, saving: [record]) - }() + let modificationCallback = try withDependencies { + $0.currentTime.now += 60 + } operation: { + let record = try syncEngine.private.database.record(for: Reminder.recordID(for: 1)) + record.setValue("Buy milk", forKey: "title", at: now) + return try syncEngine.modifyRecords(scope: .private, saving: [record]) + } try await withDependencies { $0.currentTime.now += 30 @@ -79,8 +81,8 @@ try await userDatabase.userWrite { db in try Reminder.find(1).update { $0.isCompleted = true }.execute(db) } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) } - try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { """ @@ -126,8 +128,16 @@ """ } - try await syncEngine.processPendingRecordZoneChanges(scope: .private) - await modificationCallback.notify() + try await withDependencies { + $0.currentTime.now += 30 + } operation: { + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + } + await withDependencies { + $0.currentTime.now += 60 + } operation: { + await modificationCallback.notify() + } assertInlineSnapshot(of: container, as: .customDump) { """ diff --git a/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift b/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift index b8b86d11..762d9907 100644 --- a/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift +++ b/Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift @@ -56,7 +56,7 @@ } var nonEncryptedKeys = Set(allKeys()) .subtracting(encryptedValues.allKeys()) - .subtracting(["_recordChangeTag"]) + .subtracting(["_modificationDate", "_recordChangeTag"]) if self is CKShare { nonEncryptedKeys.insert(CKShare.SystemFieldKey.title) }