Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 48 additions & 15 deletions Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,23 @@

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
extension SyncEngine {
package func processPendingRecordZoneChanges(
package struct SendRecordsCallback {
fileprivate let operation: @Sendable () async -> Void
@discardableResult
package func receive() async {
await operation()
}
}

package func sendPendingRecordZoneChanges(
options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions(),
scope: CKDatabase.Scope,
forceAtomicByZone: Bool? = nil,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async throws {
) async throws -> SendRecordsCallback {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need this bit of infrastructure in the mocks to wiggle in between the moment a batch is sent out to iCloud and the moment the sentRecordZoneChanges delegate method is called.

let syncEngine = syncEngine(for: scope)
guard !syncEngine.state.pendingRecordZoneChanges.isEmpty
else {
Expand All @@ -218,7 +226,7 @@
line: line,
column: column
)
return
return SendRecordsCallback {}
}
guard try await container.accountStatus() == .available
else {
Expand All @@ -231,7 +239,7 @@
line: line,
column: column
)
return
return SendRecordsCallback {}
}

var batch = await nextRecordZoneChangeBatch(
Expand All @@ -254,7 +262,9 @@
batch?.atomicByZone = forceAtomicByZone
}
guard let batch
else { return }
else {
return SendRecordsCallback {}
}

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

await syncEngine.parentSyncEngine
.handleEvent(
.sentRecordZoneChanges(
savedRecords: savedRecords,
failedRecordSaves: failedRecordSaves,
deletedRecordIDs: deletedRecordIDs,
failedRecordDeletes: failedRecordDeletes
),
syncEngine: syncEngine
)
return SendRecordsCallback { [savedRecords, failedRecordSaves, deletedRecordIDs, failedRecordDeletes] in
await syncEngine.parentSyncEngine
.handleEvent(
.sentRecordZoneChanges(
savedRecords: savedRecords,
failedRecordSaves: failedRecordSaves,
deletedRecordIDs: deletedRecordIDs,
failedRecordDeletes: failedRecordDeletes
),
syncEngine: syncEngine
)
}
}

package func processPendingRecordZoneChanges(
options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions(),
scope: CKDatabase.Scope,
forceAtomicByZone: Bool? = nil,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) async throws {
try await sendPendingRecordZoneChanges(
options: options,
scope: scope,
forceAtomicByZone: forceAtomicByZone,
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
.receive()
}

package func processPendingDatabaseChanges(
Expand Down
4 changes: 3 additions & 1 deletion Sources/SQLiteData/CloudKit/SyncEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2424,7 +2424,9 @@
self.lastKnownServerRecord = lastKnownServerRecord
self._lastKnownServerRecordAllFields = lastKnownServerRecord
if let lastKnownServerRecord {
self.userModificationTime = lastKnownServerRecord.userModificationTime
self.userModificationTime = #sql("""
max(\(self.userModificationTime), \(lastKnownServerRecord.userModificationTime))
""")
Comment on lines +2427 to +2429
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix.

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,71 @@
"""
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Test(.printTimestamps(true), .printRecordChangeTag)
func editBetweenBatchAndSentRecordZoneChanges() async throws {
try await userDatabase.userWrite { db in
try db.seed {
RemindersList(id: 1, title: "Personal")
}
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)

try await withDependencies {
$0.currentTime.now += 1
} operation: {
try await userDatabase.userWrite { db in
try RemindersList.find(1).update { $0.title = "Personal 2" }.execute(db)
}

let changes = try await syncEngine.sendPendingRecordZoneChanges(scope: .private)

try await withDependencies {
$0.currentTime.now += 1
} operation: {
try await userDatabase.userWrite { db in
try RemindersList.find(1).update { $0.title = "Personal 3" }.execute(db)
}
await changes.receive()
try await syncEngine.processPendingRecordZoneChanges(scope: .private)

try await userDatabase.read { db in
expectNoDifference(
try RemindersList.fetchAll(db),
[RemindersList(id: 1, title: "Personal 3")]
)
}
assertInlineSnapshot(of: syncEngine.container, as: .customDump) {
"""
MockCloudContainer(
privateCloudDatabase: MockCloudDatabase(
databaseScope: .private,
storage: [
[0]: CKRecord(
recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__),
recordType: "remindersLists",
parent: nil,
share: nil,
recordChangeTag: 3,
id: 1,
id🗓️: 0,
title: "Personal 3",
title🗓️: 2,
🗓️: 2
Comment on lines +315 to +316
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can write a failing test that shows data not properly syncing (we need to better support multiple sync engines in a test), but this at least shows the edit was made at the right time. Without the changes in this PR this timestamp was 1.

)
]
),
sharedCloudDatabase: MockCloudDatabase(
databaseScope: .shared,
storage: []
)
)
"""
}
}
}
}
}
}

Expand Down
17 changes: 11 additions & 6 deletions Tests/SQLiteDataTests/Internal/CloudKit+CustomDump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

extension CKRecord {
@TaskLocal static var printTimestamps = false
@TaskLocal static var printRecordChangeTag = false
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted a way to get visibility into record change tags.

}

@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
Expand Down Expand Up @@ -50,14 +51,18 @@
let nonEncryptedKeys = Set(allKeys())
.subtracting(encryptedValues.allKeys())
.subtracting(["_recordChangeTag"])
var baseChildren = [
("recordID", recordID as Any),
("recordType", recordType as Any),
("parent", parent as Any),
("share", share as Any),
]
if Self.printRecordChangeTag {
baseChildren.append(("recordChangeTag", _recordChangeTag as Any))
}
return Mirror(
self,
children: [
("recordID", recordID as Any),
("recordType", recordType as Any),
("parent", parent as Any),
("share", share as Any),
]
children: baseChildren
+ keys
.map {
$0.hasPrefix(CKRecord.userModificationTimeKey)
Expand Down
28 changes: 0 additions & 28 deletions Tests/SQLiteDataTests/Internal/PrintTimestampsScope.swift

This file was deleted.

49 changes: 49 additions & 0 deletions Tests/SQLiteDataTests/Internal/TestScopes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,53 @@
Self(syncEngineDelegate: syncEngineDelegate)
}
}

struct _PrintRecordChangeTag: SuiteTrait, TestScoping, TestTrait {
let printRecordChangeTag: Bool
init(_ printRecordChangeTag: Bool = true) {
self.printRecordChangeTag = printRecordChangeTag
}

func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
try await CKRecord.$printRecordChangeTag.withValue(true) {
try await function()
}
}
}

extension Trait where Self == _PrintRecordChangeTag {
static var printRecordChangeTag: Self { Self() }
static func printRecordChangeTag(_ printRecordChangeTag: Bool) -> Self {
Self(printRecordChangeTag)
}
}

struct _PrintTimestampsScope: SuiteTrait, TestScoping, TestTrait {
let printTimestamps: Bool
init(_ printTimestamps: Bool = true) {
self.printTimestamps = printTimestamps
}

func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
try await CKRecord.$printTimestamps.withValue(true) {
try await function()
}
}
}

extension Trait where Self == _PrintTimestampsScope {
static var printTimestamps: Self { Self() }
static func printTimestamps(_ printTimestamps: Bool) -> Self {
Self(printTimestamps)
}
}

#endif