309 lines
11 KiB
Swift
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)
|
|
}
|
|
}
|