Files

309 lines
11 KiB
Swift

import XCTest
import GRDB
class DatabaseConfigurationTests: GRDBTestCase {
// MARK: - prepareDatabase
func testPrepareDatabase() throws {
// prepareDatabase is called when connection opens
var connectionCount = 0
var configuration = Configuration()
configuration.prepareDatabase { db in
connectionCount += 1
}
_ = try DatabaseQueue(configuration: configuration)
XCTAssertEqual(connectionCount, 1)
_ = try makeDatabaseQueue(configuration: configuration)
XCTAssertEqual(connectionCount, 2)
let pool = try makeDatabasePool(configuration: configuration)
XCTAssertEqual(connectionCount, 3)
try pool.read { _ in }
XCTAssertEqual(connectionCount, 4)
try pool.makeSnapshot().read { _ in }
XCTAssertEqual(connectionCount, 5)
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
try pool.makeSnapshotPool().read { _ in }
XCTAssertEqual(connectionCount, 6)
#endif
}
func testPrepareDatabaseError() throws {
struct TestError: Error { }
var error: TestError?
var configuration = Configuration()
configuration.prepareDatabase { db in
if let error {
throw error
}
}
// TODO: what about in-memory DatabaseQueue???
do {
error = TestError()
_ = try makeDatabaseQueue(configuration: configuration)
XCTFail("Expected TestError")
} catch is TestError { }
do {
error = TestError()
_ = try makeDatabasePool(configuration: configuration)
XCTFail("Expected TestError")
} catch is TestError { }
do {
error = nil
let pool = try makeDatabasePool(configuration: configuration)
do {
error = TestError()
try pool.read { _ in }
XCTFail("Expected TestError")
} catch is TestError { }
do {
error = TestError()
_ = try pool.makeSnapshot()
XCTFail("Expected TestError")
} catch is TestError { }
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
do {
error = TestError()
_ = try pool.makeSnapshotPool()
XCTFail("Expected TestError")
} catch is TestError { }
#endif
}
}
// MARK: - acceptsDoubleQuotedStringLiterals
func testAcceptsDoubleQuotedStringLiteralsDefault() throws {
let configuration = Configuration()
XCTAssertFalse(configuration.acceptsDoubleQuotedStringLiterals)
}
func testAcceptsDoubleQuotedStringLiteralsTrue() throws {
var configuration = Configuration()
configuration.acceptsDoubleQuotedStringLiterals = true
let dbQueue = try makeDatabaseQueue(configuration: configuration)
try dbQueue.inDatabase { db in
try db.execute(sql: """
CREATE TABLE player(name TEXT);
INSERT INTO player DEFAULT VALUES;
""")
}
// Test SQLITE_DBCONFIG_DQS_DML
let foo = try dbQueue.inDatabase { db in
try String.fetchOne(db, sql: "SELECT \"foo\" FROM player")
}
XCTAssertEqual(foo, "foo")
// Test SQLITE_DBCONFIG_DQS_DDL
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE INDEX i ON player(\"foo\")")
}
}
func testAcceptsDoubleQuotedStringLiteralsFalse() throws {
var configuration = Configuration()
configuration.acceptsDoubleQuotedStringLiterals = false
let dbQueue = try makeDatabaseQueue(configuration: configuration)
try dbQueue.inDatabase { db in
try db.execute(sql: """
CREATE TABLE player(name TEXT);
INSERT INTO player DEFAULT VALUES;
""")
}
// Test SQLITE_DBCONFIG_DQS_DML
do {
let foo = try dbQueue.inDatabase { db in
try String.fetchOne(db, sql: "SELECT \"foo\" FROM player")
}
if sqlite3_libversion_number() >= 3029000 {
XCTFail("Expected error")
} else {
XCTAssertEqual(foo, "foo")
}
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.sql, "SELECT \"foo\" FROM player")
}
// Test SQLITE_DBCONFIG_DQS_DDL
do {
try dbQueue.inDatabase { db in
try db.execute(sql: "CREATE INDEX i ON player(\"foo\")")
}
if sqlite3_libversion_number() >= 3029000 {
XCTFail("Expected error")
}
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_ERROR)
XCTAssertEqual(error.sql, "CREATE INDEX i ON player(\"foo\")")
}
}
// MARK: - busyMode
func testBusyModeImmediate() throws {
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
#if GRDBCIPHER_USE_ENCRYPTION
// Work around SQLCipher bug when two connections are open to the
// same empty database: make sure the database is not empty before
// running this test
try dbQueue1.inDatabase { db in
try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)")
}
#endif
var configuration2 = dbQueue1.configuration
configuration2.busyMode = .immediateError
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite", configuration: configuration2)
let s1 = DispatchSemaphore(value: 0)
let s2 = DispatchSemaphore(value: 0)
let queue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
queue.async(group: group) {
do {
try dbQueue1.inTransaction(.exclusive) { db in
s2.signal()
queue.asyncAfter(deadline: .now() + 1) {
s1.signal()
}
_ = s1.wait(timeout: .distantFuture)
return .commit
}
} catch {
XCTFail("\(error)")
}
}
queue.async(group: group) {
do {
_ = s2.wait(timeout: .distantFuture)
try dbQueue2.inTransaction(.exclusive) { db in return .commit }
XCTFail("Expected error")
} catch DatabaseError.SQLITE_BUSY {
} catch {
XCTFail("\(error)")
}
}
_ = group.wait(timeout: .distantFuture)
}
func testBusyModeTimeoutTooShort() throws {
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
#if GRDBCIPHER_USE_ENCRYPTION
// Work around SQLCipher bug when two connections are open to the
// same empty database: make sure the database is not empty before
// running this test
try dbQueue1.inDatabase { db in
try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)")
}
#endif
var configuration2 = dbQueue1.configuration
configuration2.busyMode = .timeout(0.1)
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite", configuration: configuration2)
let s1 = DispatchSemaphore(value: 0)
let s2 = DispatchSemaphore(value: 0)
let queue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
queue.async(group: group) {
do {
try dbQueue1.inTransaction(.exclusive) { db in
// Let dbQueue2 attempt at opening an exclusive transaction
s2.signal()
// Wait for dbQueue2 to fail 😈
_ = s1.wait(timeout: .distantFuture)
return .commit
}
} catch {
XCTFail("\(error)")
}
}
queue.async(group: group) {
do {
// Wait for dbQueue1 to start an exclusive transaction
_ = s2.wait(timeout: .distantFuture)
try dbQueue2.inTransaction(.exclusive) { db in return .commit }
XCTFail("Expected error")
} catch DatabaseError.SQLITE_BUSY {
} catch {
XCTFail("\(error)")
}
// Let dbQueue1 end its transaction
s1.signal()
}
_ = group.wait(timeout: .distantFuture)
}
func testBusyModeTimeoutTooLong() throws {
let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite")
#if GRDBCIPHER_USE_ENCRYPTION
// Work around SQLCipher bug when two connections are open to the
// same empty database: make sure the database is not empty before
// running this test
try dbQueue1.inDatabase { db in
try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)")
}
#endif
var configuration2 = dbQueue1.configuration
configuration2.busyMode = .timeout(1)
let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite", configuration: configuration2)
let s1 = DispatchSemaphore(value: 0)
let s2 = DispatchSemaphore(value: 0)
let queue = DispatchQueue.global(qos: .default)
let group = DispatchGroup()
queue.async(group: group) {
do {
try dbQueue1.inTransaction(.exclusive) { db in
// Let dbQueue2 attempt at opening an exclusive transaction
s2.signal()
// Wait for a delay and end dbQueue1 transaction
queue.asyncAfter(deadline: .now() + 0.1) {
s1.signal()
}
_ = s1.wait(timeout: .distantFuture)
return .commit
}
} catch {
XCTFail("\(error)")
}
}
queue.async(group: group) {
do {
// Wait for dbQueue1 to start an exclusive transaction
_ = s2.wait(timeout: .distantFuture)
try dbQueue2.inTransaction(.exclusive) { db in return .commit }
} catch {
XCTFail("\(error)")
}
}
_ = group.wait(timeout: .distantFuture)
}
}