Skip to content

Commit d505fe7

Browse files
committed
Add test to handle data migrations.
1 parent 622c963 commit d505fe7

File tree

2 files changed

+176
-2
lines changed

2 files changed

+176
-2
lines changed

Tests/SQLiteDataTests/Internal/Schema.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ import SQLiteData
7777
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
7878
func database(
7979
containerIdentifier: String,
80-
attachMetadatabase: Bool
80+
attachMetadatabase: Bool,
81+
url databaseURL: URL? = nil
8182
) throws -> DatabasePool {
8283
var configuration = Configuration()
8384
configuration.prepareDatabase { db in
@@ -88,8 +89,11 @@ func database(
8889
// print($0.expandedDescription)
8990
// }
9091
}
91-
let url = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).sqlite")
92+
let url = databaseURL ?? URL.temporaryDirectory.appending(path: "\(UUID().uuidString).sqlite")
9293
let database = try DatabasePool(path: url.path(), configuration: configuration)
94+
guard databaseURL == nil else {
95+
return database
96+
}
9397
try database.write { db in
9498
try #sql(
9599
"""

Tests/SQLiteDataTests/MigrationTests.swift

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)