521 lines
20 KiB
Swift
521 lines
20 KiB
Swift
import XCTest
|
|
@testable import GRDB
|
|
|
|
class DatabaseSuspensionTests : GRDBTestCase {
|
|
|
|
private func makeDatabaseQueue(journalMode: String) throws -> DatabaseQueue {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
let actualJournalMode = try dbQueue.inDatabase { try String.fetchOne($0, sql: "PRAGMA journal_mode=\(journalMode)") }
|
|
XCTAssertEqual(actualJournalMode, journalMode)
|
|
return dbQueue
|
|
}
|
|
|
|
// MARK: - BEGIN TRANSACTION
|
|
|
|
func testSuspensionPreventsNewTransactionInDeleteJournalMode() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "delete")
|
|
dbQueue.suspend()
|
|
|
|
do {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "BEGIN TRANSACTION")
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "BEGIN TRANSACTION")
|
|
}
|
|
|
|
do {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "BEGIN IMMEDIATE TRANSACTION")
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "BEGIN IMMEDIATE TRANSACTION")
|
|
}
|
|
|
|
do {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "BEGIN EXCLUSIVE TRANSACTION")
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "BEGIN EXCLUSIVE TRANSACTION")
|
|
}
|
|
|
|
do {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "SAVEPOINT test")
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "SAVEPOINT test")
|
|
}
|
|
}
|
|
|
|
func testSuspensionDoesNotPreventNewDeferredTransactionInWALMode() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "wal")
|
|
dbQueue.suspend()
|
|
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "BEGIN TRANSACTION; ROLLBACK")
|
|
try db.execute(sql: "SAVEPOINT test; RELEASE SAVEPOINT test")
|
|
}
|
|
}
|
|
|
|
func testSuspensionPreventsNewImmediateOrExclusiveTransactionInWALMode() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "wal")
|
|
dbQueue.suspend()
|
|
|
|
do {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "BEGIN IMMEDIATE TRANSACTION")
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "BEGIN IMMEDIATE TRANSACTION")
|
|
}
|
|
|
|
do {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "BEGIN EXCLUSIVE TRANSACTION")
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "BEGIN EXCLUSIVE TRANSACTION")
|
|
}
|
|
}
|
|
|
|
// MARK: - COMMIT, ROLLBACK, RELEASE SAVEPOINT, ROLLBACK TRANSACTION TO SAVEPOINT
|
|
|
|
func testSuspensionDoesNotPreventCommit() throws {
|
|
func test(_ dbQueue: DatabaseQueue) throws {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "BEGIN TRANSACTION")
|
|
dbQueue.suspend()
|
|
try db.execute(sql: "COMMIT")
|
|
}
|
|
}
|
|
try test(makeDatabaseQueue(journalMode: "delete"))
|
|
try test(makeDatabaseQueue(journalMode: "wal"))
|
|
}
|
|
|
|
func testSuspensionDoesNotPreventRollback() throws {
|
|
func test(_ dbQueue: DatabaseQueue) throws {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "BEGIN TRANSACTION")
|
|
dbQueue.suspend()
|
|
try db.execute(sql: "ROLLBACK")
|
|
}
|
|
}
|
|
try test(makeDatabaseQueue(journalMode: "delete"))
|
|
try test(makeDatabaseQueue(journalMode: "wal"))
|
|
}
|
|
|
|
func testSuspensionDoesNotPreventReleaseSavePoint() throws {
|
|
func test(_ dbQueue: DatabaseQueue) throws {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "SAVEPOINT test")
|
|
dbQueue.suspend()
|
|
try db.execute(sql: "RELEASE SAVEPOINT test")
|
|
}
|
|
}
|
|
try test(makeDatabaseQueue(journalMode: "delete"))
|
|
try test(makeDatabaseQueue(journalMode: "wal"))
|
|
}
|
|
|
|
func testSuspensionDoesNotPreventRollbackSavePoint() throws {
|
|
func test(_ dbQueue: DatabaseQueue) throws {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "SAVEPOINT test")
|
|
dbQueue.suspend()
|
|
try db.execute(sql: "ROLLBACK TRANSACTION TO SAVEPOINT test")
|
|
try db.execute(sql: "RELEASE SAVEPOINT test")
|
|
}
|
|
}
|
|
try test(makeDatabaseQueue(journalMode: "delete"))
|
|
try test(makeDatabaseQueue(journalMode: "wal"))
|
|
}
|
|
|
|
// MARK: - SELECT
|
|
|
|
func testSuspensionPreventsReadInDeleteJournalMode() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "delete")
|
|
do {
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
dbQueue.suspend()
|
|
_ = 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, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "SELECT COUNT(*) FROM t")
|
|
}
|
|
}
|
|
|
|
func testSuspensionDoesNotPreventReadInWALMode() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "wal")
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
dbQueue.suspend()
|
|
|
|
try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t"), 0)
|
|
|
|
try db.inTransaction(.deferred) {
|
|
try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t"), 0)
|
|
return .commit
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - INSERT
|
|
|
|
func testSuspensionPreventsWriteInDeleteJournalMode() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "delete")
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
dbQueue.suspend()
|
|
do {
|
|
try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "INSERT INTO t DEFAULT VALUES")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testSuspensionPreventsWriteInWALMode() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "wal")
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
dbQueue.suspend()
|
|
|
|
do {
|
|
try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "INSERT INTO t DEFAULT VALUES")
|
|
}
|
|
|
|
do {
|
|
try db.inTransaction(.deferred) {
|
|
try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
|
|
return .commit
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "INSERT INTO t DEFAULT VALUES")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Automatic ROLLBACK
|
|
|
|
func testSuspensionRollbacksOnPreventedWrite() throws {
|
|
func test(_ dbQueue: DatabaseQueue) throws {
|
|
do {
|
|
try dbQueue.write { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
XCTAssertTrue(db.isInsideTransaction)
|
|
dbQueue.suspend()
|
|
do {
|
|
try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Database is suspended")
|
|
XCTAssertEqual(error.sql, "INSERT INTO t DEFAULT VALUES")
|
|
}
|
|
// Aborded transaction
|
|
XCTAssertFalse(db.isInsideTransaction)
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_ABORT)
|
|
XCTAssertEqual(error.message, "Transaction was aborted")
|
|
}
|
|
}
|
|
try test(makeDatabaseQueue(journalMode: "delete"))
|
|
try test(makeDatabaseQueue(journalMode: "wal"))
|
|
}
|
|
|
|
// MARK: - Concurrent Lock Prevention
|
|
|
|
func testSuspensionAbortsDatabaseQueueAccess() throws {
|
|
func test(_ dbQueue: DatabaseQueue) throws {
|
|
let semaphore1 = DispatchSemaphore(value: 0)
|
|
let semaphore2 = DispatchSemaphore(value: 0)
|
|
dbQueue.inDatabase { db in
|
|
db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in
|
|
semaphore1.signal()
|
|
semaphore2.wait()
|
|
return nil
|
|
})
|
|
}
|
|
|
|
let block1 = {
|
|
do {
|
|
_ = try dbQueue.inDatabase {
|
|
try Row.fetchAll($0, 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()
|
|
dbQueue.suspend()
|
|
semaphore2.signal()
|
|
}
|
|
let blocks = [block1, block2]
|
|
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
|
|
blocks[index]()
|
|
}
|
|
}
|
|
try test(makeDatabaseQueue(journalMode: "delete"))
|
|
try test(makeDatabaseQueue(journalMode: "wal"))
|
|
}
|
|
|
|
func testSuspensionDoesNotPreventFurtherReadInWALMode() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "wal")
|
|
try dbQueue.write { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
}
|
|
|
|
let semaphore1 = DispatchSemaphore(value: 0)
|
|
let semaphore2 = DispatchSemaphore(value: 0)
|
|
|
|
dbQueue.inDatabase { db in
|
|
db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in
|
|
semaphore1.signal()
|
|
semaphore2.wait()
|
|
return nil
|
|
})
|
|
}
|
|
let block1 = {
|
|
do {
|
|
try dbQueue.inTransaction { db in
|
|
do {
|
|
_ = try Row.fetchAll(db, sql: "SELECT wait()")
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT)
|
|
}
|
|
XCTAssertTrue(db.isInsideTransaction)
|
|
try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t"), 0)
|
|
return .commit
|
|
}
|
|
} catch {
|
|
XCTFail("Unexpected error: \(error)")
|
|
}
|
|
}
|
|
let block2 = {
|
|
semaphore1.wait()
|
|
dbQueue.suspend()
|
|
semaphore2.signal()
|
|
}
|
|
let blocks = [block1, block2]
|
|
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
|
|
blocks[index]()
|
|
}
|
|
}
|
|
|
|
func testWriteTransactionAbortedDuringStatementExecution() throws {
|
|
func test(_ dbQueue: DatabaseQueue) throws {
|
|
try dbQueue.write { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
}
|
|
|
|
let semaphore1 = DispatchSemaphore(value: 0)
|
|
let semaphore2 = DispatchSemaphore(value: 0)
|
|
|
|
dbQueue.inDatabase { db in
|
|
db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in
|
|
semaphore1.signal()
|
|
semaphore2.wait()
|
|
return nil
|
|
})
|
|
}
|
|
let block1 = {
|
|
do {
|
|
try dbQueue.write { db in
|
|
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()
|
|
dbQueue.suspend()
|
|
semaphore2.signal()
|
|
}
|
|
let blocks = [block1, block2]
|
|
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
|
|
blocks[index]()
|
|
}
|
|
}
|
|
try test(makeDatabaseQueue(journalMode: "delete"))
|
|
try test(makeDatabaseQueue(journalMode: "wal"))
|
|
}
|
|
|
|
func testWriteTransactionAbortedDuringStatementExecutionPreventsFurtherDatabaseAccess() throws {
|
|
func test(_ dbQueue: DatabaseQueue) throws {
|
|
try dbQueue.write { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
}
|
|
|
|
let semaphore1 = DispatchSemaphore(value: 0)
|
|
let semaphore2 = DispatchSemaphore(value: 0)
|
|
|
|
dbQueue.inDatabase { db in
|
|
db.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in
|
|
semaphore1.signal()
|
|
semaphore2.wait()
|
|
return nil
|
|
})
|
|
}
|
|
let block1 = {
|
|
do {
|
|
try dbQueue.write { db in
|
|
do {
|
|
try db.execute(sql: "INSERT INTO t SELECT wait()")
|
|
XCTFail("Expected error")
|
|
} catch let error as DatabaseError {
|
|
XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT)
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
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()
|
|
dbQueue.suspend()
|
|
semaphore2.signal()
|
|
}
|
|
let blocks = [block1, block2]
|
|
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
|
|
blocks[index]()
|
|
}
|
|
}
|
|
try test(makeDatabaseQueue(journalMode: "delete"))
|
|
try test(makeDatabaseQueue(journalMode: "wal"))
|
|
}
|
|
|
|
// MARK: - resume
|
|
|
|
func testResume() throws {
|
|
let dbQueue = try makeDatabaseQueue(journalMode: "delete")
|
|
try dbQueue.inDatabase { db in
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
dbQueue.suspend()
|
|
dbQueue.resume()
|
|
try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 0)
|
|
}
|
|
}
|
|
|
|
// MARK: - journalModeCache
|
|
|
|
// Test for internals. Make sure the journalModeCache is not set too early,
|
|
// especially not before user can choose the journal mode
|
|
func testJournalModeCache() throws {
|
|
do {
|
|
let dbQueue = try makeDatabaseQueue()
|
|
try dbQueue.inDatabase { db in
|
|
XCTAssertNil(db.journalModeCache)
|
|
try db.execute(sql: "PRAGMA journal_mode=truncate")
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
XCTAssertNil(db.journalModeCache)
|
|
}
|
|
dbQueue.suspend()
|
|
dbQueue.inDatabase { db in
|
|
XCTAssertNil(db.journalModeCache)
|
|
try? db.execute(sql: "SELECT * FROM sqlite_master")
|
|
XCTAssertEqual(db.journalModeCache, "truncate")
|
|
}
|
|
}
|
|
do {
|
|
var configuration = Configuration()
|
|
configuration.prepareDatabase { db in
|
|
try db.execute(sql: "PRAGMA journal_mode=truncate")
|
|
}
|
|
let dbQueue = try makeDatabaseQueue(configuration: configuration)
|
|
dbQueue.suspend()
|
|
dbQueue.inDatabase { db in
|
|
XCTAssertNil(db.journalModeCache)
|
|
try? db.execute(sql: "SELECT * FROM sqlite_master")
|
|
XCTAssertEqual(db.journalModeCache, "truncate")
|
|
}
|
|
}
|
|
do {
|
|
let dbPool = try makeDatabasePool()
|
|
try dbPool.write { db in
|
|
XCTAssertNil(db.journalModeCache)
|
|
try db.execute(sql: "CREATE TABLE t(a)")
|
|
XCTAssertNil(db.journalModeCache)
|
|
}
|
|
dbPool.suspend()
|
|
try dbPool.writeWithoutTransaction { db in
|
|
XCTAssertNil(db.journalModeCache)
|
|
try db.execute(sql: "SELECT * FROM sqlite_master")
|
|
XCTAssertEqual(db.journalModeCache, "wal")
|
|
}
|
|
try dbPool.write { db in
|
|
XCTAssertEqual(db.journalModeCache, "wal")
|
|
}
|
|
try dbPool.read { db in
|
|
XCTAssertNil(db.journalModeCache)
|
|
try db.execute(sql: "SELECT * FROM sqlite_master")
|
|
XCTAssertNil(db.journalModeCache)
|
|
}
|
|
}
|
|
}
|
|
}
|