Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/Tests/GRDBTests/DatabaseQueueTests.swift
T
2025-01-28 12:28:03 +08:00

478 lines
18 KiB
Swift

import XCTest
import Dispatch
import GRDB
class DatabaseQueueTests: GRDBTestCase {
func testJournalModeConfiguration() throws {
do {
// Factory default
let config = Configuration()
let dbQueue = try makeDatabaseQueue(filename: "factory", configuration: config)
let journalMode = try dbQueue.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "delete")
}
do {
// Explicit default
var config = Configuration()
config.journalMode = .default
let dbQueue = try makeDatabaseQueue(filename: "default", configuration: config)
let journalMode = try dbQueue.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "delete")
}
do {
// Explicit wal
var config = Configuration()
config.journalMode = .wal
let dbQueue = try makeDatabaseQueue(filename: "wal", configuration: config)
let journalMode = try dbQueue.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "wal")
}
}
func testInvalidFileFormat() throws {
do {
let url = testBundle.url(forResource: "Betty", withExtension: "jpeg")!
guard (try? Data(contentsOf: url)) != nil else {
XCTFail("Missing file")
return
}
_ = try DatabaseQueue(path: url.path)
XCTFail("Expected error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_NOTADB)
XCTAssert([
"file is encrypted or is not a database",
"file is not a database"].contains(error.message!))
}
}
func testAddRemoveFunction() throws {
// Adding a function and then removing it should succeed
let dbQueue = try makeDatabaseQueue()
let fn = DatabaseFunction("succ", argumentCount: 1) { dbValues in
guard let int = Int.fromDatabaseValue(dbValues[0]) else {
return nil
}
return int + 1
}
try dbQueue.inDatabase { db in
db.add(function: fn)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT succ(1)"), 2) // 2
db.remove(function: fn)
}
do {
try dbQueue.inDatabase { db in
try db.execute(sql: "SELECT succ(1)")
XCTFail("Expected Error")
}
XCTFail("Expected Error")
}
catch let error as DatabaseError {
// expected error
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.message!.lowercased(), "no such function: succ") // lowercaseString: accept multiple SQLite version
XCTAssertEqual(error.sql!, "SELECT succ(1)")
XCTAssertEqual(error.description.lowercased(), "sqlite error 1: no such function: succ - while executing `select succ(1)`")
}
}
func testAddRemoveCollation() throws {
// Adding a collation and then removing it should succeed
let dbQueue = try makeDatabaseQueue()
let collation = DatabaseCollation("test_collation_foo") { (string1, string2) in
return (string1 as NSString).localizedStandardCompare(string2)
}
try dbQueue.inDatabase { db in
db.add(collation: collation)
try db.execute(sql: "CREATE TABLE files (name TEXT COLLATE TEST_COLLATION_FOO)")
db.remove(collation: collation)
}
do {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE TABLE files_fail (name TEXT COLLATE TEST_COLLATION_FOO)")
XCTFail("Expected Error")
}
XCTFail("Expected Error")
}
catch let error as DatabaseError {
// expected error
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.message!.lowercased(), "no such collation sequence: test_collation_foo") // lowercaseString: accept multiple SQLite version
XCTAssertEqual(error.sql!, "CREATE TABLE files_fail (name TEXT COLLATE TEST_COLLATION_FOO)")
XCTAssertEqual(error.description.lowercased(), "sqlite error 1: no such collation sequence: test_collation_foo - while executing `create table files_fail (name text collate test_collation_foo)`")
}
}
func testAllowsUnsafeTransactions() throws {
dbConfiguration.allowsUnsafeTransactions = true
let dbQueue = try makeDatabaseQueue()
try dbQueue.writeWithoutTransaction { db in
try db.beginTransaction()
}
try dbQueue.writeWithoutTransaction { db in
try db.commit()
}
}
func testDefaultLabel() throws {
let dbQueue = try makeDatabaseQueue()
XCTAssertEqual(dbQueue.configuration.label, nil)
dbQueue.inDatabase { db in
XCTAssertEqual(db.configuration.label, nil)
XCTAssertEqual(db.description, "GRDB.DatabaseQueue")
// This test CAN break in future releases: the dispatch queue labels
// are documented to be a debug-only tool.
let label = String(utf8String: __dispatch_queue_get_label(nil))
XCTAssertEqual(label, "GRDB.DatabaseQueue")
}
}
func testCustomLabel() throws {
dbConfiguration.label = "Toreador"
let dbQueue = try makeDatabaseQueue()
XCTAssertEqual(dbQueue.configuration.label, "Toreador")
dbQueue.inDatabase { db in
XCTAssertEqual(db.configuration.label, "Toreador")
XCTAssertEqual(db.description, "Toreador")
// This test CAN break in future releases: the dispatch queue labels
// are documented to be a debug-only tool.
let label = String(utf8String: __dispatch_queue_get_label(nil))
XCTAssertEqual(label, "Toreador")
}
}
func testTargetQueue() throws {
func test(targetQueue: DispatchQueue) throws {
dbConfiguration.targetQueue = targetQueue
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { _ in
dispatchPrecondition(condition: .onQueue(targetQueue))
}
try dbQueue.read { _ in
dispatchPrecondition(condition: .onQueue(targetQueue))
}
}
// background queue
try test(targetQueue: .global(qos: .background))
// main queue
let expectation = self.expectation(description: "main")
DispatchQueue.global(qos: .default).async {
try! test(targetQueue: .main)
expectation.fulfill()
}
waitForExpectations(timeout: 1, handler: nil)
}
func testWriteTargetQueue() throws {
func test(targetQueue: DispatchQueue, writeTargetQueue: DispatchQueue) throws {
dbConfiguration.targetQueue = targetQueue // unused
dbConfiguration.writeTargetQueue = writeTargetQueue
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { _ in
dispatchPrecondition(condition: .onQueue(writeTargetQueue))
}
try dbQueue.read { _ in
dispatchPrecondition(condition: .onQueue(writeTargetQueue))
}
}
// background queue
try test(targetQueue: .global(qos: .background), writeTargetQueue: DispatchQueue(label: "writer"))
// main queue
let expectation = self.expectation(description: "main")
DispatchQueue.global(qos: .default).async {
try! test(targetQueue: .main, writeTargetQueue: .main)
expectation.fulfill()
}
waitForExpectations(timeout: 1, handler: nil)
}
func testWriteTargetQueueReadOnly() throws {
// Create a database before we perform read-only accesses
_ = try makeDatabaseQueue(filename: "test")
func test(targetQueue: DispatchQueue, writeTargetQueue: DispatchQueue) throws {
dbConfiguration.readonly = true
dbConfiguration.targetQueue = targetQueue
dbConfiguration.writeTargetQueue = writeTargetQueue // unused
let dbQueue = try makeDatabaseQueue(filename: "test")
try dbQueue.write { _ in
dispatchPrecondition(condition: .onQueue(targetQueue))
}
try dbQueue.read { _ in
dispatchPrecondition(condition: .onQueue(targetQueue))
}
}
try test(targetQueue: .global(qos: .background), writeTargetQueue: DispatchQueue(label: "writer"))
}
func testQoS() throws {
func test(qos: DispatchQoS) throws {
// https://forums.swift.org/t/what-is-the-default-target-queue-for-a-serial-queue/18094/5
//
// > [...] the default target queue [for a serial queue] is the
// > [default] overcommit [global concurrent] queue.
//
// We want this default target queue in order to test database QoS
// with dispatchPrecondition(condition:).
//
// > [...] You can get a reference to the overcommit queue by
// > dropping down to the C function dispatch_get_global_queue
// > (available in Swift with a __ prefix) and passing the private
// > value of DISPATCH_QUEUE_OVERCOMMIT.
// >
// > [...] Of course you should not do this in production code,
// > because DISPATCH_QUEUE_OVERCOMMIT is not a public API. I don't
// > know of a way to get a reference to the overcommit queue using
// > only public APIs.
let DISPATCH_QUEUE_OVERCOMMIT: UInt = 2
let targetQueue = __dispatch_get_global_queue(
Int(qos.qosClass.rawValue.rawValue),
DISPATCH_QUEUE_OVERCOMMIT)
dbConfiguration.qos = qos
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { _ in
dispatchPrecondition(condition: .onQueue(targetQueue))
}
try dbQueue.read { _ in
dispatchPrecondition(condition: .onQueue(targetQueue))
}
}
try test(qos: .background)
try test(qos: .userInitiated)
}
// MARK: - SQLITE_BUSY prevention
// See <https://github.com/groue/GRDB.swift/issues/1483>
func test_busy_timeout_does_not_prevent_SQLITE_BUSY_when_write_lock_is_acquired_by_other_connection() throws {
var configuration = dbConfiguration!
configuration.journalMode = .wal
configuration.busyMode = .timeout(1) // Does not help in this case
let dbQueue1 = try makeDatabaseQueue(filename: "test", configuration: configuration)
let dbQueue2 = try makeDatabaseQueue(filename: "test", configuration: configuration)
// DB1 DB2
// BEGIN DEFERRED TRANSACTION
// READ
let s1 = DispatchSemaphore(value: 0)
// BEGIN DEFERRED TRANSACTION
// WRITE
let s2 = DispatchSemaphore(value: 0)
// WRITE -> SQLITE_BUSY because DB2 is already holding the write lock.
let s3 = DispatchSemaphore(value: 0)
// COMMIT
let block1 = {
try! dbQueue1.inDatabase { db in
try db.inTransaction(.deferred) {
try db.execute(sql: "SELECT * FROM sqlite_master")
s1.signal()
s2.wait()
do {
try db.execute(sql: "CREATE TABLE test1(a)")
XCTFail("Expected error")
} catch DatabaseError.SQLITE_BUSY {
// Test success
}
s3.signal()
return .commit
}
}
}
let block2 = {
try! dbQueue2.inDatabase { db in
s1.wait()
try db.inTransaction(.deferred) {
try db.execute(sql: "CREATE TABLE test2(a)")
s2.signal()
s3.wait()
return .commit
}
}
}
let blocks = [block1, block2]
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
blocks[index]()
}
}
// See <https://github.com/groue/GRDB.swift/issues/1483>
func test_busy_timeout_does_not_prevent_SQLITE_BUSY_when_write_lock_was_acquired_by_other_connection() throws {
var configuration = dbConfiguration!
configuration.journalMode = .wal
configuration.busyMode = .timeout(1) // Does not help in this case
let dbQueue1 = try makeDatabaseQueue(filename: "test", configuration: configuration)
let dbQueue2 = try makeDatabaseQueue(filename: "test", configuration: configuration)
// DB1 DB2
// BEGIN DEFERRED TRANSACTION
// READ
let s1 = DispatchSemaphore(value: 0)
// WRITE
let s2 = DispatchSemaphore(value: 0)
// WRITE -> SQLITE_BUSY because write lock can't be acquired,
// even though DB2 does no longer hold any lock.
let block1 = {
try! dbQueue1.inDatabase { db in
try db.inTransaction(.deferred) {
try db.execute(sql: "SELECT * FROM sqlite_master")
s1.signal()
s2.wait()
do {
try db.execute(sql: "CREATE TABLE test1(a)")
XCTFail("Expected error")
} catch DatabaseError.SQLITE_BUSY {
// Test success
}
return .commit
}
}
}
let block2 = {
try! dbQueue2.inDatabase { db in
s1.wait()
try db.execute(sql: "CREATE TABLE test2(a)")
s2.signal()
}
}
let blocks = [block1, block2]
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
blocks[index]()
}
}
// See <https://github.com/groue/GRDB.swift/issues/1483>
func test_busy_timeout_and_IMMEDIATE_transactions_do_prevent_SQLITE_BUSY() throws {
var configuration = dbConfiguration!
// Test fails when this line is commented
configuration.defaultTransactionKind = .immediate
// Test fails when this line is commented
configuration.busyMode = .timeout(10)
let dbQueue = try makeDatabaseQueue(filename: "test")
try dbQueue.inDatabase { db in
try db.execute(sql: "PRAGMA journal_mode = wal")
try db.execute(sql: "CREATE TABLE test(a)")
}
let parallelWritesCount = 50
DispatchQueue.concurrentPerform(iterations: parallelWritesCount) { index in
let dbQueue = try! makeDatabaseQueue(filename: "test", configuration: configuration)
try! dbQueue.write { db in
_ = try Table("test").fetchCount(db)
try db.execute(sql: "INSERT INTO test VALUES (1)")
}
}
let count = try dbQueue.read(Table("test").fetchCount)
XCTAssertEqual(count, parallelWritesCount)
}
// MARK: - Closing
func testClose() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.close()
// After close, access throws SQLITE_MISUSE
do {
try dbQueue.inDatabase { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
// After close, closing is a noop
try dbQueue.close()
}
func testCloseAfterUse() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
try dbQueue.close()
// After close, access throws SQLITE_MISUSE
do {
try dbQueue.inDatabase { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
// After close, closing is a noop
try dbQueue.close()
}
func testCloseWithCachedStatement() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
_ = try db.cachedStatement(sql: "SELECT * FROM sqlite_master")
}
try dbQueue.close()
// After close, access throws SQLITE_MISUSE
do {
try dbQueue.inDatabase { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
// After close, closing is a noop
try dbQueue.close()
}
func testFailedClose() throws {
let dbQueue = try makeDatabaseQueue()
let statement = try dbQueue.inDatabase { db in
try db.makeStatement(sql: "SELECT * FROM sqlite_master")
}
try withExtendedLifetime(statement) {
do {
try dbQueue.close()
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_BUSY { }
}
XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master"))
// Database is not closed: no error
try dbQueue.inDatabase { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
}
// Regression test for <https://github.com/groue/GRDB.swift/issues/1612>
func test_releaseMemory_after_close() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.close()
dbQueue.releaseMemory()
}
}