Skip to content

Commit b206f0b

Browse files
authored
Adding more test coverage on schema changes. (#361)
1 parent 5a05b22 commit b206f0b

1 file changed

Lines changed: 337 additions & 0 deletions

File tree

Tests/SQLiteDataTests/CloudKitTests/SchemaChangeTests.swift

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import Foundation
55
import InlineSnapshotTesting
66
import SQLiteData
7+
import SQLiteDataTestSupport
78
import SnapshotTestingCustomDump
89
import Testing
910

@@ -106,6 +107,342 @@
106107
}
107108
}
108109

110+
/*
111+
* Test run from perspective of old device with old schema.
112+
* New schema saves record in cloud database.
113+
* Record syncs to old device with old schema.
114+
* Old device edits record without access to new schema.
115+
=> All data (new+old schema) is sync'd to cloud database.
116+
*/
117+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
118+
@Test func oldSchemaUpdatesNewSchemaRecord() async throws {
119+
let remindersListRecord = CKRecord(
120+
recordType: RemindersList.tableName,
121+
recordID: RemindersList.recordID(for: 1)
122+
)
123+
remindersListRecord.setValue(1, forKey: "id", at: now)
124+
remindersListRecord.setValue("Personal", forKey: "title", at: now)
125+
remindersListRecord.setValue(42, forKey: "position", at: 0)
126+
127+
try await syncEngine.modifyRecords(scope: .private, saving: [remindersListRecord]).notify()
128+
129+
try await userDatabase.userWrite { db in
130+
try #expect(RemindersList.fetchCount(db) == 1)
131+
try #expect(RemindersList.find(1).fetchOne(db) == RemindersList(id: 1, title: "Personal"))
132+
try RemindersList.find(1).update { $0.title = "My Stuff" }.execute(db)
133+
}
134+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
135+
136+
assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) {
137+
"""
138+
┌────────────────────────────────────────────────────────────────────┐
139+
│ SyncMetadata( │
140+
│ id: SyncMetadata.ID( │
141+
│ recordPrimaryKey: "1", │
142+
│ recordType: "remindersLists"
143+
│ ), │
144+
│ zoneName: "zone", │
145+
│ ownerName: "__defaultOwner__", │
146+
│ recordName: "1:remindersLists", │
147+
│ parentRecordID: nil, │
148+
│ parentRecordName: nil, │
149+
│ lastKnownServerRecord: CKRecord( │
150+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
151+
│ recordType: "remindersLists", │
152+
│ parent: nil, │
153+
│ share: nil │
154+
│ ), │
155+
│ _lastKnownServerRecordAllFields: CKRecord( │
156+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
157+
│ recordType: "remindersLists", │
158+
│ parent: nil, │
159+
│ share: nil, │
160+
│ id: 1, │
161+
│ position: 42, │
162+
│ title: "My Stuff"
163+
│ ), │
164+
│ share: nil, │
165+
│ _isDeleted: false, │
166+
│ hasLastKnownServerRecord: true, │
167+
│ isShared: false, │
168+
│ userModificationTime: 0 │
169+
│ ) │
170+
└────────────────────────────────────────────────────────────────────┘
171+
"""
172+
}
173+
assertInlineSnapshot(of: syncEngine.container, as: .customDump) {
174+
"""
175+
MockCloudContainer(
176+
privateCloudDatabase: MockCloudDatabase(
177+
databaseScope: .private,
178+
storage: [
179+
[0]: CKRecord(
180+
recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__),
181+
recordType: "remindersLists",
182+
parent: nil,
183+
share: nil,
184+
id: 1,
185+
position: 42,
186+
title: "My Stuff"
187+
)
188+
]
189+
),
190+
sharedCloudDatabase: MockCloudDatabase(
191+
databaseScope: .shared,
192+
storage: []
193+
)
194+
)
195+
"""
196+
}
197+
}
198+
199+
/*
200+
* Test run from perspective of old device with old schema.
201+
* Old schema saves record in cloud database.
202+
* New device with new schema saves record with extra fields.
203+
=> All data (new+old schema) is sync'd to old device with old schema.
204+
*/
205+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
206+
@Test func newSchemaUpdatesOldSchemaRecord() async throws {
207+
let remindersList = RemindersList(id: 1, title: "Personal")
208+
try await userDatabase.userWrite { db in
209+
try db.seed { remindersList }
210+
}
211+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
212+
213+
let remindersListRecord = try syncEngine.private.database.record(
214+
for: RemindersList.recordID(for: 1)
215+
)
216+
remindersListRecord.setValue("My Stuff", forKey: "title", at: 1)
217+
remindersListRecord.setValue(42, forKey: "position", at: 1)
218+
try await syncEngine.modifyRecords(scope: .private, saving: [remindersListRecord]).notify()
219+
220+
try await userDatabase.read { db in
221+
try #expect(RemindersList.find(1).fetchOne(db) == RemindersList(id: 1, title: "My Stuff"))
222+
}
223+
224+
assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) {
225+
"""
226+
┌────────────────────────────────────────────────────────────────────┐
227+
│ SyncMetadata( │
228+
│ id: SyncMetadata.ID( │
229+
│ recordPrimaryKey: "1", │
230+
│ recordType: "remindersLists"
231+
│ ), │
232+
│ zoneName: "zone", │
233+
│ ownerName: "__defaultOwner__", │
234+
│ recordName: "1:remindersLists", │
235+
│ parentRecordID: nil, │
236+
│ parentRecordName: nil, │
237+
│ lastKnownServerRecord: CKRecord( │
238+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
239+
│ recordType: "remindersLists", │
240+
│ parent: nil, │
241+
│ share: nil │
242+
│ ), │
243+
│ _lastKnownServerRecordAllFields: CKRecord( │
244+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
245+
│ recordType: "remindersLists", │
246+
│ parent: nil, │
247+
│ share: nil, │
248+
│ id: 1, │
249+
│ position: 42, │
250+
│ title: "My Stuff"
251+
│ ), │
252+
│ share: nil, │
253+
│ _isDeleted: false, │
254+
│ hasLastKnownServerRecord: true, │
255+
│ isShared: false, │
256+
│ userModificationTime: 1 │
257+
│ ) │
258+
└────────────────────────────────────────────────────────────────────┘
259+
"""
260+
}
261+
assertInlineSnapshot(of: syncEngine.container, as: .customDump) {
262+
"""
263+
MockCloudContainer(
264+
privateCloudDatabase: MockCloudDatabase(
265+
databaseScope: .private,
266+
storage: [
267+
[0]: CKRecord(
268+
recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__),
269+
recordType: "remindersLists",
270+
parent: nil,
271+
share: nil,
272+
id: 1,
273+
position: 42,
274+
title: "My Stuff"
275+
)
276+
]
277+
),
278+
sharedCloudDatabase: MockCloudDatabase(
279+
databaseScope: .shared,
280+
storage: []
281+
)
282+
)
283+
"""
284+
}
285+
}
286+
287+
/*
288+
* Test run from perspective of new device with new schema.
289+
* Old schema saves record in cloud database.
290+
=> Data syncs new to new device with new schema.
291+
* New device updates record.
292+
=> Data syncs new to cloud database.
293+
*/
294+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
295+
@Test func runWithNewSchema_oldSchemaSavesRecord_NewSchemaUpdatesRecord() async throws {
296+
syncEngine.stop()
297+
try syncEngine.tearDownSyncEngine()
298+
299+
try await userDatabase.userWrite { db in
300+
try #sql(
301+
"""
302+
ALTER TABLE "remindersLists"
303+
ADD COLUMN "position" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0
304+
"""
305+
)
306+
.execute(db)
307+
}
308+
let newSyncEngine = try await SyncEngine(
309+
container: syncEngine.container,
310+
userDatabase: syncEngine.userDatabase,
311+
tables: syncEngine.tables
312+
.filter { $0.base != RemindersList.self }
313+
+ [
314+
SynchronizedTable(for: RemindersListWithPosition.self),
315+
],
316+
privateTables: syncEngine.privateTables
317+
)
318+
defer { _ = newSyncEngine }
319+
320+
let remindersListRecord = CKRecord(
321+
recordType: RemindersList.tableName,
322+
recordID: RemindersList.recordID(for: 1)
323+
)
324+
remindersListRecord.setValue(1, forKey: "id", at: now)
325+
remindersListRecord.setValue("Personal", forKey: "title", at: now)
326+
327+
try await newSyncEngine.modifyRecords(scope: .private, saving: [remindersListRecord])
328+
.notify()
329+
330+
try await userDatabase.read { db in
331+
try #expect(
332+
RemindersListWithPosition.find(1).fetchOne(db)
333+
== RemindersListWithPosition(id: 1, title: "Personal", position: 0)
334+
)
335+
}
336+
337+
assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) {
338+
"""
339+
┌────────────────────────────────────────────────────────────────────┐
340+
│ SyncMetadata( │
341+
│ id: SyncMetadata.ID( │
342+
│ recordPrimaryKey: "1", │
343+
│ recordType: "remindersLists"
344+
│ ), │
345+
│ zoneName: "zone", │
346+
│ ownerName: "__defaultOwner__", │
347+
│ recordName: "1:remindersLists", │
348+
│ parentRecordID: nil, │
349+
│ parentRecordName: nil, │
350+
│ lastKnownServerRecord: CKRecord( │
351+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
352+
│ recordType: "remindersLists", │
353+
│ parent: nil, │
354+
│ share: nil │
355+
│ ), │
356+
│ _lastKnownServerRecordAllFields: CKRecord( │
357+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
358+
│ recordType: "remindersLists", │
359+
│ parent: nil, │
360+
│ share: nil, │
361+
│ id: 1, │
362+
│ title: "Personal"
363+
│ ), │
364+
│ share: nil, │
365+
│ _isDeleted: false, │
366+
│ hasLastKnownServerRecord: true, │
367+
│ isShared: false, │
368+
│ userModificationTime: 0 │
369+
│ ) │
370+
└────────────────────────────────────────────────────────────────────┘
371+
"""
372+
}
373+
374+
try await userDatabase.userWrite { db in
375+
try RemindersListWithPosition.find(1).update {
376+
$0.title = "My Stuff"
377+
$0.position = 42
378+
}
379+
.execute(db)
380+
}
381+
try await newSyncEngine.processPendingRecordZoneChanges(scope: .private)
382+
383+
assertQuery(SyncMetadata.all, database: syncEngine.metadatabase) {
384+
"""
385+
┌────────────────────────────────────────────────────────────────────┐
386+
│ SyncMetadata( │
387+
│ id: SyncMetadata.ID( │
388+
│ recordPrimaryKey: "1", │
389+
│ recordType: "remindersLists"
390+
│ ), │
391+
│ zoneName: "zone", │
392+
│ ownerName: "__defaultOwner__", │
393+
│ recordName: "1:remindersLists", │
394+
│ parentRecordID: nil, │
395+
│ parentRecordName: nil, │
396+
│ lastKnownServerRecord: CKRecord( │
397+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
398+
│ recordType: "remindersLists", │
399+
│ parent: nil, │
400+
│ share: nil │
401+
│ ), │
402+
│ _lastKnownServerRecordAllFields: CKRecord( │
403+
│ recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), │
404+
│ recordType: "remindersLists", │
405+
│ parent: nil, │
406+
│ share: nil, │
407+
│ id: 1, │
408+
│ position: 42, │
409+
│ title: "My Stuff"
410+
│ ), │
411+
│ share: nil, │
412+
│ _isDeleted: false, │
413+
│ hasLastKnownServerRecord: true, │
414+
│ isShared: false, │
415+
│ userModificationTime: 0 │
416+
│ ) │
417+
└────────────────────────────────────────────────────────────────────┘
418+
"""
419+
}
420+
assertInlineSnapshot(of: syncEngine.container, as: .customDump) {
421+
"""
422+
MockCloudContainer(
423+
privateCloudDatabase: MockCloudDatabase(
424+
databaseScope: .private,
425+
storage: [
426+
[0]: CKRecord(
427+
recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__),
428+
recordType: "remindersLists",
429+
parent: nil,
430+
share: nil,
431+
id: 1,
432+
position: 42,
433+
title: "My Stuff"
434+
)
435+
]
436+
),
437+
sharedCloudDatabase: MockCloudDatabase(
438+
databaseScope: .shared,
439+
storage: []
440+
)
441+
)
442+
"""
443+
}
444+
}
445+
109446
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
110447
@Test func addAssetToRemindersList() async throws {
111448
let personalList = RemindersList(id: 1, title: "Personal")

0 commit comments

Comments
 (0)