Skip to content

Commit 08adaa0

Browse files
committed
Replace _isDeleted with enum value _pendingStatus
Trigger record rebuilding behavior and cleanup on _pendingStatus == .reinserted
1 parent d39ab5f commit 08adaa0

14 files changed

Lines changed: 582 additions & 181 deletions

Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,30 @@
125125
)
126126
.execute(db)
127127
}
128+
migrator.registerMigration("Replace _isDeleted with _pendingStatus") { db in
129+
try #sql(
130+
"""
131+
ALTER TABLE "\(raw: .sqliteDataCloudKitSchemaName)_metadata"
132+
ADD COLUMN "_pendingStatus" INTEGER
133+
"""
134+
)
135+
.execute(db)
136+
try #sql(
137+
"""
138+
UPDATE "\(raw: .sqliteDataCloudKitSchemaName)_metadata"
139+
SET "_pendingStatus" = 0
140+
WHERE "_isDeleted" = 1
141+
"""
142+
)
143+
.execute(db)
144+
try #sql(
145+
"""
146+
ALTER TABLE "\(raw: .sqliteDataCloudKitSchemaName)_metadata"
147+
DROP COLUMN "_isDeleted"
148+
"""
149+
)
150+
.execute(db)
151+
}
128152
#if DEBUG
129153
try metadatabase.read { db in
130154
let hasSchemaChanges = try migrator.hasSchemaChanges(db)

Sources/SQLiteData/CloudKit/Internal/Triggers.swift

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
$0.recordPrimaryKey.eq(#sql("\(old.primaryKey)"))
5555
&& $0.recordType.eq(tableName)
5656
}
57-
.update { $0._isDeleted = true }
57+
.update { $0._pendingStatus = #bind(.deleted) }
5858
} when: { old, new in
5959
old.primaryKey.neq(new.primaryKey)
6060
}
@@ -86,12 +86,11 @@
8686
.where {
8787
$0.recordPrimaryKey.eq(#sql("\(new.primaryKey)"))
8888
&& $0.recordType.eq(tableName)
89-
&& $0._isDeleted
89+
&& $0._pendingStatus.eq(PendingStatus.deleted)
9090
}
9191
.update {
92-
$0._isDeleted = false
92+
$0._pendingStatus = #bind(.reinserted)
9393
$0.userModificationTime = $currentTime()
94-
$0._lastKnownServerRecordAllFields = #bind(nil)
9594
}
9695
}
9796
)
@@ -150,7 +149,7 @@
150149
$0.recordPrimaryKey.eq(#sql("\(old.primaryKey)"))
151150
&& $0.recordType.eq(tableName)
152151
}
153-
.update { $0._isDeleted = true }
152+
.update { $0._pendingStatus = #bind(.deleted) }
154153
} when: { _ in
155154
!SyncEngine.$isSynchronizing
156155
}
@@ -253,7 +252,7 @@
253252
afterZoneUpdateTrigger(),
254253
afterUpdateTrigger(for: syncEngine),
255254
afterSoftDeleteTrigger(for: syncEngine),
256-
afterUndeleteTrigger(for: syncEngine),
255+
afterReinsertTrigger(for: syncEngine),
257256
]
258257
}
259258

@@ -335,7 +334,7 @@
335334
)
336335
)
337336
} when: { old, new in
338-
old._isDeleted.eq(new._isDeleted) && !SyncEngine.$isSynchronizing
337+
old._pendingStatus.is(new._pendingStatus) && !SyncEngine.$isSynchronizing
339338
}
340339
)
341340
}
@@ -346,7 +345,7 @@
346345
createTemporaryTrigger(
347346
"\(String.sqliteDataCloudKitSchemaName)_after_delete_on_sqlitedata_icloud_metadata",
348347
ifNotExists: true,
349-
after: .update(of: \._isDeleted) { _, new in
348+
after: .update(of: \._pendingStatus) { _, new in
350349
Values(
351350
syncEngine.$didDelete(
352351
recordName: new.recordName,
@@ -356,18 +355,20 @@
356355
)
357356
)
358357
} when: { old, new in
359-
!old._isDeleted && new._isDeleted && !SyncEngine.$isSynchronizing
358+
(old._pendingStatus.is(nil) || old._pendingStatus.neq(PendingStatus.deleted))
359+
&& new._pendingStatus.eq(PendingStatus.deleted)
360+
&& !SyncEngine.$isSynchronizing
360361
}
361362
)
362363
}
363364

364-
fileprivate static func afterUndeleteTrigger(
365+
fileprivate static func afterReinsertTrigger(
365366
for syncEngine: SyncEngine
366367
) -> TemporaryTrigger<Self> {
367368
createTemporaryTrigger(
368-
"\(String.sqliteDataCloudKitSchemaName)_after_undelete_on_sqlitedata_icloud_metadata",
369+
"\(String.sqliteDataCloudKitSchemaName)_after_reinsert_on_sqlitedata_icloud_metadata",
369370
ifNotExists: true,
370-
after: .update(of: \._isDeleted) { _, new in
371+
after: .update(of: \._pendingStatus) { _, new in
371372
Values(
372373
syncEngine.$didUpdate(
373374
recordName: new.recordName,
@@ -379,7 +380,9 @@
379380
)
380381
)
381382
} when: { old, new in
382-
old._isDeleted && !new._isDeleted && !SyncEngine.$isSynchronizing
383+
old._pendingStatus.eq(PendingStatus.deleted)
384+
&& new._pendingStatus.eq(PendingStatus.reinserted)
385+
&& !SyncEngine.$isSynchronizing
383386
}
384387
)
385388
}

Sources/SQLiteData/CloudKit/SyncEngine.swift

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,13 +1125,12 @@
11251125

11261126
let batch = await syncEngine.recordZoneChangeBatch(pendingChanges: changes) { recordID in
11271127
guard
1128-
let (metadata, allFields) = await withErrorReporting(
1128+
let metadata = await withErrorReporting(
11291129
.sqliteDataCloudKitFailure,
11301130
catching: {
11311131
try await metadatabase.read { db in
11321132
try SyncMetadata
11331133
.find(recordID)
1134-
.select { ($0, $0._lastKnownServerRecordAllFields) }
11351134
.fetchOne(db)
11361135
}
11371136
}
@@ -1197,8 +1196,7 @@
11971196
}
11981197

11991198
let record =
1200-
allFields
1201-
?? metadata.lastKnownServerRecord
1199+
metadata.allFieldsRecord
12021200
?? CKRecord(
12031201
recordType: metadata.recordType,
12041202
recordID: recordID
@@ -1221,7 +1219,17 @@
12211219
with: T(queryOutput: row),
12221220
userModificationTime: metadata.userModificationTime
12231221
)
1224-
await refreshLastKnownServerRecord(record)
1222+
await withErrorReporting(.sqliteDataCloudKitFailure) {
1223+
try await userDatabase.write { db in
1224+
try refreshLastKnownServerRecord(record, db: db)
1225+
if metadata._pendingStatus == .reinserted {
1226+
try SyncMetadata
1227+
.find(record.recordID)
1228+
.update { $0._pendingStatus = #bind(nil) }
1229+
.execute(db)
1230+
}
1231+
}
1232+
}
12251233
sentRecord = recordID
12261234
return record
12271235
}
@@ -1949,9 +1957,12 @@
19491957
func open<T>(_ table: some SynchronizableTable<T>) throws {
19501958
var columnNames: [String] = T.TableColumns.writableColumns.map(\.name)
19511959
if !force,
1952-
let allFields = metadata._lastKnownServerRecordAllFields,
1960+
let allFields = metadata.allFieldsRecord,
19531961
let row = try T.find(#sql("\(bind: metadata.recordPrimaryKey)")).fetchOne(db)
19541962
{
1963+
if metadata._pendingStatus == .reinserted {
1964+
allFields.update(with: T(queryOutput: row), userModificationTime: metadata.userModificationTime)
1965+
}
19551966
serverRecord.update(
19561967
with: allFields,
19571968
row: T(queryOutput: row),
@@ -1971,6 +1982,12 @@
19711982
.find(serverRecord.recordID)
19721983
.update { $0.setLastKnownServerRecord(serverRecord) }
19731984
.execute(db)
1985+
if metadata._pendingStatus == .reinserted {
1986+
try SyncMetadata
1987+
.find(serverRecord.recordID)
1988+
.update { $0._pendingStatus = #bind(nil) }
1989+
.execute(db)
1990+
}
19741991
} catch {
19751992
guard
19761993
let error = error as? DatabaseError,
@@ -1993,22 +2010,25 @@
19932010
private func refreshLastKnownServerRecord(_ record: CKRecord) async {
19942011
await withErrorReporting(.sqliteDataCloudKitFailure) {
19952012
try await userDatabase.write { db in
1996-
let metadata = try SyncMetadata.find(record.recordID).fetchOne(db)
1997-
func updateLastKnownServerRecord() throws {
1998-
try SyncMetadata
1999-
.find(record.recordID)
2000-
.update { $0.setLastKnownServerRecord(record) }
2001-
.execute(db)
2002-
}
2003-
2004-
if let lastKnownDate = metadata?.lastKnownServerRecord?.modificationDate {
2005-
if let recordDate = record.modificationDate, lastKnownDate < recordDate {
2006-
try updateLastKnownServerRecord()
2007-
}
2008-
} else {
2009-
try updateLastKnownServerRecord()
2010-
}
2013+
try refreshLastKnownServerRecord(record, db: db)
2014+
}
2015+
}
2016+
}
2017+
2018+
private func refreshLastKnownServerRecord(_ record: CKRecord, db: Database) throws {
2019+
let metadata = try SyncMetadata.find(record.recordID).fetchOne(db)
2020+
func updateLastKnownServerRecord() throws {
2021+
try SyncMetadata
2022+
.find(record.recordID)
2023+
.update { $0.setLastKnownServerRecord(record) }
2024+
.execute(db)
2025+
}
2026+
if let lastKnownDate = metadata?.lastKnownServerRecord?.modificationDate {
2027+
if let recordDate = record.modificationDate, lastKnownDate < recordDate {
2028+
try updateLastKnownServerRecord()
20112029
}
2030+
} else {
2031+
try updateLastKnownServerRecord()
20122032
}
20132033
}
20142034

Sources/SQLiteData/CloudKit/SyncMetadata.swift

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
#if canImport(CloudKit)
22
import CloudKit
33

4+
/// Represents the pending synchronization state of a record.
5+
public enum PendingStatus: Int, Hashable, Sendable, QueryBindable, QueryDecodable, QueryRepresentable {
6+
/// Indicates the metadata has been "soft" deleted. It will be fully deleted once the
7+
/// next batch of pending changes is processed.
8+
case deleted = 0
9+
/// Indicates the record has been reinserted after being soft-deleted. This status
10+
/// will be cleared after the next batch of pending changes is processed or server
11+
/// record changes are applied.
12+
case reinserted = 1
13+
}
14+
415
/// A table that tracks metadata related to synchronized data.
516
///
617
/// Each row of this table represents a synchronized record across all tables synchronized with
@@ -93,9 +104,23 @@
93104
@Column(as: CKShare?.SystemFieldsRepresentation.self)
94105
public let share: CKShare?
95106

96-
/// Determines if the metadata has been "soft" deleted. It will be fully deleted once the
97-
/// next batch of pending changes is processed.
98-
public let _isDeleted: Bool
107+
/// The pending synchronization state of the record which can require special handling.
108+
///
109+
/// `nil` indicates a normal record with standard sync behavior.
110+
public let _pendingStatus: PendingStatus?
111+
112+
/// The appropriate all-fields record to use as a base when building a `CKRecord` for upload.
113+
///
114+
/// For reinserted rows, returns `lastKnownServerRecord` (system fields only)
115+
/// For all others, returns `_lastKnownServerRecordAllFields`.
116+
var allFieldsRecord: CKRecord? {
117+
switch _pendingStatus {
118+
case .reinserted:
119+
lastKnownServerRecord
120+
case nil, .deleted:
121+
_lastKnownServerRecordAllFields
122+
}
123+
}
99124

100125
@Column("hasLastKnownServerRecord", generated: .virtual)
101126
public let _hasLastKnownServerRecord: Bool
@@ -191,7 +216,7 @@
191216
self._hasLastKnownServerRecord = lastKnownServerRecord != nil
192217
self._isShared = share != nil
193218
self.userModificationTime = userModificationTime
194-
self._isDeleted = false
219+
self._pendingStatus = nil
195220
}
196221

197222
package static func find(_ recordID: CKRecord.ID) -> Where<Self> {

Tests/SQLiteDataTests/CloudKitTests/AccountLifecycleTests.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@
170170
│ title: "Personal"
171171
│ ), │
172172
│ share: nil, │
173-
_isDeleted: false,
173+
_pendingStatus: nil,
174174
│ _hasLastKnownServerRecord: true, │
175175
│ _isShared: false, │
176176
│ userModificationTime: 0 │
@@ -192,7 +192,7 @@
192192
│ lastKnownServerRecord: nil, │
193193
│ _lastKnownServerRecordAllFields: nil, │
194194
│ share: nil, │
195-
_isDeleted: false,
195+
_pendingStatus: nil,
196196
│ _hasLastKnownServerRecord: false, │
197197
│ _isShared: false, │
198198
│ userModificationTime: 0 │
@@ -256,7 +256,7 @@
256256
│ title: "Personal"
257257
│ ), │
258258
│ share: nil, │
259-
_isDeleted: false,
259+
_pendingStatus: nil,
260260
│ _hasLastKnownServerRecord: true, │
261261
│ _isShared: false, │
262262
│ userModificationTime: 0 │
@@ -292,7 +292,7 @@
292292
│ title: "Get milk"
293293
│ ), │
294294
│ share: nil, │
295-
_isDeleted: false,
295+
_pendingStatus: nil,
296296
│ _hasLastKnownServerRecord: true, │
297297
│ _isShared: false, │
298298
│ userModificationTime: 0 │
@@ -420,7 +420,7 @@
420420
│ parent: nil, │
421421
│ share: nil │
422422
│ ), │
423-
_isDeleted: false,
423+
_pendingStatus: nil,
424424
│ _hasLastKnownServerRecord: true, │
425425
│ _isShared: true, │
426426
│ userModificationTime: 0 │
@@ -442,7 +442,7 @@
442442
│ lastKnownServerRecord: nil, │
443443
│ _lastKnownServerRecordAllFields: nil, │
444444
│ share: nil, │
445-
_isDeleted: false,
445+
_pendingStatus: nil,
446446
│ _hasLastKnownServerRecord: false, │
447447
│ _isShared: false, │
448448
│ userModificationTime: 0 │
@@ -517,7 +517,7 @@
517517
│ parent: nil, │
518518
│ share: nil │
519519
│ ), │
520-
_isDeleted: false,
520+
_pendingStatus: nil,
521521
│ _hasLastKnownServerRecord: true, │
522522
│ _isShared: true, │
523523
│ userModificationTime: 0 │
@@ -553,7 +553,7 @@
553553
│ title: "Get milk"
554554
│ ), │
555555
│ share: nil, │
556-
_isDeleted: false,
556+
_pendingStatus: nil,
557557
│ _hasLastKnownServerRecord: true, │
558558
│ _isShared: false, │
559559
│ userModificationTime: 0 │

Tests/SQLiteDataTests/CloudKitTests/AttachedMetadatabaseTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
│ │ title: "Personal"
5656
│ │ ), │
5757
│ │ share: nil, │
58-
│ │ _isDeleted: false,
58+
│ │ _pendingStatus: nil,
5959
│ │ _hasLastKnownServerRecord: true, │
6060
│ │ _isShared: false, │
6161
│ │ userModificationTime: 0 │

0 commit comments

Comments
 (0)