Skip to content

Commit 37e20be

Browse files
committed
Fix 'NULL DEFAULT _' migrations.
1 parent 57274d5 commit 37e20be

2 files changed

Lines changed: 81 additions & 4 deletions

File tree

Sources/SQLiteData/CloudKit/SyncEngine.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2003,7 +2003,11 @@
20032003
) async throws -> QueryFragment {
20042004
let nonPrimaryKeyChangedColumns =
20052005
changedColumnNames
2006-
.filter { $0 != T.primaryKey.name }
2006+
.filter {
2007+
$0 != T.primaryKey.name
2008+
&& (record.allKeys().contains($0)
2009+
|| record.encryptedValues.allKeys().contains($0))
2010+
}
20072011
guard
20082012
!nonPrimaryKeyChangedColumns.isEmpty
20092013
else {

Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
try #sql(
219219
"""
220220
ALTER TABLE "remindersLists"
221-
ADD COLUMN "position" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0
221+
ADD COLUMN "position" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 42
222222
"""
223223
)
224224
.execute(db)
@@ -236,6 +236,63 @@
236236
],
237237
privateTables: syncEngine.privateTables
238238
)
239+
240+
try await userDatabase.read { db in
241+
try #expect(
242+
RemindersListWithPosition.fetchAll(db) == [
243+
RemindersListWithPosition(id: 1, title: "Personal", position: 42)
244+
]
245+
)
246+
}
247+
}
248+
249+
/*
250+
* Old schema creates record and synchronizes to iCloud.
251+
* Schema is migrated to add a "NULL DEFAULT _" column.
252+
* New sync engine is launched.
253+
=> Sync starts without emitting an error and default value is persisted in local database.
254+
*/
255+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
256+
@Test func addNullableColumn_OldRecordsSyncToNewSchema() async throws {
257+
let remindersList = RemindersList(id: 1, title: "Personal")
258+
try await userDatabase.userWrite { db in
259+
try db.seed {
260+
remindersList
261+
}
262+
}
263+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
264+
265+
syncEngine.stop()
266+
267+
try await userDatabase.userWrite { db in
268+
try #sql(
269+
"""
270+
ALTER TABLE "remindersLists"
271+
ADD COLUMN "color" INTEGER DEFAULT 42
272+
"""
273+
)
274+
.execute(db)
275+
}
276+
277+
// NB: Sync engine should start without emitting issue.
278+
_ = try await SyncEngine(
279+
container: syncEngine.container,
280+
userDatabase: syncEngine.userDatabase,
281+
tables: syncEngine.tables
282+
.filter { $0.base != RemindersList.self }
283+
+ [
284+
SynchronizedTable(for: RemindersListWithColor.self),
285+
],
286+
privateTables: syncEngine.privateTables
287+
)
288+
289+
try await userDatabase.read { db in
290+
try #expect(
291+
RemindersListWithColor.fetchAll(db) == [
292+
RemindersListWithColor(id: 1, title: "Personal", color: 42)
293+
]
294+
)
295+
}
239296
}
240297

241298
/*
@@ -668,14 +725,30 @@
668725
)
669726
}
670727
}
728+
729+
/*
730+
* Sync engine saves records with old schema.
731+
* Sync engine is relaunched with new schema, adding a 'NOT NULL DEFAULT' column.
732+
=> Defaults are set in local records.
733+
*/
734+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
735+
@Test func addNotNullColumn() async throws {
736+
}
671737
}
672738
}
673739

740+
@Table("remindersLists")
741+
private struct RemindersListWithPosition: Equatable, Identifiable {
742+
let id: Int
743+
var title = ""
744+
var position = 0
745+
}
746+
674747
@Table("remindersLists")
675-
private struct RemindersListWithPosition: Equatable, Identifiable {
748+
private struct RemindersListWithColor: Equatable, Identifiable {
676749
let id: Int
677750
var title = ""
678-
var position = 0
751+
var color: Int?
679752
}
680753

681754
@Table("reminders")

0 commit comments

Comments
 (0)