From 9ea5bdd88b79d7c9db88021e568c8a66789a21fa Mon Sep 17 00:00:00 2001 From: James Sutula Date: Sat, 7 Mar 2026 19:55:28 -0800 Subject: [PATCH] Setup to repro CloudKit data loss on delete-then-reinsert --- .../CloudKitDemo/CountersListFeature.swift | 47 +++++++++++++++++-- Examples/CloudKitDemo/Schema.swift | 20 +++++++- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/Examples/CloudKitDemo/CountersListFeature.swift b/Examples/CloudKitDemo/CountersListFeature.swift index ae9e0de0..bac46b9a 100644 --- a/Examples/CloudKitDemo/CountersListFeature.swift +++ b/Examples/CloudKitDemo/CountersListFeature.swift @@ -2,12 +2,15 @@ import CloudKit import SQLiteData import SwiftUI +let initialAssetSize = 10 + struct CountersListView: View { @FetchAll( Counter - .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($1.id) } + .leftJoin(CounterAsset.all) { $0.id.eq($1.counterID) } + .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) } .select { - Row.Columns(counter: $0, isShared: $1.isShared.ifnull(false)) + Row.Columns(counter: $0, counterAsset: $1, isShared: $2.isShared.ifnull(false)) } ) var rows @Dependency(\.defaultDatabase) var database @@ -15,6 +18,7 @@ struct CountersListView: View { @Selection struct Row { let counter: Counter + let counterAsset: CounterAsset? let isShared: Bool } @@ -39,10 +43,13 @@ struct CountersListView: View { Task { withErrorReporting { try database.write { db in - try Counter.insert { + let counterID = try Counter.insert { Counter.Draft() - } - .execute(db) + }.returning(\.id) + .fetchOne(db)! + try CounterAsset.insert { + CounterAsset(counterID: counterID, assetData: Data(count: initialAssetSize)) + }.execute(db) } } } @@ -66,6 +73,7 @@ struct CountersListView: View { struct CounterRow: View { let row: CountersListView.Row @State var sharedRecord: SharedRecord? + @State var updateAssetCount = 0 @Dependency(\.defaultDatabase) var database @Dependency(\.defaultSyncEngine) var syncEngine @@ -83,6 +91,16 @@ struct CounterRow: View { incrementButtonTapped() } Spacer() + if let assetData = row.counterAsset?.assetData { + Text("Asset: \(assetData.count) bytes") + } else { + Text("").foregroundStyle(.red) + } + Spacer() + Button("Update asset") { + updateAssetCount += 1 + updateAssetButtonTapped() + } Button { shareButtonTapped() } label: { @@ -124,6 +142,25 @@ struct CounterRow: View { } } } + + func updateAssetButtonTapped() { + withErrorReporting { + let sizeInBytes = initialAssetSize + updateAssetCount + let assetData = Data(count: sizeInBytes) + try database.write { db in + // This delete isn't strictly necessary, but it's + // what causes the data loss + try CounterAsset + .where { $0.counterID.eq(row.counter.id) } + .delete() + .execute(db) + + try CounterAsset.upsert { + CounterAsset(counterID: row.counter.id, assetData: assetData) + }.execute(db) + } + } + } } #Preview { diff --git a/Examples/CloudKitDemo/Schema.swift b/Examples/CloudKitDemo/Schema.swift index b4194cc1..19206655 100644 --- a/Examples/CloudKitDemo/Schema.swift +++ b/Examples/CloudKitDemo/Schema.swift @@ -8,6 +8,13 @@ nonisolated struct Counter: Identifiable { var count = 0 } +@Table +nonisolated struct CounterAsset: Identifiable { + @Column(primaryKey: true) public let counterID: Counter.ID + public var id: Counter.ID { counterID } + public var assetData: Data +} + extension DependencyValues { mutating func bootstrapDatabase() throws { var configuration = Configuration() @@ -37,11 +44,22 @@ extension DependencyValues { ) .execute(db) } + migrator.registerMigration("Add CounterAsset") { db in + try #sql( + """ + CREATE TABLE "counterAssets" ( + "counterID" TEXT PRIMARY KEY NOT NULL, + "assetData" BLOB NOT NULL DEFAULT (x'') + ) STRICT + """ + ) + .execute(db) + } try migrator.migrate(database) defaultDatabase = database defaultSyncEngine = try SyncEngine( for: defaultDatabase, - tables: Counter.self + tables: Counter.self, CounterAsset.self ) } }