Skip to content

Commit f3f7f5f

Browse files
authored
Fix user modification timestamp. (#386)
* Fix user modification timestamp. * wip
1 parent 76ec072 commit f3f7f5f

File tree

6 files changed

+176
-50
lines changed

6 files changed

+176
-50
lines changed

Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,23 @@
199199

200200
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
201201
extension SyncEngine {
202-
package func processPendingRecordZoneChanges(
202+
package struct SendRecordsCallback {
203+
fileprivate let operation: @Sendable () async -> Void
204+
@discardableResult
205+
package func receive() async {
206+
await operation()
207+
}
208+
}
209+
210+
package func sendPendingRecordZoneChanges(
203211
options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions(),
204212
scope: CKDatabase.Scope,
205213
forceAtomicByZone: Bool? = nil,
206214
fileID: StaticString = #fileID,
207215
filePath: StaticString = #filePath,
208216
line: UInt = #line,
209217
column: UInt = #column
210-
) async throws {
218+
) async throws -> SendRecordsCallback {
211219
let syncEngine = syncEngine(for: scope)
212220
guard !syncEngine.state.pendingRecordZoneChanges.isEmpty
213221
else {
@@ -218,7 +226,7 @@
218226
line: line,
219227
column: column
220228
)
221-
return
229+
return SendRecordsCallback {}
222230
}
223231
guard try await container.accountStatus() == .available
224232
else {
@@ -231,7 +239,7 @@
231239
line: line,
232240
column: column
233241
)
234-
return
242+
return SendRecordsCallback {}
235243
}
236244

237245
var batch = await nextRecordZoneChangeBatch(
@@ -254,7 +262,9 @@
254262
batch?.atomicByZone = forceAtomicByZone
255263
}
256264
guard let batch
257-
else { return }
265+
else {
266+
return SendRecordsCallback {}
267+
}
258268

259269
let (saveResults, deleteResults) = try syncEngine.database.modifyRecords(
260270
saving: batch.recordsToSave,
@@ -302,16 +312,39 @@
302312
pendingRecordZoneChanges: failedRecordDeletes.keys.map { .deleteRecord($0) }
303313
)
304314

305-
await syncEngine.parentSyncEngine
306-
.handleEvent(
307-
.sentRecordZoneChanges(
308-
savedRecords: savedRecords,
309-
failedRecordSaves: failedRecordSaves,
310-
deletedRecordIDs: deletedRecordIDs,
311-
failedRecordDeletes: failedRecordDeletes
312-
),
313-
syncEngine: syncEngine
314-
)
315+
return SendRecordsCallback { [savedRecords, failedRecordSaves, deletedRecordIDs, failedRecordDeletes] in
316+
await syncEngine.parentSyncEngine
317+
.handleEvent(
318+
.sentRecordZoneChanges(
319+
savedRecords: savedRecords,
320+
failedRecordSaves: failedRecordSaves,
321+
deletedRecordIDs: deletedRecordIDs,
322+
failedRecordDeletes: failedRecordDeletes
323+
),
324+
syncEngine: syncEngine
325+
)
326+
}
327+
}
328+
329+
package func processPendingRecordZoneChanges(
330+
options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions(),
331+
scope: CKDatabase.Scope,
332+
forceAtomicByZone: Bool? = nil,
333+
fileID: StaticString = #fileID,
334+
filePath: StaticString = #filePath,
335+
line: UInt = #line,
336+
column: UInt = #column
337+
) async throws {
338+
try await sendPendingRecordZoneChanges(
339+
options: options,
340+
scope: scope,
341+
forceAtomicByZone: forceAtomicByZone,
342+
fileID: fileID,
343+
filePath: filePath,
344+
line: line,
345+
column: column
346+
)
347+
.receive()
315348
}
316349

317350
package func processPendingDatabaseChanges(

Sources/SQLiteData/CloudKit/SyncEngine.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2426,7 +2426,9 @@
24262426
self.lastKnownServerRecord = lastKnownServerRecord
24272427
self._lastKnownServerRecordAllFields = lastKnownServerRecord
24282428
if let lastKnownServerRecord {
2429-
self.userModificationTime = lastKnownServerRecord.userModificationTime
2429+
self.userModificationTime = #sql("""
2430+
max(\(self.userModificationTime), \(lastKnownServerRecord.userModificationTime))
2431+
""")
24302432
}
24312433
}
24322434
}

Tests/SQLiteDataTests/CloudKitTests/NextRecordZoneChangeBatchTests.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,71 @@
262262
"""
263263
}
264264
}
265+
266+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
267+
@Test(.printTimestamps(true), .printRecordChangeTag)
268+
func editBetweenBatchAndSentRecordZoneChanges() async throws {
269+
try await userDatabase.userWrite { db in
270+
try db.seed {
271+
RemindersList(id: 1, title: "Personal")
272+
}
273+
}
274+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
275+
276+
try await withDependencies {
277+
$0.currentTime.now += 1
278+
} operation: {
279+
try await userDatabase.userWrite { db in
280+
try RemindersList.find(1).update { $0.title = "Personal 2" }.execute(db)
281+
}
282+
283+
let changes = try await syncEngine.sendPendingRecordZoneChanges(scope: .private)
284+
285+
try await withDependencies {
286+
$0.currentTime.now += 1
287+
} operation: {
288+
try await userDatabase.userWrite { db in
289+
try RemindersList.find(1).update { $0.title = "Personal 3" }.execute(db)
290+
}
291+
await changes.receive()
292+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
293+
294+
try await userDatabase.read { db in
295+
expectNoDifference(
296+
try RemindersList.fetchAll(db),
297+
[RemindersList(id: 1, title: "Personal 3")]
298+
)
299+
}
300+
assertInlineSnapshot(of: syncEngine.container, as: .customDump) {
301+
"""
302+
MockCloudContainer(
303+
privateCloudDatabase: MockCloudDatabase(
304+
databaseScope: .private,
305+
storage: [
306+
[0]: CKRecord(
307+
recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__),
308+
recordType: "remindersLists",
309+
parent: nil,
310+
share: nil,
311+
recordChangeTag: 3,
312+
id: 1,
313+
id🗓️: 0,
314+
title: "Personal 3",
315+
title🗓️: 2,
316+
🗓️: 2
317+
)
318+
]
319+
),
320+
sharedCloudDatabase: MockCloudDatabase(
321+
databaseScope: .shared,
322+
storage: []
323+
)
324+
)
325+
"""
326+
}
327+
}
328+
}
329+
}
265330
}
266331
}
267332

Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
extension CKRecord {
2222
@TaskLocal static var printTimestamps = false
23+
@TaskLocal static var printRecordChangeTag = false
2324
}
2425

2526
@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
@@ -50,14 +51,18 @@
5051
let nonEncryptedKeys = Set(allKeys())
5152
.subtracting(encryptedValues.allKeys())
5253
.subtracting(["_recordChangeTag"])
54+
var baseChildren = [
55+
("recordID", recordID as Any),
56+
("recordType", recordType as Any),
57+
("parent", parent as Any),
58+
("share", share as Any),
59+
]
60+
if Self.printRecordChangeTag {
61+
baseChildren.append(("recordChangeTag", _recordChangeTag as Any))
62+
}
5363
return Mirror(
5464
self,
55-
children: [
56-
("recordID", recordID as Any),
57-
("recordType", recordType as Any),
58-
("parent", parent as Any),
59-
("share", share as Any),
60-
]
65+
children: baseChildren
6166
+ keys
6267
.map {
6368
$0.hasPrefix(CKRecord.userModificationTimeKey)

Tests/SQLiteDataTests/Internal/PrintTimestampsScope.swift

Lines changed: 0 additions & 28 deletions
This file was deleted.

Tests/SQLiteDataTests/Internal/TestScopes.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,53 @@
126126
Self(syncEngineDelegate: syncEngineDelegate)
127127
}
128128
}
129+
130+
struct _PrintRecordChangeTag: SuiteTrait, TestScoping, TestTrait {
131+
let printRecordChangeTag: Bool
132+
init(_ printRecordChangeTag: Bool = true) {
133+
self.printRecordChangeTag = printRecordChangeTag
134+
}
135+
136+
func provideScope(
137+
for test: Test,
138+
testCase: Test.Case?,
139+
performing function: @Sendable () async throws -> Void
140+
) async throws {
141+
try await CKRecord.$printRecordChangeTag.withValue(true) {
142+
try await function()
143+
}
144+
}
145+
}
146+
147+
extension Trait where Self == _PrintRecordChangeTag {
148+
static var printRecordChangeTag: Self { Self() }
149+
static func printRecordChangeTag(_ printRecordChangeTag: Bool) -> Self {
150+
Self(printRecordChangeTag)
151+
}
152+
}
153+
154+
struct _PrintTimestampsScope: SuiteTrait, TestScoping, TestTrait {
155+
let printTimestamps: Bool
156+
init(_ printTimestamps: Bool = true) {
157+
self.printTimestamps = printTimestamps
158+
}
159+
160+
func provideScope(
161+
for test: Test,
162+
testCase: Test.Case?,
163+
performing function: @Sendable () async throws -> Void
164+
) async throws {
165+
try await CKRecord.$printTimestamps.withValue(true) {
166+
try await function()
167+
}
168+
}
169+
}
170+
171+
extension Trait where Self == _PrintTimestampsScope {
172+
static var printTimestamps: Self { Self() }
173+
static func printTimestamps(_ printTimestamps: Bool) -> Self {
174+
Self(printTimestamps)
175+
}
176+
}
177+
129178
#endif

0 commit comments

Comments
 (0)