@@ -33,7 +33,177 @@ import Testing
3333 }
3434}
3535
36+
3637@available ( iOS 15 , * )
3738@Table private struct Model {
3839 var date : Date
3940}
41+
42+
43+ #if canImport(CloudKit)
44+ @available ( iOS 15 , * )
45+ @Table private struct User : Identifiable {
46+ var id : UUID
47+ var name : String
48+ }
49+
50+ @Table ( " users " ) private struct UpdatedUser : Identifiable {
51+ var id : UUID
52+ var name : String
53+ var honorific : String ?
54+ }
55+
56+ import CloudKit
57+ import ConcurrencyExtras
58+ import CustomDump
59+ import InlineSnapshotTesting
60+ import OrderedCollections
61+ import SQLiteData
62+ import SQLiteDataTestSupport
63+ import SnapshotTestingCustomDump
64+ import Testing
65+
66+ @MainActor
67+ @Suite
68+ final class MigrationSyncEngineTests {
69+ @available ( iOS 17 , macOS 14 , tvOS 17 , watchOS 10 , * )
70+
71+ private let _container : any Sendable
72+
73+ var container : MockCloudContainer {
74+ _container as! MockCloudContainer
75+ }
76+
77+ let testContainerIdentifier : String
78+ let databaseURL : URL
79+
80+ func userDatabase( ) throws -> UserDatabase {
81+ UserDatabase (
82+ database: try SQLiteDataTests . database (
83+ containerIdentifier: testContainerIdentifier,
84+ attachMetadatabase: false ,
85+ url: databaseURL
86+ )
87+ )
88+ }
89+
90+
91+ init ( ) async throws {
92+ testContainerIdentifier = " iCloud.co.pointfree.Testing. \( UUID ( ) ) "
93+ databaseURL = URL . temporaryDirectory. appending ( path: " \( UUID ( ) . uuidString) .sqlite " )
94+
95+ let privateDatabase = MockCloudDatabase ( databaseScope: . private)
96+ let sharedDatabase = MockCloudDatabase ( databaseScope: . shared)
97+ let container = MockCloudContainer (
98+ accountStatus: _AccountStatusScope. accountStatus,
99+ containerIdentifier: testContainerIdentifier,
100+ privateCloudDatabase: privateDatabase,
101+ sharedCloudDatabase: sharedDatabase
102+ )
103+ _container = container
104+ privateDatabase. set ( container: container)
105+ sharedDatabase. set ( container: container)
106+ }
107+
108+ @available ( iOS 15 , * )
109+ @Test func handleDataMigration( ) async throws {
110+ let userDatabase = try userDatabase ( )
111+ // Do first migration
112+ try await userDatabase. userWrite { db in
113+ try #sql(
114+ """
115+ CREATE TABLE " users " (
116+ " id " TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT (uuid()),
117+ " name " TEXT NOT NULL
118+ )
119+ """
120+ )
121+ . execute ( db)
122+ }
123+
124+ var syncEngine : Optional < SyncEngine > = try await SyncEngine (
125+ container: container,
126+ userDatabase: userDatabase,
127+ delegate: nil ,
128+ privateTables: User . self,
129+ startImmediately: true
130+ )
131+
132+ let currentUserRecordID = CKRecord . ID (
133+ recordName: " currentUser "
134+ )
135+
136+ await syncEngine!. handleEvent (
137+ . accountChange( changeType: . signIn( currentUser: currentUserRecordID) ) ,
138+ syncEngine: syncEngine!. private
139+ )
140+ await syncEngine!. handleEvent (
141+ . accountChange( changeType: . signIn( currentUser: currentUserRecordID) ) ,
142+ syncEngine: syncEngine!. shared
143+ )
144+ try await syncEngine!. processPendingDatabaseChanges ( scope: . private)
145+
146+ try await userDatabase. userWrite { db in
147+ try User . insert {
148+ User . Draft ( name: " Bob " )
149+ User . Draft ( name: " Alice " )
150+ User . Draft ( name: " Alfred " )
151+ } . execute ( db)
152+ }
153+
154+ // Do we need to await something here?
155+ try await Task . sleep ( for: . seconds( 1 ) )
156+ try await syncEngine? . processPendingRecordZoneChanges ( scope: . private)
157+ syncEngine? . stop ( )
158+ syncEngine = nil
159+
160+ try userDatabase. database. close ( )
161+
162+ let newDbConnection = try self . userDatabase ( )
163+
164+ try await newDbConnection. userWrite { db in
165+ try #sql(
166+ """
167+ ALTER TABLE " users "
168+ ADD COLUMN " honorific " TEXT
169+ """
170+ )
171+ . execute ( db)
172+
173+ let existingUsers = try UpdatedUser . all. fetchAll ( db)
174+ for user in existingUsers {
175+ switch user. name. lowercased ( ) {
176+ case " bob " :
177+ try UpdatedUser . find ( user. id) . update {
178+ $0. honorific = " Mr "
179+ } . execute ( db)
180+ case " alice " :
181+ try UpdatedUser . find ( user. id) . update {
182+ $0. honorific = " Ms "
183+ } . execute ( db)
184+ default :
185+ continue
186+ }
187+ }
188+ }
189+
190+ syncEngine = try await SyncEngine (
191+ container: container,
192+ userDatabase: newDbConnection,
193+ delegate: nil ,
194+ privateTables: UpdatedUser . self,
195+ startImmediately: true
196+ )
197+
198+ let bob = try await newDbConnection. read { db in
199+ try UpdatedUser . all. where { $0. name. eq ( " Bob " ) } . fetchOne ( db)
200+ }
201+ #expect( bob? . honorific == " Mr " )
202+ newDbConnection.
203+ }
204+
205+
206+
207+ // Do we need to wait here...
208+ }
209+ #endif
0 commit comments