Skip to content

Commit 2cbe734

Browse files
committed
wip
1 parent 41f9b0e commit 2cbe734

5 files changed

Lines changed: 147 additions & 21 deletions

File tree

Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
package let databaseScope: CKDatabase.Scope
1010
let _container = IsolatedWeakVar<MockCloudContainer>()
1111
let dataManager = Dependency(\.dataManager)
12-
let quota: Int
12+
let quota: LockIsolated<Int>
1313

1414
struct AssetID: Hashable {
1515
let recordID: CKRecord.ID
@@ -23,7 +23,11 @@
2323

2424
package init(databaseScope: CKDatabase.Scope, quota: Int = Int.max) {
2525
self.databaseScope = databaseScope
26-
self.quota = quota
26+
self.quota = LockIsolated(quota)
27+
}
28+
29+
package func setQuota(_ quota: Int) {
30+
self.quota.withValue { $0 = quota }
2731
}
2832

2933
package func set(container: MockCloudContainer) {
@@ -281,7 +285,7 @@
281285
// Emulate quotas by reverting all changes if the total number of records stored exceeds
282286
// the quota. This is a very rough approximation of how iCloud handles this in the real
283287
// database.
284-
guard storage.totalRecords <= quota
288+
guard storage.totalRecords <= quota.withValue(\.self)
285289
else {
286290
storage = previousStorage
287291
for saveSuccessRecordID in saveResults.keys {

Sources/SQLiteData/CloudKit/SyncEngine.swift

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@
553553
}
554554
guard let privateSyncEngine, let sharedSyncEngine
555555
else { return }
556+
try await enqueueLocallyPendingChanges()
556557
async let `private`: Void = privateSyncEngine.sendChanges(options)
557558
async let shared: Void = sharedSyncEngine.sendChanges(options)
558559
_ = try await (`private`, shared)
@@ -590,8 +591,6 @@
590591
) async throws {
591592
try await enqueueLocallyPendingChanges()
592593
try await userDatabase.write { db in
593-
try PendingRecordZoneChange.delete().execute(db)
594-
595594
let newTableNames = currentRecordTypeByTableName.keys.filter { tableName in
596595
previousRecordTypeByTableName[tableName] == nil
597596
}
@@ -605,9 +604,10 @@
605604
}
606605

607606
private func enqueueLocallyPendingChanges() async throws {
608-
let pendingRecordZoneChanges = try await metadatabase.read { db in
607+
let pendingRecordZoneChanges = try await metadatabase.write { db in
609608
try PendingRecordZoneChange
610-
.select(\.pendingRecordZoneChange)
609+
.delete()
610+
.returning(\.pendingRecordZoneChange)
611611
.fetchAll(db)
612612
}
613613
let changesByIsPrivate = Dictionary(grouping: pendingRecordZoneChanges) {
@@ -1577,6 +1577,12 @@
15771577
var newPendingRecordZoneChanges: [CKSyncEngine.PendingRecordZoneChange] = []
15781578
var newPendingDatabaseChanges: [CKSyncEngine.PendingDatabaseChange] = []
15791579
defer {
1580+
let quotaExceeded = failedRecordSaves.contains(where: { $0.error.code == .quotaExceeded })
1581+
delegate?.syncEngine(
1582+
self,
1583+
quotaExceeded: quotaExceeded,
1584+
scope: syncEngine.database.databaseScope
1585+
)
15801586
syncEngine.state.add(pendingDatabaseChanges: newPendingDatabaseChanges)
15811587
syncEngine.state.add(pendingRecordZoneChanges: newPendingRecordZoneChanges)
15821588
}
@@ -1700,7 +1706,14 @@
17001706
break
17011707

17021708
case .quotaExceeded:
1703-
delegate?.syncEngine(self, quotaExceeded: syncEngine.database.databaseScope)
1709+
await withErrorReporting {
1710+
try await userDatabase.write { db in
1711+
try PendingRecordZoneChange.insert {
1712+
PendingRecordZoneChange(.saveRecord(failedRecord.recordID))
1713+
}
1714+
.execute(db)
1715+
}
1716+
}
17041717

17051718
case .networkFailure, .networkUnavailable, .zoneBusy, .serviceUnavailable,
17061719
.notAuthenticated, .operationCancelled,
@@ -1738,15 +1751,15 @@
17381751
syncEngine.state.add(pendingRecordZoneChanges: [.deleteRecord(failedRecordID)])
17391752
break
17401753
case .networkFailure, .networkUnavailable, .zoneBusy, .serviceUnavailable,
1741-
.notAuthenticated, .operationCancelled, .internalError, .partialFailure,
1742-
.badContainer, .requestRateLimited, .missingEntitlement, .invalidArguments,
1743-
.resultsTruncated, .assetFileNotFound, .assetFileModified, .incompatibleVersion,
1744-
.constraintViolation, .changeTokenExpired, .badDatabase, .quotaExceeded,
1745-
.limitExceeded, .userDeletedZone, .tooManyParticipants, .alreadyShared,
1746-
.managedAccountRestricted, .participantMayNeedVerification, .serverResponseLost,
1747-
.assetNotAvailable, .accountTemporarilyUnavailable, .permissionFailure,
1748-
.unknownItem, .serverRecordChanged, .serverRejectedRequest, .zoneNotFound,
1749-
.participantAlreadyInvited:
1754+
.notAuthenticated, .operationCancelled, .internalError, .partialFailure,
1755+
.badContainer, .requestRateLimited, .missingEntitlement, .invalidArguments,
1756+
.resultsTruncated, .assetFileNotFound, .assetFileModified, .incompatibleVersion,
1757+
.constraintViolation, .changeTokenExpired, .badDatabase, .quotaExceeded,
1758+
.limitExceeded, .userDeletedZone, .tooManyParticipants, .alreadyShared,
1759+
.managedAccountRestricted, .participantMayNeedVerification, .serverResponseLost,
1760+
.assetNotAvailable, .accountTemporarilyUnavailable, .permissionFailure,
1761+
.unknownItem, .serverRecordChanged, .serverRejectedRequest, .zoneNotFound,
1762+
.participantAlreadyInvited:
17501763
break
17511764
@unknown default:
17521765
break

Sources/SQLiteData/CloudKit/SyncEngineDelegate.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,15 @@
9292
/// in user, and _their_ iCloud storage is full. You can let the user know that they may want
9393
/// to contact the owner about upgrading their storage or cleaning up their iCloud account.
9494
///
95-
///
95+
///
9696
///
9797
/// - Parameters:
9898
/// - syncEngine: The sync engine that generates the event.
9999
/// - scope: The database that the event occured on.
100100
func syncEngine(
101101
_ syncEngine: SyncEngine,
102-
quotaExceeded scope: CKDatabase.Scope
102+
quotaExceeded: Bool,
103+
scope: CKDatabase.Scope
103104
)
104105
}
105106

@@ -123,7 +124,8 @@
123124

124125
public func syncEngine(
125126
_ syncEngine: SyncEngine,
126-
quotaExceeded scope: CKDatabase.Scope
127+
quotaExceeded: Bool,
128+
scope: CKDatabase.Scope
127129
) {
128130
}
129131
}

Tests/SQLiteDataTests/CloudKitTests/SyncEngineDelegateTests.swift

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,112 @@
288288
)
289289
"""
290290
}
291+
292+
syncEngine.private.database.setQuota(1)
293+
try await syncEngine.sendChanges()
294+
assertInlineSnapshot(of: container, as: .customDump) {
295+
"""
296+
MockCloudContainer(
297+
privateCloudDatabase: MockCloudDatabase(
298+
databaseScope: .private,
299+
storage: [
300+
[0]: CKRecord(
301+
recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__),
302+
recordType: "remindersLists",
303+
parent: nil,
304+
share: nil,
305+
id: 1,
306+
title: "Personal"
307+
)
308+
]
309+
),
310+
sharedCloudDatabase: MockCloudDatabase(
311+
databaseScope: .shared,
312+
storage: []
313+
)
314+
)
315+
"""
316+
}
317+
}
318+
319+
@Test(.quota(1), .syncEngineDelegate(QuotaExceededDelegate()))
320+
func quotaNotExceeded() async throws {
321+
try await userDatabase.userWrite { db in
322+
try db.seed {
323+
RemindersList(id: 1, title: "Personal")
324+
}
325+
}
326+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
327+
328+
assertQuery(RemindersList.all, database: userDatabase.database) {
329+
"""
330+
┌─────────────────────┐
331+
│ RemindersList( │
332+
│ id: 1, │
333+
│ title: "Personal"
334+
│ ) │
335+
└─────────────────────┘
336+
"""
337+
}
338+
assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) {
339+
"""
340+
┌────────────────────────────────────────────────────────────────────┐
341+
│ SyncMetadata( │
342+
│ id: SyncMetadata.ID( │
343+
│ recordPrimaryKey: "1", │
344+
│ recordType: "remindersLists"
345+
│ ), │
346+
│ zoneName: "zone", │
347+
│ ownerName: "__defaultOwner__", │
348+
│ recordName: "1:remindersLists", │
349+
│ parentRecordID: nil, │
350+
│ parentRecordName: nil, │
351+
│ lastKnownServerRecord: CKRecord( │
352+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
353+
│ recordType: "remindersLists", │
354+
│ parent: nil, │
355+
│ share: nil │
356+
│ ), │
357+
│ _lastKnownServerRecordAllFields: CKRecord( │
358+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
359+
│ recordType: "remindersLists", │
360+
│ parent: nil, │
361+
│ share: nil, │
362+
│ id: 1, │
363+
│ title: "Personal"
364+
│ ), │
365+
│ share: nil, │
366+
│ _isDeleted: false, │
367+
│ hasLastKnownServerRecord: true, │
368+
│ isShared: false, │
369+
│ userModificationTime: 0 │
370+
│ ) │
371+
└────────────────────────────────────────────────────────────────────┘
372+
"""
373+
}
374+
assertInlineSnapshot(of: container, as: .customDump) {
375+
"""
376+
MockCloudContainer(
377+
privateCloudDatabase: MockCloudDatabase(
378+
databaseScope: .private,
379+
storage: [
380+
[0]: CKRecord(
381+
recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__),
382+
recordType: "remindersLists",
383+
parent: nil,
384+
share: nil,
385+
id: 1,
386+
title: "Personal"
387+
)
388+
]
389+
),
390+
sharedCloudDatabase: MockCloudDatabase(
391+
databaseScope: .shared,
392+
storage: []
393+
)
394+
)
395+
"""
396+
}
291397
}
292398
}
293399
}

Tests/SQLiteDataTests/Internal/BaseCloudKitTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ class BaseCloudKitTests: @unchecked Sendable {
145145
syncEngine.private.assertAcceptedShareMetadata([])
146146

147147
try! syncEngine.metadatabase.read { db in
148-
try #expect(UnsyncedRecordID.count().fetchOne(db) == 0)
148+
try #expect(UnsyncedRecordID.fetchCount(db) == 0)
149+
try #expect(PendingRecordZoneChange.fetchCount(db) == 0)
149150
}
150151
} else {
151152
Issue.record("Tests must be run on iOS 17+, macOS 14+, tvOS 17+ and watchOS 10+.")

0 commit comments

Comments
 (0)