Skip to content

Commit 02ddad0

Browse files
diatomingstephencelismbrandonw
authored
Add option to drop unique constraints inside SyncEngine.migratePrimaryKeys (#253)
* drop unique constraints inside SyncEngine.migratePrimaryKeys * Call down to existing helpers * wip * new test and fix --------- Co-authored-by: Stephen Celis <stephen@stephencelis.com> Co-authored-by: Brandon Williams <mbrandonw@hey.com>
1 parent 217e4c9 commit 02ddad0

2 files changed

Lines changed: 212 additions & 18 deletions

File tree

Sources/SQLiteData/CloudKit/PrimaryKeyMigration.swift

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
public static func migratePrimaryKeys<each T: PrimaryKeyedTable>(
5151
_ db: Database,
5252
tables: repeat (each T).Type,
53+
dropUniqueConstraints: Bool = false,
5354
uuid uuidFunction: (any ScalarDatabaseFunction<(), UUID>)? = nil
5455
) throws
5556
where
@@ -82,6 +83,7 @@
8283
for table in repeat each tables {
8384
try table.migratePrimaryKeyToUUID(
8485
db: db,
86+
dropUniqueConstraints: dropUniqueConstraints,
8587
uuidFunction: uuidFunction,
8688
migratedTableNames: migratedTableNames,
8789
salt: salt
@@ -126,6 +128,7 @@
126128
extension PrimaryKeyedTable where TableColumns.PrimaryColumn: TableColumnExpression {
127129
fileprivate static func migratePrimaryKeyToUUID(
128130
db: Database,
131+
dropUniqueConstraints: Bool,
129132
uuidFunction: (any ScalarDatabaseFunction<(), UUID>)? = nil,
130133
migratedTableNames: [String],
131134
salt: String
@@ -164,6 +167,7 @@
164167
let newTableName = "new_\(tableName)"
165168
let uuidFunction = uuidFunction?.name ?? "uuid"
166169
let newSchema = try schema.rewriteSchema(
170+
dropUniqueConstraints: dropUniqueConstraints,
167171
oldPrimaryKey: primaryKeys.first?.name,
168172
newPrimaryKey: columns.primaryKey.name,
169173
foreignKeys: foreignKeys.map(\.from),
@@ -231,13 +235,15 @@
231235

232236
extension String {
233237
func rewriteSchema(
238+
dropUniqueConstraints: Bool,
234239
oldPrimaryKey: String?,
235240
newPrimaryKey: String,
236241
foreignKeys: [String],
237242
uuidFunction: String
238243
) throws -> String {
239244
var substring = self[...]
240245
return try substring.rewriteSchema(
246+
dropUniqueConstraints: dropUniqueConstraints,
241247
oldPrimaryKey: oldPrimaryKey,
242248
newPrimaryKey: newPrimaryKey,
243249
foreignKeys: foreignKeys,
@@ -248,6 +254,7 @@
248254

249255
extension Substring {
250256
mutating func rewriteSchema(
257+
dropUniqueConstraints: Bool,
251258
oldPrimaryKey: String?,
252259
newPrimaryKey: String,
253260
foreignKeys: [String],
@@ -315,15 +322,25 @@
315322
newSchema.append("TEXT")
316323
}
317324
}
325+
326+
if dropUniqueConstraints, columnName != oldPrimaryKey {
327+
if let range = peek({ $0.parseUniqueConstraintRange() }) {
328+
newSchema.append(String(base[index..<range.lowerBound]))
329+
index = range.upperBound
330+
}
331+
}
332+
318333
if try parseToNextColumnDefinitionOrTableConstraint(
319334
skipIf: columnName == oldPrimaryKey
320335
) {
321336
break
322337
}
323338
}
339+
324340
while peek({ $0.parseColumnConstraint() }) {
325341
if try parseToNextColumnDefinitionOrTableConstraint(
326342
skipIf: parseKeywords(["PRIMARY", "KEY"])
343+
|| dropUniqueConstraints && parseKeyword("UNIQUE")
327344
) {
328345
break
329346
}
@@ -338,15 +355,50 @@
338355
return try body(&substring)
339356
}
340357

358+
mutating func parseUniqueConstraintRange() -> Range<String.Index>? {
359+
guard
360+
let constraintEndIndex = try? peek({
361+
try $0.parseBalanced { [",", ")"].contains($0.first) }
362+
return $0.startIndex
363+
})
364+
else { return nil }
365+
366+
var constraint = self[..<constraintEndIndex]
367+
guard
368+
(try? constraint.parseBalanced(upTo: {
369+
$0.peek {
370+
$0.parseKeyword("UNIQUE")
371+
}
372+
})) != nil
373+
else { return nil }
374+
let startIndex = constraint.startIndex
375+
guard constraint.parseKeyword("UNIQUE") else { return nil }
376+
if constraint.parseKeywords(["ON", "CONFLICT"]) {
377+
guard
378+
["ABORT", "FAIL", "IGNORE", "REPLACE", "ROLLBACK"].contains(
379+
where: { constraint.parseKeyword($0) }
380+
)
381+
else { return nil }
382+
}
383+
return startIndex..<constraint.startIndex
384+
}
385+
341386
mutating func parseBalanced(upTo endCharacter: Character = ",") throws {
387+
try parseBalanced {
388+
$0.parseTrivia()
389+
return $0.first == endCharacter
390+
}
391+
}
392+
393+
mutating func parseBalanced(upTo predicate: (inout Substring) throws -> Bool) throws {
342394
let substring = self
343-
parseTrivia()
344395
var parenDepth = 0
345-
while let character = first {
346-
defer { parseTrivia() }
347-
switch character {
348-
case endCharacter where parenDepth == 0:
396+
loop: while !isEmpty {
397+
if parenDepth == 0, try predicate(&self) {
349398
return
399+
}
400+
401+
switch first {
350402
case "(":
351403
parenDepth += 1
352404
removeFirst()
@@ -357,6 +409,8 @@
357409
_ = try parseIdentifier()
358410
case "'":
359411
_ = try parseText()
412+
case nil:
413+
break loop
360414
default:
361415
removeFirst()
362416
continue
@@ -382,7 +436,7 @@
382436
|| parseKeyword("CHECK")
383437
|| parseKeywords(["FOREIGN", "KEY"])
384438
{
385-
try parseBalanced(upTo: ",")
439+
try parseBalanced { [",", ")"].contains($0.first) }
386440
return true
387441
} else {
388442
return false

Tests/SQLiteDataTests/PrimaryKeyMigrationTests.swift

Lines changed: 152 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,11 @@ struct PrimaryKeyMigrationTests {
197197
"""
198198
)
199199
.execute(db)
200-
try #sql("""
200+
try #sql(
201+
"""
201202
INSERT INTO "parents" ("id", "title") VALUES (1, 'blob'), (1000, 'blob jr')
202-
""")
203+
"""
204+
)
203205
.execute(db)
204206
}
205207

@@ -264,6 +266,135 @@ struct PrimaryKeyMigrationTests {
264266
}
265267
}
266268

269+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
270+
@Test func dropUniqueConstraints() throws {
271+
try database.write { db in
272+
try #sql(
273+
"""
274+
CREATE TABLE "users" (
275+
"id" INTEGER,
276+
"title" TEXT NOT NULL,
277+
278+
PRIMARY KEY("id"),
279+
UNIQUE("title") ON CONFLICT REPLACE
280+
) STRICT
281+
"""
282+
)
283+
.execute(db)
284+
try #sql(
285+
"""
286+
CREATE TABLE "tags" (
287+
"title" TEXT NOT NULL UNIQUE,
288+
"name" TEXT NOT NULL UNIQUE ON CONFLICT IGNORE
289+
) STRICT
290+
"""
291+
)
292+
.execute(db)
293+
}
294+
295+
try database.writeWithoutTransaction { db in
296+
try #sql("PRAGMA foreign_keys = OFF").execute(db)
297+
do {
298+
try db.inTransaction {
299+
try SyncEngine.migratePrimaryKeys(
300+
db,
301+
tables: User.self,
302+
Tag.self,
303+
dropUniqueConstraints: true,
304+
uuid: $uuid
305+
)
306+
return .commit
307+
}
308+
}
309+
try #sql("PRAGMA foreign_keys = ON").execute(db)
310+
}
311+
312+
assertQuery(SQLiteSchema.default, database: database) {
313+
#"""
314+
┌────────────────────────────────────────────────────────────────────────────┐
315+
│ SQLiteSchema( │
316+
│ type: .table, │
317+
│ name: "tags", │
318+
│ tableName: "tags", │
319+
│ sql: """ │
320+
│ CREATE TABLE "tags" ( │
321+
│ "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT ("uuid"()), │
322+
│ "title" TEXT NOT NULL, │
323+
│ "name" TEXT NOT NULL │
324+
│ ) STRICT │
325+
│ """ │
326+
│ ) │
327+
├────────────────────────────────────────────────────────────────────────────┤
328+
│ SQLiteSchema( │
329+
│ type: .table, │
330+
│ name: "users", │
331+
│ tableName: "users", │
332+
│ sql: """ │
333+
│ CREATE TABLE "users" ( │
334+
│ "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT ("uuid"()), │
335+
│ "title" TEXT NOT NULL) STRICT │
336+
│ """ │
337+
│ ) │
338+
└────────────────────────────────────────────────────────────────────────────┘
339+
"""#
340+
}
341+
}
342+
343+
344+
@Table("users") struct PrimaryKeyNamedUnique {
345+
@Column(primaryKey: true)
346+
let unique: UUID
347+
var title = ""
348+
}
349+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
350+
@Test func primaryKeyNamedUnique() throws {
351+
try database.write { db in
352+
try #sql(
353+
"""
354+
CREATE TABLE "users" (
355+
"unique" INTEGER,
356+
"title" TEXT NOT NULL,
357+
PRIMARY KEY("unique")
358+
) STRICT
359+
"""
360+
)
361+
.execute(db)
362+
}
363+
364+
try database.writeWithoutTransaction { db in
365+
try #sql("PRAGMA foreign_keys = OFF").execute(db)
366+
do {
367+
try db.inTransaction {
368+
try SyncEngine.migratePrimaryKeys(
369+
db,
370+
tables: PrimaryKeyNamedUnique.self,
371+
dropUniqueConstraints: true,
372+
uuid: $uuid
373+
)
374+
return .commit
375+
}
376+
}
377+
try #sql("PRAGMA foreign_keys = ON").execute(db)
378+
}
379+
380+
assertQuery(SQLiteSchema.default, database: database) {
381+
#"""
382+
┌────────────────────────────────────────────────────────────────────────────────┐
383+
│ SQLiteSchema( │
384+
│ type: .table, │
385+
│ name: "users", │
386+
│ tableName: "users", │
387+
│ sql: """ │
388+
│ CREATE TABLE "users" ( │
389+
│ "unique" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT ("uuid"()), │
390+
│ "title" TEXT NOT NULL) STRICT │
391+
│ """ │
392+
│ ) │
393+
└────────────────────────────────────────────────────────────────────────────────┘
394+
"""#
395+
}
396+
}
397+
267398
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
268399
@Test func columnConstraints() throws {
269400
try database.write { db in
@@ -698,11 +829,13 @@ struct PrimaryKeyMigrationTests {
698829
"""
699830
)
700831
.execute(db)
701-
try #sql("""
832+
try #sql(
833+
"""
702834
INSERT INTO "phoneNumbers"
703835
VALUES
704836
('212-555-1234')
705-
""")
837+
"""
838+
)
706839
.execute(db)
707840
}
708841

@@ -747,7 +880,7 @@ struct PrimaryKeyMigrationTests {
747880
CREATE TABLE "parents" (
748881
"id" INTEGER,
749882
"title" TEXT NOT NULL,
750-
883+
751884
PRIMARY KEY("id", "title")
752885
) STRICT
753886
"""
@@ -776,11 +909,13 @@ struct PrimaryKeyMigrationTests {
776909
"""
777910
)
778911
.execute(db)
779-
try #sql("""
912+
try #sql(
913+
"""
780914
INSERT INTO "users"
781915
VALUES
782916
('blob'), ('blob jr'), ('blob sr')
783-
""")
917+
"""
918+
)
784919
.execute(db)
785920
}
786921

@@ -826,15 +961,19 @@ struct PrimaryKeyMigrationTests {
826961
"""
827962
)
828963
.execute(db)
829-
try #sql("""
964+
try #sql(
965+
"""
830966
CREATE INDEX "parents_name" ON "parents"("title")
831-
""")
967+
"""
968+
)
832969
.execute(db)
833-
try #sql("""
970+
try #sql(
971+
"""
834972
CREATE TRIGGER "parents_trigger" AFTER UPDATE ON "parents" BEGIN
835973
SELECT 1;
836974
END
837-
""")
975+
"""
976+
)
838977
.execute(db)
839978
}
840979

@@ -1078,7 +1217,8 @@ struct PrimaryKeyMigrationTests {
10781217
@DatabaseFunction private func customUUID() -> UUID { DependencyValues._current.uuid() }
10791218
10801219
extension SQLiteSchema {
1081-
static let `default` = Self
1220+
static let `default` =
1221+
Self
10821222
.where { !$0.name.hasPrefix("sqlite_") }
10831223
.order(by: \.name)
10841224
}

0 commit comments

Comments
 (0)