374 lines
15 KiB
Swift
374 lines
15 KiB
Swift
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<T: DatabaseWriter>(_ 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<T: DatabaseWriter>(_ 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<T: DatabaseWriter>(_ 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<T: DatabaseWriter>(_ 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<T: DatabaseWriter>(_ 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;
|
|
""")
|
|
}
|
|
}
|
|
}
|