Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/Tests/GRDBTests/DatabaseMigratorTests.swift
T
2025-01-28 12:28:03 +08:00

1043 lines
44 KiB
Swift

import XCTest
import GRDB
class DatabaseMigratorTests : GRDBTestCase {
// Test passes if it compiles.
// See <https://github.com/groue/GRDB.swift/issues/1541>
func testMigrateAnyDatabaseWriter(writer: any DatabaseWriter) throws {
let migrator = DatabaseMigrator()
try migrator.migrate(writer)
}
func testEmptyMigratorSync() throws {
func test(writer: some DatabaseWriter) throws {
let migrator = DatabaseMigrator()
try migrator.migrate(writer)
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testEmptyMigratorAsync() throws {
func test(writer: some DatabaseWriter) throws {
let expectation = self.expectation(description: "")
let migrator = DatabaseMigrator()
migrator.asyncMigrate(writer, completion: { dbResult in
// No migration error
let db = try! dbResult.get()
// Write access
try! db.execute(sql: "CREATE TABLE t(a)")
expectation.fulfill()
})
waitForExpectations(timeout: 5, handler: nil)
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testEmptyMigratorPublisher() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
let migrator = DatabaseMigrator()
let publisher = migrator.migratePublisher(writer)
let recorder = publisher.record()
try wait(for: recorder.single, timeout: 1)
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testNonEmptyMigratorSync() throws {
func test(writer: some DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("createPersons") { db in
try db.execute(sql: """
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
name TEXT)
""")
}
migrator.registerMigration("createPets") { db in
try db.execute(sql: """
CREATE TABLE pets (
id INTEGER PRIMARY KEY,
masterID INTEGER NOT NULL
REFERENCES persons(id)
ON DELETE CASCADE ON UPDATE CASCADE,
name TEXT)
""")
}
var migrator2 = migrator
migrator2.registerMigration("destroyPersons") { db in
try db.execute(sql: "DROP TABLE pets")
}
try migrator.migrate(writer)
try writer.read { db in
XCTAssertTrue(try db.tableExists("persons"))
XCTAssertTrue(try db.tableExists("pets"))
}
try migrator2.migrate(writer)
try writer.read { db in
XCTAssertTrue(try db.tableExists("persons"))
XCTAssertFalse(try db.tableExists("pets"))
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testNonEmptyMigratorAsync() throws {
func test(writer: some DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("createPersons") { db in
try db.execute(sql: """
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
name TEXT)
""")
}
migrator.registerMigration("createPets") { db in
try db.execute(sql: """
CREATE TABLE pets (
id INTEGER PRIMARY KEY,
masterID INTEGER NOT NULL
REFERENCES persons(id)
ON DELETE CASCADE ON UPDATE CASCADE,
name TEXT)
""")
}
var migrator2 = migrator
migrator2.registerMigration("destroyPersons") { db in
try db.execute(sql: "DROP TABLE pets")
}
let expectation = self.expectation(description: "")
migrator.asyncMigrate(writer, completion: { dbResult in
// No migration error
let db = try! dbResult.get()
XCTAssertTrue(try! db.tableExists("persons"))
XCTAssertTrue(try! db.tableExists("pets"))
migrator2.asyncMigrate(writer, completion: { dbResult in
// No migration error
let db = try! dbResult.get()
XCTAssertTrue(try! db.tableExists("persons"))
XCTAssertFalse(try! db.tableExists("pets"))
expectation.fulfill()
})
})
waitForExpectations(timeout: 5, handler: nil)
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testNonEmptyMigratorPublisher() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("createPersons") { db in
try db.execute(sql: """
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
name TEXT)
""")
}
migrator.registerMigration("createPets") { db in
try db.execute(sql: """
CREATE TABLE pets (
id INTEGER PRIMARY KEY,
masterID INTEGER NOT NULL
REFERENCES persons(id)
ON DELETE CASCADE ON UPDATE CASCADE,
name TEXT)
""")
}
var migrator2 = migrator
migrator2.registerMigration("destroyPersons") { db in
try db.execute(sql: "DROP TABLE pets")
}
do {
let publisher = migrator.migratePublisher(writer)
let recorder = publisher.record()
try wait(for: recorder.single, timeout: 1)
try writer.read { db in
XCTAssertTrue(try db.tableExists("persons"))
XCTAssertTrue(try db.tableExists("pets"))
}
}
do {
let publisher = migrator2.migratePublisher(writer)
let recorder = publisher.record()
try wait(for: recorder.single, timeout: 1)
try writer.read { db in
XCTAssertTrue(try db.tableExists("persons"))
XCTAssertFalse(try db.tableExists("pets"))
}
}
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testEmptyMigratorPublisherIsAsynchronous() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
let migrator = DatabaseMigrator()
let expectation = self.expectation(description: "")
let semaphore = DispatchSemaphore(value: 0)
let cancellable = migrator.migratePublisher(writer).sink(
receiveCompletion: { _ in },
receiveValue: { _ in
semaphore.wait()
expectation.fulfill()
})
semaphore.signal()
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testNonEmptyMigratorPublisherIsAsynchronous() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test(writer: some DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("first", migrate: { _ in })
let expectation = self.expectation(description: "")
let semaphore = DispatchSemaphore(value: 0)
let cancellable = migrator.migratePublisher(writer).sink(
receiveCompletion: { _ in },
receiveValue: { _ in
semaphore.wait()
expectation.fulfill()
})
semaphore.signal()
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testMigratorPublisherDefaultScheduler() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test<Writer: DatabaseWriter>(writer: Writer) {
var migrator = DatabaseMigrator()
migrator.registerMigration("first", migrate: { _ in })
let expectation = self.expectation(description: "")
expectation.expectedFulfillmentCount = 2 // value + completion
let cancellable = migrator.migratePublisher(writer).sink(
receiveCompletion: { completion in
dispatchPrecondition(condition: .onQueue(.main))
expectation.fulfill()
},
receiveValue: { _ in
dispatchPrecondition(condition: .onQueue(.main))
expectation.fulfill()
})
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testMigratorPublisherCustomScheduler() throws {
guard #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) else {
throw XCTSkip("Combine is not available")
}
func test<Writer: DatabaseWriter>(writer: Writer) {
var migrator = DatabaseMigrator()
migrator.registerMigration("first", migrate: { _ in })
let queue = DispatchQueue(label: "test")
let expectation = self.expectation(description: "")
expectation.expectedFulfillmentCount = 2 // value + completion
let cancellable = migrator.migratePublisher(writer, receiveOn: queue).sink(
receiveCompletion: { completion in
dispatchPrecondition(condition: .onQueue(queue))
expectation.fulfill()
},
receiveValue: { _ in
dispatchPrecondition(condition: .onQueue(queue))
expectation.fulfill()
})
waitForExpectations(timeout: 5, handler: nil)
cancellable.cancel()
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testMigrateUpTo() throws {
func test(writer: some DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("a") { db in
try db.execute(sql: "CREATE TABLE a (id INTEGER PRIMARY KEY)")
}
migrator.registerMigration("b") { db in
try db.execute(sql: "CREATE TABLE b (id INTEGER PRIMARY KEY)")
}
migrator.registerMigration("c") { db in
try db.execute(sql: "CREATE TABLE c (id INTEGER PRIMARY KEY)")
}
// one step
try migrator.migrate(writer, upTo: "a")
try writer.read { db in
XCTAssertTrue(try db.tableExists("a"))
XCTAssertFalse(try db.tableExists("b"))
}
// zero step
try migrator.migrate(writer, upTo: "a")
try writer.read { db in
XCTAssertTrue(try db.tableExists("a"))
XCTAssertFalse(try db.tableExists("b"))
}
// two steps
try migrator.migrate(writer, upTo: "c")
try writer.read { db in
XCTAssertTrue(try db.tableExists("a"))
XCTAssertTrue(try db.tableExists("b"))
XCTAssertTrue(try db.tableExists("c"))
}
// zero step
try migrator.migrate(writer, upTo: "c")
try migrator.migrate(writer)
// fatal error: undefined migration: "missing"
// try migrator.migrate(writer, upTo: "missing")
// fatal error: database is already migrated beyond migration "b"
// try migrator.migrate(writer, upTo: "b")
}
try Test(test).run { try DatabaseQueue() }
try Test(test).runAtTemporaryDatabasePath { try DatabaseQueue(path: $0) }
try Test(test).runAtTemporaryDatabasePath { try DatabasePool(path: $0) }
}
func testMigrationFailureTriggersRollback() throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("createPersons") { db in
try db.execute(sql: "CREATE TABLE persons (id INTEGER PRIMARY KEY, name TEXT)")
try db.execute(sql: "CREATE TABLE pets (masterId INTEGER NOT NULL REFERENCES persons(id), name TEXT)")
try db.execute(sql: "INSERT INTO persons (name) VALUES ('Arthur')")
}
migrator.registerMigration("foreignKeyError") { db in
try db.execute(sql: "INSERT INTO persons (name) VALUES ('Barbara')")
// triggers foreign key error:
try db.execute(sql: "INSERT INTO pets (masterId, name) VALUES (?, ?)", arguments: [123, "Bobby"])
}
// Sync
do {
let dbQueue = try makeDatabaseQueue()
do {
try migrator.migrate(dbQueue)
XCTFail("Expected error")
} catch let error as DatabaseError {
// The first migration should be committed.
// The second migration should be rollbacked.
XCTAssertEqual(error.extendedResultCode, .SQLITE_CONSTRAINT_FOREIGNKEY)
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
XCTAssertEqual(error.message, #"FOREIGN KEY constraint violation - from pets(masterId) to persons(id), in [masterId:123 name:"Bobby"]"#)
let names = try dbQueue.inDatabase { db in
try String.fetchAll(db, sql: "SELECT name FROM persons")
}
XCTAssertEqual(names, ["Arthur"])
}
}
// Async
do {
let expectation = self.expectation(description: "")
let dbQueue = try makeDatabaseQueue()
migrator.asyncMigrate(dbQueue, completion: { dbResult in
// The first migration should be committed.
// The second migration should be rollbacked.
guard case let .failure(error as DatabaseError) = dbResult else {
XCTFail("Expected DatabaseError")
expectation.fulfill()
return
}
XCTAssertEqual(error.extendedResultCode, .SQLITE_CONSTRAINT_FOREIGNKEY)
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
XCTAssertEqual(error.message, #"FOREIGN KEY constraint violation - from pets(masterId) to persons(id), in [masterId:123 name:"Bobby"]"#)
dbQueue.asyncRead { dbResult in
let names = try! String.fetchAll(dbResult.get(), sql: "SELECT name FROM persons")
XCTAssertEqual(names, ["Arthur"])
expectation.fulfill()
}
})
waitForExpectations(timeout: 5, handler: nil)
}
}
func testForeignKeyViolation() throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("createPersons") { db in
try db.execute(sql: "CREATE TABLE persons (id INTEGER PRIMARY KEY, name TEXT, tmp TEXT)")
try db.execute(sql: "CREATE TABLE pets (masterId INTEGER NOT NULL REFERENCES persons(id), name TEXT)")
try db.execute(sql: "INSERT INTO persons (name) VALUES ('Arthur')")
let personId = db.lastInsertedRowID
try db.execute(sql: "INSERT INTO pets (masterId, name) VALUES (?, 'Bobby')", arguments:[personId])
}
migrator.registerMigration("removePersonTmpColumn") { db in
// Test the technique described at https://www.sqlite.org/lang_altertable.html#otheralter
try db.execute(sql: "CREATE TABLE new_persons (id INTEGER PRIMARY KEY, name TEXT)")
try db.execute(sql: "INSERT INTO new_persons SELECT id, name FROM persons")
try db.execute(sql: "DROP TABLE persons")
try db.execute(sql: "ALTER TABLE new_persons RENAME TO persons")
}
migrator.registerMigration("foreignKeyError") { db in
try db.execute(sql: "INSERT INTO persons (name) VALUES ('Barbara')")
// triggers foreign key error:
try db.execute(sql: "INSERT INTO pets (masterId, name) VALUES (?, ?)", arguments: [123, "Bobby"])
}
let dbQueue = try makeDatabaseQueue()
do {
try migrator.migrate(dbQueue)
XCTFail("Expected error")
} catch let error as DatabaseError {
// Migration 1 and 2 should be committed.
// Migration 3 should not be committed.
XCTAssertEqual(error.extendedResultCode, .SQLITE_CONSTRAINT_FOREIGNKEY)
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
XCTAssertEqual(error.message, #"FOREIGN KEY constraint violation - from pets(masterId) to persons(id), in [masterId:123 name:"Bobby"]"#)
try dbQueue.inDatabase { db in
// Arthur inserted (migration 1), Barbara (migration 3) not inserted.
var rows = try Row.fetchAll(db, sql: "SELECT * FROM persons")
XCTAssertEqual(rows.count, 1)
var row = rows.first!
XCTAssertEqual(row["name"] as String, "Arthur")
// persons table has no "tmp" column (migration 2)
XCTAssertEqual(Array(row.columnNames), ["id", "name"])
// Bobby inserted (migration 1), not deleted by migration 2.
rows = try Row.fetchAll(db, sql: "SELECT * FROM pets")
XCTAssertEqual(rows.count, 1)
row = rows.first!
XCTAssertEqual(row["name"] as String, "Bobby")
}
}
}
func testAppliedMigrations() throws {
var migrator = DatabaseMigrator()
// No migration
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertEqual(dbQueue.read(migrator.appliedMigrations), [])
}
// One migration
migrator.registerMigration("1", migrate: { _ in })
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertEqual(dbQueue.read(migrator.appliedMigrations), [])
try migrator.migrate(dbQueue, upTo: "1")
try XCTAssertEqual(dbQueue.read(migrator.appliedMigrations), ["1"])
}
// Two migrations
migrator.registerMigration("2", migrate: { _ in })
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertEqual(dbQueue.read(migrator.appliedMigrations), [])
try migrator.migrate(dbQueue, upTo: "1")
try XCTAssertEqual(dbQueue.read(migrator.appliedMigrations), ["1"])
try migrator.migrate(dbQueue, upTo: "2")
try XCTAssertEqual(dbQueue.read(migrator.appliedMigrations), ["1", "2"])
}
}
func testCompletedMigrations() throws {
var migrator = DatabaseMigrator()
// No migration
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertEqual(dbQueue.read(migrator.completedMigrations), [])
try XCTAssertTrue(dbQueue.read(migrator.hasCompletedMigrations))
}
// One migration
migrator.registerMigration("1", migrate: { _ in })
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertEqual(dbQueue.read(migrator.completedMigrations), [])
try XCTAssertFalse(dbQueue.read(migrator.hasCompletedMigrations))
try migrator.migrate(dbQueue, upTo: "1")
try XCTAssertEqual(dbQueue.read(migrator.completedMigrations), ["1"])
try XCTAssertTrue(dbQueue.read(migrator.hasCompletedMigrations))
}
// Two migrations
migrator.registerMigration("2", migrate: { _ in })
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertEqual(dbQueue.read(migrator.completedMigrations), [])
try XCTAssertFalse(dbQueue.read(migrator.hasCompletedMigrations))
try migrator.migrate(dbQueue, upTo: "1")
try XCTAssertEqual(dbQueue.read(migrator.completedMigrations), ["1"])
try XCTAssertFalse(dbQueue.read(migrator.hasCompletedMigrations))
try migrator.migrate(dbQueue, upTo: "2")
try XCTAssertEqual(dbQueue.read(migrator.completedMigrations), ["1", "2"])
try XCTAssertTrue(dbQueue.read(migrator.hasCompletedMigrations))
}
}
func testSuperseded() throws {
var migrator = DatabaseMigrator()
// No migration
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertFalse(dbQueue.read(migrator.hasBeenSuperseded))
}
// One migration
migrator.registerMigration("1", migrate: { _ in })
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertFalse(dbQueue.read(migrator.hasBeenSuperseded))
try migrator.migrate(dbQueue, upTo: "1")
try XCTAssertFalse(dbQueue.read(migrator.hasBeenSuperseded))
}
// Two migrations
migrator.registerMigration("2", migrate: { _ in })
do {
let dbQueue = try makeDatabaseQueue()
try XCTAssertFalse(dbQueue.read(migrator.hasBeenSuperseded))
try migrator.migrate(dbQueue, upTo: "1")
try XCTAssertFalse(dbQueue.read(migrator.hasBeenSuperseded))
try migrator.migrate(dbQueue, upTo: "2")
try XCTAssertFalse(dbQueue.read(migrator.hasBeenSuperseded))
}
}
func testMergedMigrators() throws {
// Migrate a database
var migrator1 = DatabaseMigrator()
migrator1.registerMigration("1", migrate: { _ in })
migrator1.registerMigration("3", migrate: { _ in })
let dbQueue = try makeDatabaseQueue()
try migrator1.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read(migrator1.appliedMigrations), ["1", "3"])
try XCTAssertEqual(dbQueue.read(migrator1.appliedIdentifiers), ["1", "3"])
try XCTAssertEqual(dbQueue.read(migrator1.completedMigrations), ["1", "3"])
try XCTAssertTrue(dbQueue.read(migrator1.hasCompletedMigrations))
try XCTAssertFalse(dbQueue.read(migrator1.hasBeenSuperseded))
// ---
// A source code merge inserts a migration between "1" and "3"
var migrator2 = DatabaseMigrator()
migrator2.registerMigration("1", migrate: { _ in })
migrator2.registerMigration("2", migrate: { _ in })
migrator2.registerMigration("3", migrate: { _ in })
try XCTAssertEqual(dbQueue.read(migrator2.appliedMigrations), ["1", "3"])
try XCTAssertEqual(dbQueue.read(migrator2.appliedIdentifiers), ["1", "3"])
try XCTAssertEqual(dbQueue.read(migrator2.completedMigrations), ["1"])
try XCTAssertFalse(dbQueue.read(migrator2.hasCompletedMigrations))
try XCTAssertFalse(dbQueue.read(migrator2.hasBeenSuperseded))
// The new source code migrates the database
try migrator2.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read(migrator1.appliedMigrations), ["1", "3"])
try XCTAssertEqual(dbQueue.read(migrator1.appliedIdentifiers), ["1", "2", "3"])
try XCTAssertEqual(dbQueue.read(migrator1.completedMigrations), ["1", "3"])
try XCTAssertTrue(dbQueue.read(migrator1.hasCompletedMigrations))
try XCTAssertTrue(dbQueue.read(migrator1.hasBeenSuperseded))
try XCTAssertEqual(dbQueue.read(migrator2.appliedMigrations), ["1", "2", "3"])
try XCTAssertEqual(dbQueue.read(migrator2.appliedIdentifiers), ["1", "2", "3"])
try XCTAssertEqual(dbQueue.read(migrator2.completedMigrations), ["1", "2", "3"])
try XCTAssertTrue(dbQueue.read(migrator2.hasCompletedMigrations))
try XCTAssertFalse(dbQueue.read(migrator2.hasBeenSuperseded))
// ---
// A source code merge appends a migration
var migrator3 = migrator2
migrator3.registerMigration("4", migrate: { _ in })
try XCTAssertEqual(dbQueue.read(migrator3.appliedMigrations), ["1", "2", "3"])
try XCTAssertEqual(dbQueue.read(migrator3.appliedIdentifiers), ["1", "2", "3"])
try XCTAssertEqual(dbQueue.read(migrator3.completedMigrations), ["1", "2", "3"])
try XCTAssertFalse(dbQueue.read(migrator3.hasCompletedMigrations))
try XCTAssertFalse(dbQueue.read(migrator3.hasBeenSuperseded))
// The new source code migrates the database
try migrator3.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read(migrator1.appliedMigrations), ["1", "3"])
try XCTAssertEqual(dbQueue.read(migrator1.appliedIdentifiers), ["1", "2", "3", "4"])
try XCTAssertEqual(dbQueue.read(migrator1.completedMigrations), ["1", "3"])
try XCTAssertTrue(dbQueue.read(migrator1.hasCompletedMigrations))
try XCTAssertTrue(dbQueue.read(migrator1.hasBeenSuperseded))
try XCTAssertEqual(dbQueue.read(migrator2.appliedMigrations), ["1", "2", "3"])
try XCTAssertEqual(dbQueue.read(migrator2.appliedIdentifiers), ["1", "2", "3", "4"])
try XCTAssertEqual(dbQueue.read(migrator2.completedMigrations), ["1", "2", "3"])
try XCTAssertTrue(dbQueue.read(migrator2.hasCompletedMigrations))
try XCTAssertTrue(dbQueue.read(migrator2.hasBeenSuperseded))
try XCTAssertEqual(dbQueue.read(migrator3.appliedMigrations), ["1", "2", "3", "4"])
try XCTAssertEqual(dbQueue.read(migrator3.appliedIdentifiers), ["1", "2", "3", "4"])
try XCTAssertEqual(dbQueue.read(migrator3.completedMigrations), ["1", "2", "3", "4"])
try XCTAssertTrue(dbQueue.read(migrator3.hasCompletedMigrations))
try XCTAssertFalse(dbQueue.read(migrator3.hasBeenSuperseded))
}
// Regression test for https://github.com/groue/GRDB.swift/issues/741
func testEraseDatabaseOnSchemaChangeDoesNotDeadLockOnTargetQueue() throws {
dbConfiguration.targetQueue = DispatchQueue(label: "target")
let dbQueue = try makeDatabaseQueue()
var migrator = DatabaseMigrator()
migrator.eraseDatabaseOnSchemaChange = true
migrator.registerMigration("1", migrate: { _ in })
try migrator.migrate(dbQueue)
migrator.registerMigration("2", migrate: { _ in })
try migrator.migrate(dbQueue)
}
// Regression test for https://github.com/groue/GRDB.swift/issues/741
func testEraseDatabaseOnSchemaChangeDoesNotDeadLockOnWriteTargetQueue() throws {
dbConfiguration.writeTargetQueue = DispatchQueue(label: "writerTarget")
let dbQueue = try makeDatabaseQueue()
var migrator = DatabaseMigrator()
migrator.eraseDatabaseOnSchemaChange = true
migrator.registerMigration("1", migrate: { _ in })
try migrator.migrate(dbQueue)
migrator.registerMigration("2", migrate: { _ in })
try migrator.migrate(dbQueue)
}
func testEraseDatabaseOnSchemaChange() throws {
// 1st version of the migrator
var migrator1 = DatabaseMigrator()
migrator1.registerMigration("1") { db in
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text)
}
}
// 2nd version of the migrator
var migrator2 = DatabaseMigrator()
migrator2.registerMigration("1") { db in
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text)
t.column("score", .integer) // <- schema change, because reasons (development)
}
}
migrator2.registerMigration("2") { db in
try db.execute(sql: "INSERT INTO player (id, name, score) VALUES (NULL, 'Arthur', 1000)")
}
// Apply 1st migrator
let dbQueue = try makeDatabaseQueue()
try migrator1.migrate(dbQueue)
// Test than 2nd migrator can't run...
do {
try migrator2.migrate(dbQueue)
XCTFail("Expected DatabaseError")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.message, "table player has no column named score")
}
try XCTAssertEqual(dbQueue.read(migrator2.appliedMigrations), ["1"])
// ... unless database gets erased
migrator2.eraseDatabaseOnSchemaChange = true
try migrator2.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read(migrator2.appliedMigrations), ["1", "2"])
}
func testEraseDatabaseOnSchemaChangeWithConfiguration() throws {
// 1st version of the migrator
var migrator1 = DatabaseMigrator()
migrator1.registerMigration("1") { db in
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text)
}
try db.execute(sql: "INSERT INTO player (id, name) VALUES (NULL, testFunction())")
}
// 2nd version of the migrator
var migrator2 = DatabaseMigrator()
migrator2.registerMigration("1") { db in
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text)
t.column("score", .integer) // <- schema change
}
try db.execute(sql: "INSERT INTO player (id, name, score) VALUES (NULL, testFunction(), 1000)")
}
migrator2.registerMigration("2") { db in
try db.execute(sql: "INSERT INTO player (id, name, score) VALUES (NULL, testFunction(), 2000)")
}
// Apply 1st migrator
dbConfiguration.prepareDatabase { db in
let function = DatabaseFunction("testFunction", argumentCount: 0, pure: true) { _ in "Arthur" }
db.add(function: function)
}
let dbQueue = try makeDatabaseQueue()
try migrator1.migrate(dbQueue)
// Test than 2nd migrator can't run...
do {
try migrator2.migrate(dbQueue)
XCTFail("Expected DatabaseError")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.message, "table player has no column named score")
}
try XCTAssertEqual(dbQueue.read(migrator2.appliedMigrations), ["1"])
// ... unless database gets erased
migrator2.eraseDatabaseOnSchemaChange = true
try migrator2.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read(migrator2.appliedMigrations), ["1", "2"])
}
func testEraseDatabaseOnSchemaChangeDoesNotEraseDatabaseOnAddedMigration() throws {
var migrator = DatabaseMigrator()
migrator.eraseDatabaseOnSchemaChange = true
var witness = 1
migrator.registerMigration("1") { db in
try db.execute(sql: """
CREATE TABLE t1(id INTEGER PRIMARY KEY);
INSERT INTO t1(id) VALUES (?)
""", arguments: [witness])
witness += 1
}
let dbQueue = try makeDatabaseQueue()
// 1st migration
try migrator.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read { try Int.fetchOne($0, sql: "SELECT id FROM t1") }, 1)
// 2nd migration does not erase database
migrator.registerMigration("2") { db in
try db.execute(sql: """
CREATE TABLE t2(id INTEGER PRIMARY KEY);
""")
}
try migrator.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read { try Int.fetchOne($0, sql: "SELECT id FROM t1") }, 1)
try XCTAssertTrue(dbQueue.read { try $0.tableExists("t2") })
}
// Regression test for <https://github.com/groue/GRDB.swift/issues/1360>
func testEraseDatabaseOnSchemaChangeIgnoresInternalSchemaObjects() throws {
// Given a migrator with eraseDatabaseOnSchemaChange
var migrator = DatabaseMigrator()
migrator.eraseDatabaseOnSchemaChange = true
migrator.registerMigration("1") { db in
try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY)")
}
let dbQueue = try makeDatabaseQueue()
try migrator.migrate(dbQueue)
// When we add an internal schema object (sqlite_stat1)
try dbQueue.write { db in
try db.execute(sql: """
INSERT INTO t DEFAULT VALUES;
ANALYZE;
""")
try XCTAssertTrue(db.tableExists("sqlite_stat1"))
}
// Then 2nd migration does not erase database
try migrator.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read { try Int.fetchOne($0, sql: "SELECT id FROM t") }, 1)
}
func testEraseDatabaseOnSchemaChangeWithRenamedMigration() throws {
let dbQueue = try makeDatabaseQueue()
// 1st migration
var migrator1 = DatabaseMigrator()
migrator1.registerMigration("1") { db in
try db.execute(sql: """
CREATE TABLE t1(id INTEGER PRIMARY KEY);
INSERT INTO t1(id) VALUES (1)
""")
}
try migrator1.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read { try Int.fetchOne($0, sql: "SELECT id FROM t1") }, 1)
// 2nd migration does not erase database
var migrator2 = DatabaseMigrator()
migrator2.eraseDatabaseOnSchemaChange = true
migrator2.registerMigration("2") { db in
try db.execute(sql: """
CREATE TABLE t1(id INTEGER PRIMARY KEY);
INSERT INTO t1(id) VALUES (2)
""")
}
try migrator2.migrate(dbQueue)
try XCTAssertEqual(dbQueue.read { try Int.fetchOne($0, sql: "SELECT id FROM t1") }, 2)
}
func testMigrations() throws {
do {
let migrator = DatabaseMigrator()
XCTAssertEqual(migrator.migrations, [])
}
do {
var migrator = DatabaseMigrator()
migrator.registerMigration("foo", migrate: { _ in })
XCTAssertEqual(migrator.migrations, ["foo"])
}
do {
var migrator = DatabaseMigrator()
migrator.registerMigration("foo", migrate: { _ in })
migrator.registerMigration("bar", migrate: { _ in })
XCTAssertEqual(migrator.migrations, ["foo", "bar"])
}
}
func testMigrationForeignKeyChecks() throws {
let foreignKeyViolation = """
CREATE TABLE parent(id INTEGER NOT NULL PRIMARY KEY);
CREATE TABLE child(parentId INTEGER REFERENCES parent(id));
INSERT INTO child (parentId) VALUES (1);
"""
let transientForeignKeyViolation = """
CREATE TABLE parent(id INTEGER NOT NULL PRIMARY KEY);
CREATE TABLE child(parentId INTEGER REFERENCES parent(id));
INSERT INTO child (parentId) VALUES (1);
DELETE FROM child;
"""
// Foreign key violation
do {
var migrator = DatabaseMigrator()
migrator.registerMigration("A") { db in
try db.execute(sql: foreignKeyViolation)
}
let dbQueue = try makeDatabaseQueue()
do {
try migrator.migrate(dbQueue)
XCTFail("Expected error")
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY { }
try dbQueue.read { try $0.checkForeignKeys() }
}
do {
var migrator = DatabaseMigrator()
migrator.registerMigration("A", foreignKeyChecks: .immediate) { db in
try db.execute(sql: foreignKeyViolation)
}
let dbQueue = try makeDatabaseQueue()
do {
try migrator.migrate(dbQueue)
XCTFail("Expected error")
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY { }
try dbQueue.read { try $0.checkForeignKeys() }
}
// Transient foreign key violation
do {
var migrator = DatabaseMigrator()
migrator.registerMigration("A") { db in
try db.execute(sql: transientForeignKeyViolation)
}
let dbQueue = try makeDatabaseQueue()
try migrator.migrate(dbQueue)
try dbQueue.read { try $0.checkForeignKeys() }
}
do {
var migrator = DatabaseMigrator()
migrator.registerMigration("A", foreignKeyChecks: .immediate) { db in
try db.execute(sql: transientForeignKeyViolation)
}
let dbQueue = try makeDatabaseQueue()
do {
try migrator.migrate(dbQueue)
XCTFail("Expected error")
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY { }
try dbQueue.read { try $0.checkForeignKeys() }
}
}
func testDisablingDeferredForeignKeyChecks() throws {
let foreignKeyViolation = """
CREATE TABLE parent(id INTEGER NOT NULL PRIMARY KEY);
CREATE TABLE child(parentId INTEGER REFERENCES parent(id));
INSERT INTO child (parentId) VALUES (1);
"""
let transientForeignKeyViolation = """
CREATE TABLE parent(id INTEGER NOT NULL PRIMARY KEY);
CREATE TABLE child(parentId INTEGER REFERENCES parent(id));
INSERT INTO child (parentId) VALUES (1);
DELETE FROM child;
"""
// Foreign key violation
do {
var migrator = DatabaseMigrator().disablingDeferredForeignKeyChecks()
migrator.registerMigration("A") { db in
try db.execute(sql: foreignKeyViolation)
}
let dbQueue = try makeDatabaseQueue()
try migrator.migrate(dbQueue)
do {
// The unique opportunity for corrupt data!
try dbQueue.read { try $0.checkForeignKeys() }
XCTFail("Expected foreign key violation")
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY { }
}
do {
var migrator = DatabaseMigrator().disablingDeferredForeignKeyChecks()
migrator.registerMigration("A", foreignKeyChecks: .immediate) { db in
try db.execute(sql: foreignKeyViolation)
}
let dbQueue = try makeDatabaseQueue()
do {
try migrator.migrate(dbQueue)
XCTFail("Expected error")
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY { }
try dbQueue.read { try $0.checkForeignKeys() }
}
// Transient foreign key violation
do {
var migrator = DatabaseMigrator().disablingDeferredForeignKeyChecks()
migrator.registerMigration("A") { db in
try db.execute(sql: transientForeignKeyViolation)
}
let dbQueue = try makeDatabaseQueue()
try migrator.migrate(dbQueue)
try dbQueue.read { try $0.checkForeignKeys() }
}
do {
var migrator = DatabaseMigrator().disablingDeferredForeignKeyChecks()
migrator.registerMigration("A", foreignKeyChecks: .immediate) { db in
try db.execute(sql: transientForeignKeyViolation)
}
let dbQueue = try makeDatabaseQueue()
do {
try migrator.migrate(dbQueue)
XCTFail("Expected error")
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY { }
try dbQueue.read { try $0.checkForeignKeys() }
}
}
func test_disablingDeferredForeignKeyChecks_applies_to_newly_registered_migrations_only() throws {
let foreignKeyViolation = """
CREATE TABLE parent(id INTEGER NOT NULL PRIMARY KEY);
CREATE TABLE child(parentId INTEGER REFERENCES parent(id));
INSERT INTO child (parentId) VALUES (1);
"""
var migrator = DatabaseMigrator()
migrator.registerMigration("A") { db in
try db.execute(sql: foreignKeyViolation)
}
migrator = migrator.disablingDeferredForeignKeyChecks()
migrator.registerMigration("B") { db in
XCTFail("Should not run")
}
let dbQueue = try makeDatabaseQueue()
do {
try migrator.migrate(dbQueue)
XCTFail("Expected error")
} catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY { }
}
}