Skip to content

Commit 177f45f

Browse files
committed
wip
1 parent b13a938 commit 177f45f

4 files changed

Lines changed: 69 additions & 66 deletions

File tree

Examples/CloudKitDemo/CountersListFeature.swift

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import CloudKit
22
import SQLiteData
3-
import SwiftData
43
import SwiftUI
54
import SwiftUINavigation
65

76
struct CountersListView: View {
87
@FetchAll var counters: [Counter]
98
@Dependency(\.defaultDatabase) var database
10-
@Dependency(\.defaultSyncEngine) var syncEngine
119

1210
var body: some View {
1311
List {
@@ -35,55 +33,8 @@ struct CountersListView: View {
3533
}
3634
}
3735
}
38-
ToolbarItem {
39-
Button("Trigger") {
40-
Task { await trigger() }
41-
}
42-
}
43-
}
44-
}
45-
46-
func trigger() async {
47-
await withErrorReporting {
48-
// Step 1: Create a new counter
49-
let newCounter = try await database.write { db in
50-
try Counter.insert { Counter.Draft() }
51-
.returning(\.self)
52-
.fetchOne(db)!
53-
}
54-
55-
// Step 2: Force sending all data to iCloud
56-
try await syncEngine.sendChanges()
57-
58-
// Step 3: Grab the CKRecord for the newly inserted counter
59-
let newCounterLastKnownServerRecord = try await database.read { db in
60-
try SyncMetadata.find(newCounter.syncMetadataID).select(\.lastKnownServerRecord)
61-
.fetchOne(db)!!
62-
}
63-
64-
// Step 4: Make a change to the counter directly on iCloud
65-
let container = CKContainer(identifier: ModelConfiguration(groupContainer: .automatic).cloudKitContainerIdentifier!)
66-
let serverCounter = try await container.privateCloudDatabase.record(for: newCounterLastKnownServerRecord.recordID)
67-
serverCounter.encryptedValues["count"] = Int.random(in: 1...1_000)
68-
let (saveResults, _) = try await container.privateCloudDatabase.modifyRecords(saving: [serverCounter], deleting: [])
69-
70-
// Step 5: Make two changes to the local database: 1) decrement any counter besides the one
71-
// created above (should succeed), and 2) increment the counter just created (should
72-
// fail due to conflict)
73-
try await database.write { db in
74-
try Counter
75-
.where { $0.id.neq(newCounter.id) }
76-
.update { $0.count -= 1 }
77-
.execute(db)
78-
try Counter
79-
.find(newCounter.id)
80-
.update { $0.count += 1 }
81-
.execute(db)
8236
}
83-
84-
try await syncEngine.sendChanges()
8537
}
86-
}
8738

8839
func deleteRows(at indexSet: IndexSet) {
8940
withErrorReporting {

Examples/CloudKitDemo/Schema.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ nonisolated struct Counter: Identifiable {
1111
extension DependencyValues {
1212
mutating func bootstrapDatabase() throws {
1313
@Dependency(\.context) var context
14-
var configuration = Configuration()
15-
configuration.prepareDatabase {
16-
try $0.attachMetadatabase()
17-
}
18-
let database = try SQLiteData.defaultDatabase(configuration: configuration)
14+
let database = try SQLiteData.defaultDatabase()
1915
logger.debug(
2016
"""
2117
App database

Sources/SQLiteData/CloudKit/SyncEngine.swift

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -915,8 +915,7 @@
915915
parentForeignKey: parentForeignKey,
916916
defaultZone: defaultZone,
917917
privateTables: privateTables
918-
)
919-
{
918+
) {
920919
try trigger.execute(db)
921920
}
922921
}
@@ -932,7 +931,7 @@
932931
defaultZone: defaultZone,
933932
privateTables: privateTables
934933
)
935-
.reversed() {
934+
.reversed() {
936935
try trigger.drop().execute(db)
937936
}
938937
}
@@ -1722,20 +1721,43 @@
17221721
}
17231722
}
17241723

1724+
for (failedRecordID, error) in failedRecordDeletes {
1725+
}
1726+
1727+
//
1728+
//
1729+
17251730
let enqueuedUnsyncedRecordID =
17261731
await withErrorReporting(.sqliteDataCloudKitFailure) {
17271732
try await userDatabase.write { db in
17281733
var enqueuedUnsyncedRecordID = false
17291734
for (failedRecordID, error) in failedRecordDeletes {
1730-
guard
1731-
error.code == .referenceViolation
1732-
else { continue }
1733-
try UnsyncedRecordID.insert(or: .ignore) {
1734-
UnsyncedRecordID(recordID: failedRecordID)
1735+
switch error.code {
1736+
case .referenceViolation:
1737+
enqueuedUnsyncedRecordID = true
1738+
try UnsyncedRecordID.insert(or: .ignore) {
1739+
UnsyncedRecordID(recordID: failedRecordID)
1740+
}
1741+
.execute(db)
1742+
syncEngine.state.remove(pendingRecordZoneChanges: [.deleteRecord(failedRecordID)])
1743+
break
1744+
case .batchRequestFailed:
1745+
syncEngine.state.add(pendingRecordZoneChanges: [.deleteRecord(failedRecordID)])
1746+
break
1747+
case .networkFailure, .networkUnavailable, .zoneBusy, .serviceUnavailable,
1748+
.notAuthenticated, .operationCancelled, .internalError, .partialFailure,
1749+
.badContainer, .requestRateLimited, .missingEntitlement, .invalidArguments,
1750+
.resultsTruncated, .assetFileNotFound, .assetFileModified, .incompatibleVersion,
1751+
.constraintViolation, .changeTokenExpired, .badDatabase, .quotaExceeded,
1752+
.limitExceeded, .userDeletedZone, .tooManyParticipants, .alreadyShared,
1753+
.managedAccountRestricted, .participantMayNeedVerification, .serverResponseLost,
1754+
.assetNotAvailable, .accountTemporarilyUnavailable, .permissionFailure,
1755+
.unknownItem, .serverRecordChanged, .serverRejectedRequest, .zoneNotFound,
1756+
.participantAlreadyInvited:
1757+
break
1758+
@unknown default:
1759+
break
17351760
}
1736-
.execute(db)
1737-
syncEngine.state.remove(pendingRecordZoneChanges: [.deleteRecord(failedRecordID)])
1738-
enqueuedUnsyncedRecordID = true
17391761
}
17401762
return enqueuedUnsyncedRecordID
17411763
}

Tests/SQLiteDataTests/CloudKitTests/AtomicTests.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111

1212
extension BaseCloudKitTests {
1313
@MainActor
14+
@Suite(.atomicByZone(true))
1415
final class AtomicTests: BaseCloudKitTests, @unchecked Sendable {
1516
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
16-
@Test(.atomicByZone(true)) func basics() async throws {
17+
@Test func editConflictAndNewRecord() async throws {
1718
try await userDatabase.userWrite { db in
1819
try db.seed {
1920
RemindersList(id: 1, title: "Personal")
@@ -96,6 +97,39 @@
9697
"""
9798
}
9899
}
100+
101+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
102+
@Test func deleteConflictAndNewRecord() async throws {
103+
try await userDatabase.userWrite { db in
104+
try db.seed {
105+
RemindersList(id: 1, title: "Personal")
106+
}
107+
}
108+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
109+
110+
let remindersListRecord = try syncEngine.private.database.record(
111+
for: RemindersList.recordID(for: 1)
112+
)
113+
remindersListRecord.setValue("My stuff", forKey: "title", at: 1)
114+
let (saveResults, _) = try syncEngine.private.database.modifyRecords(saving: [remindersListRecord])
115+
#expect(saveResults.values.allSatisfy { $0.error == nil })
116+
117+
try await withDependencies {
118+
$0.currentTime.now = 2
119+
} operation: {
120+
try await userDatabase.userWrite { db in
121+
try RemindersList.find(1).delete().execute(db)
122+
try RemindersList.insert { RemindersList(id: 2, title: "Business") }.execute(db)
123+
}
124+
}
125+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
126+
127+
assertInlineSnapshot(of: container, as: .customDump)
128+
129+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
130+
131+
assertInlineSnapshot(of: container, as: .customDump)
132+
}
99133
}
100134
}
101135
#endif

0 commit comments

Comments
 (0)