import XCTest import GRDB class DatabaseAbortedTransactionTests : GRDBTestCase { func testReadTransactionAbortedByInterrupt() throws { func test(_ dbReader: some DatabaseReader) throws { let semaphore1 = DispatchSemaphore(value: 0) let semaphore2 = DispatchSemaphore(value: 0) let block1 = { do { try dbReader.read { db in db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in semaphore1.signal() semaphore2.wait() return nil }) _ = try Row.fetchAll(db, sql: "SELECT wait()") } XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) } catch { XCTFail("Unexpected error: \(error)") } } let block2 = { semaphore1.wait() dbReader.interrupt() semaphore2.signal() } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } try test(DatabaseQueue()) try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) try test(makeDatabasePool().makeSnapshotPool()) #endif } func testReadTransactionAbortedByInterruptDoesNotPreventFurtherRead() throws { func test(_ dbReader: some DatabaseReader) throws { let semaphore1 = DispatchSemaphore(value: 0) let semaphore2 = DispatchSemaphore(value: 0) let block1 = { try! dbReader.read { db in db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in semaphore1.signal() semaphore2.wait() return nil }) let wasInTransaction = db.isInsideTransaction do { _ = try Row.fetchAll(db, sql: "SELECT wait()") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) } catch { XCTFail("Unexpected error: \(error)") } XCTAssertEqual(db.isInsideTransaction, wasInTransaction) try XCTAssertTrue(Bool.fetchOne(db, sql: "SELECT 1")!) } } let block2 = { semaphore1.wait() dbReader.interrupt() semaphore2.signal() } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } try test(DatabaseQueue()) try test(makeDatabaseQueue()) try test(makeDatabasePool()) try test(makeDatabasePool().makeSnapshot()) #if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst)))) try test(makeDatabasePool().makeSnapshotPool()) #endif } func testWriteTransactionAbortedByInterrupt() throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in try db.execute(sql: "CREATE TABLE t(a);") } return dbWriter } func test(_ dbWriter: some DatabaseWriter) throws { let semaphore1 = DispatchSemaphore(value: 0) let semaphore2 = DispatchSemaphore(value: 0) let block1 = { do { try dbWriter.write { db in db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in semaphore1.signal() semaphore2.wait() return nil }) try db.execute(sql: "INSERT INTO t SELECT wait()") } XCTFail("Expected error") } catch let error as DatabaseError { // Transactions throw the first uncatched error: SQLITE_INTERRUPT XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) XCTAssertEqual(error.message, "interrupted") XCTAssertEqual(error.sql, "INSERT INTO t SELECT wait()") } catch { XCTFail("Unexpected error: \(error)") } } let block2 = { semaphore1.wait() dbWriter.interrupt() semaphore2.signal() } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } try test(setup(DatabaseQueue())) try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) } func testWriteTransactionAbortedByInterruptPreventsFurtherDatabaseAccess() throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in try db.execute(sql: "CREATE TABLE t(a);") } return dbWriter } func test(_ dbWriter: some DatabaseWriter) throws { let semaphore1 = DispatchSemaphore(value: 0) let semaphore2 = DispatchSemaphore(value: 0) let block1 = { do { try dbWriter.write { db in do { db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in semaphore1.signal() semaphore2.wait() return nil }) try db.execute(sql: "INSERT INTO t SELECT wait()") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) } catch { XCTFail("Unexpected error: \(error)") } XCTAssertFalse(db.isInsideTransaction) do { _ = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) XCTAssertEqual(error.message, "Transaction was aborted") XCTAssertEqual(error.sql, "SELECT COUNT(*) FROM t") } catch { XCTFail("Unexpected error: \(error)") } do { try db.execute(sql: "INSERT INTO t (a) VALUES (0)") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) XCTAssertEqual(error.message, "Transaction was aborted") XCTAssertEqual(error.sql, "INSERT INTO t (a) VALUES (0)") } catch { XCTFail("Unexpected error: \(error)") } } XCTFail("Expected error") } catch let error as DatabaseError { // SQLITE_INTERRUPT has been caught. So we get SQLITE_ABORT // from the last commit. XCTAssertEqual(error.resultCode, .SQLITE_ABORT) XCTAssertEqual(error.message, "Transaction was aborted") } catch { XCTFail("Unexpected error: \(error)") } } let block2 = { semaphore1.wait() dbWriter.interrupt() semaphore2.signal() } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } try test(setup(DatabaseQueue())) try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) } func testWriteTransactionAbortedByInterruptDoesNotPreventRollback() throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in try db.execute(sql: "CREATE TABLE t(a);") } return dbWriter } func test(_ dbWriter: some DatabaseWriter) throws { let semaphore1 = DispatchSemaphore(value: 0) let semaphore2 = DispatchSemaphore(value: 0) let block1 = { try! dbWriter.writeWithoutTransaction { db in db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in semaphore1.signal() semaphore2.wait() return nil }) try db.inTransaction { do { try db.execute(sql: "INSERT INTO t SELECT wait()") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) } catch { XCTFail("Unexpected error: \(error)") } XCTAssertFalse(db.isInsideTransaction) return .rollback } } } let block2 = { semaphore1.wait() dbWriter.interrupt() semaphore2.signal() } let blocks = [block1, block2] DispatchQueue.concurrentPerform(iterations: blocks.count) { index in blocks[index]() } } try test(setup(DatabaseQueue())) try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) } func testTransactionAbortedByConflictPreventsFurtherDatabaseAccess() throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in try db.execute(sql: """ CREATE TABLE t(a UNIQUE ON CONFLICT ROLLBACK); """) } return dbWriter } func test(_ dbWriter: some DatabaseWriter) throws { do { try dbWriter.write { db in do { try db.execute(sql: """ INSERT INTO t (a) VALUES (1); INSERT INTO t (a) VALUES (1); """) XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT) XCTAssertEqual(error.message, "UNIQUE constraint failed: t.a") XCTAssertEqual(error.sql, "INSERT INTO t (a) VALUES (1)") } catch { XCTFail("Unexpected error: \(error)") } XCTAssertFalse(db.isInsideTransaction) try db.execute(sql: "INSERT INTO t (a) VALUES (2)") } XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) XCTAssertEqual(error.message, "Transaction was aborted") XCTAssertEqual(error.sql, "INSERT INTO t (a) VALUES (2)") } catch { XCTFail("Unexpected error: \(error)") } } try test(setup(DatabaseQueue())) try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) } func testTransactionAbortedByUser() throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in try db.execute(sql: "CREATE TABLE t(a);") } return dbWriter } func test(_ dbReader: some DatabaseReader) throws { do { try dbReader.unsafeRead { db in try db.inTransaction { try db.execute(sql: """ SELECT * FROM t; ROLLBACK; SELECT * FROM t; """) return .commit } } XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) XCTAssertEqual(error.message, "Transaction was aborted") XCTAssertEqual(error.sql, "SELECT * FROM t") } catch { XCTFail("Unexpected error: \(error)") } } try test(setup(DatabaseQueue())) try test(setup(makeDatabaseQueue())) try test(setup(makeDatabasePool())) } func testReadTransactionRestartHack() throws { // Here we test that the "ROLLBACK; BEGIN TRANSACTION;" hack which // "refreshes" a DatabaseSnaphot works. // See https://github.com/groue/GRDB.swift/issues/619 // This hack puts temporarily the transaction in the aborded // state. Here we test that we don't throw SQLITE_ABORT. let dbPool = try makeDatabasePool() try dbPool.write { db in try db.execute(sql: "CREATE TABLE t(a);") } let snapshot = try dbPool.makeSnapshot() try snapshot.read { db in try db.execute(sql: """ ROLLBACK; BEGIN TRANSACTION; """) } try snapshot.read { db in try db.execute(sql: """ SELECT * FROM t; ROLLBACK; BEGIN TRANSACTION; SELECT * FROM t; """) } } }