Files

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)
}
}
}
}