Files

451 lines
16 KiB
Swift

import XCTest
import GRDB
class DatabasePoolTests: GRDBTestCase {
func testJournalModeConfiguration() throws {
do {
// Factory default
let config = Configuration()
let dbPool = try makeDatabasePool(filename: "factory", configuration: config)
let journalMode = try dbPool.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "wal")
}
do {
// Explicit default
var config = Configuration()
config.journalMode = .default
let dbPool = try makeDatabasePool(filename: "default", configuration: config)
let journalMode = try dbPool.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "wal")
}
do {
// Explicit wal
var config = Configuration()
config.journalMode = .wal
let dbPool = try makeDatabasePool(filename: "wal", configuration: config)
let journalMode = try dbPool.read { db in
try String.fetchOne(db, sql: "PRAGMA journal_mode")
}
XCTAssertEqual(journalMode, "wal")
}
}
func testDatabasePoolCreatesWalShm() throws {
let dbPool = try makeDatabasePool(filename: "test")
try withExtendedLifetime(dbPool) {
let fm = FileManager()
XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal"))
XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm"))
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
// A non-empty wal file makes sure ValueObservation can use wal snapshots.
// See <https://github.com/groue/GRDB.swift/issues/1383>
let walURL = URL(fileURLWithPath: dbPool.path + "-wal")
let walSize = try walURL.resourceValues(forKeys: [.fileSizeKey]).fileSize!
XCTAssertGreaterThan(walSize, 0)
#endif
}
}
func testDatabasePoolCreatesWalShmFromNonWalDatabase() throws {
do {
let dbQueue = try makeDatabaseQueue(filename: "test")
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE t(a)")
}
}
do {
let dbPool = try makeDatabasePool(filename: "test")
try withExtendedLifetime(dbPool) {
let fm = FileManager()
XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal"))
XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm"))
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
// A non-empty wal file makes sure ValueObservation can use wal snapshots.
// See <https://github.com/groue/GRDB.swift/issues/1383>
let walURL = URL(fileURLWithPath: dbPool.path + "-wal")
let walSize = try walURL.resourceValues(forKeys: [.fileSizeKey]).fileSize!
XCTAssertGreaterThan(walSize, 0)
#endif
}
}
}
func testDatabasePoolCreatesWalShmFromTruncatedWalFile() throws {
do {
let dbPool = try makeDatabasePool(filename: "test")
try dbPool.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE t(a)")
try db.checkpoint(.truncate)
}
}
do {
let dbPool = try makeDatabasePool(filename: "test")
try withExtendedLifetime(dbPool) {
let fm = FileManager()
XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal"))
XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm"))
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
// A non-empty wal file makes sure ValueObservation can use wal snapshots.
// See <https://github.com/groue/GRDB.swift/issues/1383>
let walURL = URL(fileURLWithPath: dbPool.path + "-wal")
let walSize = try walURL.resourceValues(forKeys: [.fileSizeKey]).fileSize!
XCTAssertGreaterThan(walSize, 0)
#endif
}
}
}
func testDatabasePoolCreatesWalShmFromIssue1383() throws {
let url = testBundle.url(forResource: "Issue1383", withExtension: "sqlite")!
// Delete files created by previous test runs
try? FileManager.default.removeItem(at: url.deletingLastPathComponent().appendingPathComponent("Issue1383.sqlite-wal"))
try? FileManager.default.removeItem(at: url.deletingLastPathComponent().appendingPathComponent("Issue1383.sqlite-shm"))
let dbPool = try DatabasePool(path: url.path)
try withExtendedLifetime(dbPool) {
let fm = FileManager()
XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-wal"))
XCTAssertTrue(fm.fileExists(atPath: dbPool.path + "-shm"))
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
// A non-empty wal file makes sure ValueObservation can use wal snapshots.
// See <https://github.com/groue/GRDB.swift/issues/1383>
let walURL = URL(fileURLWithPath: dbPool.path + "-wal")
let walSize = try walURL.resourceValues(forKeys: [.fileSizeKey]).fileSize!
XCTAssertGreaterThan(walSize, 0)
#endif
}
}
func testCanReadFromNewInstance() throws {
let dbPool = try makeDatabasePool()
try dbPool.read { _ in }
}
func testCanReadFromTruncatedWalFile() throws {
do {
let dbPool = try makeDatabasePool(filename: "test")
try dbPool.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE t(a)")
try db.checkpoint(.truncate)
}
}
do {
let dbPool = try makeDatabasePool(filename: "test")
let count = try dbPool.read(Table("t").fetchCount)
XCTAssertEqual(count, 0)
}
}
func testPersistentWALModeEnabled() throws {
let path: String
do {
dbConfiguration.prepareDatabase { db in
var flag: CInt = 1
let code = withUnsafeMutablePointer(to: &flag) { flagP in
sqlite3_file_control(db.sqliteConnection, nil, SQLITE_FCNTL_PERSIST_WAL, flagP)
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: ResultCode(rawValue: code))
}
}
let dbPool = try makeDatabasePool()
path = dbPool.path
}
let fm = FileManager()
XCTAssertTrue(fm.fileExists(atPath: path))
XCTAssertTrue(fm.fileExists(atPath: path + "-wal"))
XCTAssertTrue(fm.fileExists(atPath: path + "-shm"))
}
func testPersistentWALModeDisabled() throws {
let path: String
do {
dbConfiguration.prepareDatabase { db in
var flag: CInt = 0
let code = withUnsafeMutablePointer(to: &flag) { flagP in
sqlite3_file_control(db.sqliteConnection, nil, SQLITE_FCNTL_PERSIST_WAL, flagP)
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: ResultCode(rawValue: code))
}
}
let dbPool = try makeDatabasePool()
path = dbPool.path
}
let fm = FileManager()
XCTAssertTrue(fm.fileExists(atPath: path))
XCTAssertFalse(fm.fileExists(atPath: path + "-wal"))
XCTAssertFalse(fm.fileExists(atPath: path + "-shm"))
}
// Regression test
func testIssue931() throws {
dbConfiguration.prepareDatabase { db in
var flag: CInt = 0
let code = withUnsafeMutablePointer(to: &flag) { flagP in
sqlite3_file_control(db.sqliteConnection, nil, SQLITE_FCNTL_PERSIST_WAL, flagP)
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: ResultCode(rawValue: code))
}
}
let dbQueue = try makeDatabaseQueue()
var migrator = DatabaseMigrator()
migrator.registerMigration("v1", migrate: { _ in })
migrator.eraseDatabaseOnSchemaChange = true
try migrator.migrate(dbQueue)
// Trigger #931: the migrator creates a temporary database and
// calls `sqlite3_file_control` as part of the preparation function.
try migrator.migrate(dbQueue)
}
func testNumberOfThreads_asyncUnsafeRead() throws {
#if SWIFT_PACKAGE
// Can't access getThreadsCount() C function
throw XCTSkip("Thread count is not available")
#else
if getThreadsCount() < 0 {
throw XCTSkip("Thread count is not available")
}
let pool = try makeDatabasePool()
// Keep this number big, so that we have a good chance to detect
// thread explosion.
let numberOfConcurrentReads = 10000
// Wait for all concurrent reads to end
let group = DispatchGroup()
// The maximum number of threads we could witness
var maxThreadCount: CInt = 0
let lock = NSLock()
for _ in (0..<numberOfConcurrentReads) {
group.enter()
pool.asyncUnsafeRead { result in
if case let .failure(error) = result {
XCTFail("Unexpected error \(error)")
}
let threadsCount = getThreadsCount()
lock.lock()
maxThreadCount = max(maxThreadCount, threadsCount)
lock.unlock()
group.leave()
}
}
group.wait()
XCTAssert(maxThreadCount < 50)
#endif
}
func testNumberOfThreads_asyncRead() throws {
#if SWIFT_PACKAGE
// Can't access getThreadsCount() C function
throw XCTSkip("Thread count is not available")
#else
if getThreadsCount() < 0 {
throw XCTSkip("Thread count is not available")
}
let pool = try makeDatabasePool()
// Keep this number big, so that we have a good chance to detect
// thread explosion.
let numberOfConcurrentReads = 10000
// Wait for all concurrent reads to end
let group = DispatchGroup()
// The maximum number of threads we could witness
var maxThreadCount: CInt = 0
let lock = NSLock()
for _ in (0..<numberOfConcurrentReads) {
group.enter()
pool.asyncRead { result in
if case let .failure(error) = result {
XCTFail("Unexpected error \(error)")
}
let threadsCount = getThreadsCount()
lock.lock()
maxThreadCount = max(maxThreadCount, threadsCount)
lock.unlock()
group.leave()
}
}
group.wait()
XCTAssert(maxThreadCount < 50)
#endif
}
// MARK: - Closing
func testClose() throws {
let dbPool = try makeDatabasePool()
try dbPool.close()
// After close, access throws SQLITE_MISUSE
do {
try dbPool.write { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
do {
try dbPool.read { db in
_ = try Row.fetchOne(db, sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
// After close, closing is a noop
try dbPool.close()
}
func testCloseAfterUse() throws {
let dbPool = try makeDatabasePool()
try dbPool.write { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
try dbPool.read { db in
_ = try Row.fetchOne(db, sql: "SELECT * FROM sqlite_master")
}
try dbPool.close()
// After close, access throws SQLITE_MISUSE
do {
try dbPool.write { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
do {
try dbPool.read { db in
_ = try Row.fetchOne(db, sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
// After close, closing is a noop
try dbPool.close()
}
func testCloseAfterCachedStatement() throws {
let dbPool = try makeDatabasePool()
try dbPool.write { db in
_ = try db.cachedStatement(sql: "SELECT * FROM sqlite_master")
}
try dbPool.read { db in
_ = try db.cachedStatement(sql: "SELECT * FROM sqlite_master")
}
try dbPool.close()
// After close, access throws SQLITE_MISUSE
do {
try dbPool.write { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
do {
try dbPool.read { db in
_ = try Row.fetchOne(db, sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
// After close, closing is a noop
try dbPool.close()
}
func testCloseFailedDueToWriter() throws {
let dbPool = try makeDatabasePool()
let statement = try dbPool.write { db in
try db.makeStatement(sql: "SELECT * FROM sqlite_master")
}
try withExtendedLifetime(statement) {
do {
try dbPool.close()
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_BUSY { }
}
XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master"))
// Database is not closed: no error
try dbPool.write { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
try dbPool.read { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
}
func testCloseFailedDueToReader() throws {
let dbPool = try makeDatabasePool()
let statement = try dbPool.read { db in
try db.makeStatement(sql: "SELECT * FROM sqlite_master")
}
try withExtendedLifetime(statement) {
do {
try dbPool.close()
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_BUSY { }
}
// The error message can be:
// - unfinalized statement: SELECT * FROM sqlite_master
// - close deferred due to unfinalized statement: "SELECT * FROM sqlite_master"
//
// The first comes from GRDB, and the second, depending on the SQLite
// version, from `sqlite3_close_v2()`. Write the test so that it always pass:
XCTAssert(lastMessage!.contains("unfinalized statement"))
// Database is in a zombie state.
// In the zombie state, access throws SQLITE_MISUSE
do {
try dbPool.write { db in
try db.execute(sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
do {
try dbPool.read { db in
_ = try Row.fetchOne(db, sql: "SELECT * FROM sqlite_master")
}
XCTFail("Expected Error")
} catch DatabaseError.SQLITE_MISUSE { }
// In the zombie state, closing is a noop
try dbPool.close()
}
// Regression test for <https://github.com/groue/GRDB.swift/issues/1612>
func test_releaseMemory_after_close() throws {
let dbPool = try makeDatabasePool()
try dbPool.read { _ in } // Create a reader
try dbPool.close()
dbPool.releaseMemory()
}
}