From 5581705bbeb656b79a73d51dce8166a7f68dbfe9 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 9 Jan 2026 13:15:54 -0600 Subject: [PATCH 1/2] Fix for schema migration involving 'NOT NULL' columns. --- Sources/SQLiteData/CloudKit/SyncEngine.swift | 4 +-- .../CloudKitTests/SchemaChangeTests.swift | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/SyncEngine.swift b/Sources/SQLiteData/CloudKit/SyncEngine.swift index 5d094197..da96ec9d 100644 --- a/Sources/SQLiteData/CloudKit/SyncEngine.swift +++ b/Sources/SQLiteData/CloudKit/SyncEngine.swift @@ -2005,11 +2005,11 @@ if data == nil { reportIssue("Asset data not found on disk") } - return "\(quote: columnName) = \(data?.queryFragment ?? "NULL")" + return "\(quote: columnName) = \(data?.queryFragment ?? #""excluded".\#(quote: columnName)"#)" } else { return """ \(quote: columnName) = \ - \(record.encryptedValues[columnName]?.queryFragment ?? "NULL") + \(record.encryptedValues[columnName]?.queryFragment ?? #""excluded".\#(quote: columnName)"#) """ } } diff --git a/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift index 38315932..4bbcc62e 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift @@ -106,6 +106,42 @@ } } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func addColumn_OldRecordsSyncToNewSchema() async throws { + let remindersList = RemindersList(id: 1, title: "Personal") + try await userDatabase.userWrite { db in + try db.seed { + remindersList + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + syncEngine.stop() + + try await userDatabase.userWrite { db in + try #sql( + """ + ALTER TABLE "remindersLists" + ADD COLUMN "position" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0 + """ + ) + .execute(db) + } + + let relaunchedSyncEngine = try await SyncEngine( + container: syncEngine.container, + userDatabase: syncEngine.userDatabase, + tables: syncEngine.tables + .filter { $0.base != Reminder.self && $0.base != RemindersList.self } + + [ + SynchronizedTable(for: ReminderWithPosition.self), + SynchronizedTable(for: RemindersListWithPosition.self), + ], + privateTables: syncEngine.privateTables + ) + defer { _ = relaunchedSyncEngine } + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func addAssetToRemindersList() async throws { let personalList = RemindersList(id: 1, title: "Personal") From 01daefb43993452cd6fc0214018673500abefd96 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 9 Jan 2026 13:19:36 -0600 Subject: [PATCH 2/2] wip --- .../CloudKitTests/SchemaChangeTests.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift b/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift index 4bbcc62e..f9fc85c6 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift @@ -106,6 +106,12 @@ } } + /* + * Old schema creates record and synchronizes to iCloud. + * Schema is migrated to add a "NOT NULL" column. + * New sync engine is launched. + => Sync starts without emitting an error. + */ @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func addColumn_OldRecordsSyncToNewSchema() async throws { let remindersList = RemindersList(id: 1, title: "Personal") @@ -128,7 +134,8 @@ .execute(db) } - let relaunchedSyncEngine = try await SyncEngine( + // NB: Sync engine should start without emitting issue. + _ = try await SyncEngine( container: syncEngine.container, userDatabase: syncEngine.userDatabase, tables: syncEngine.tables @@ -139,7 +146,6 @@ ], privateTables: syncEngine.privateTables ) - defer { _ = relaunchedSyncEngine } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)