import XCTest import GRDB class DatabaseMigratorTests : GRDBTestCase { // Test passes if it compiles. // See 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: 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: 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 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 { } } }