435 lines
17 KiB
Swift
435 lines
17 KiB
Swift
import XCTest
|
|
@testable import GRDB
|
|
|
|
class DatabaseSnapshotTests: GRDBTestCase {
|
|
/// A helper type
|
|
private struct Counter {
|
|
init(dbPool: DatabasePool) throws {
|
|
try dbPool.write { db in
|
|
try db.execute(sql: "CREATE TABLE counter(id INTEGER PRIMARY KEY)")
|
|
}
|
|
}
|
|
|
|
func increment(_ db: Database) throws {
|
|
try db.execute(sql: "INSERT INTO counter DEFAULT VALUES")
|
|
}
|
|
|
|
func value(_ db: Database) throws -> Int {
|
|
try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM counter")!
|
|
}
|
|
}
|
|
|
|
// MARK: - Creation
|
|
|
|
func testSnapshotCanReadBeforeDatabaseModification() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try XCTAssertEqual(snapshot.read { try $0.tableExists("foo") }, false)
|
|
}
|
|
|
|
func testSnapshotCreatedFromMainQueueCanRead() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try XCTAssertEqual(snapshot.read(counter.value), 0)
|
|
}
|
|
|
|
func testSnapshotCreatedFromWriterOutsideOfTransactionCanRead() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
let snapshot = try dbPool.writeWithoutTransaction { db -> DatabaseSnapshot in
|
|
XCTAssertFalse(db.isInsideTransaction)
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try counter.increment(db)
|
|
return snapshot
|
|
}
|
|
try XCTAssertEqual(snapshot.read(counter.value), 0)
|
|
}
|
|
|
|
func testSnapshotCreatedFromReaderTransactionCanRead() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
let snapshot = try dbPool.read { db -> DatabaseSnapshot in
|
|
XCTAssertTrue(db.isInsideTransaction)
|
|
return try dbPool.makeSnapshot()
|
|
}
|
|
try XCTAssertEqual(snapshot.read(counter.value), 0)
|
|
}
|
|
|
|
func testSnapshotCreatedFromReaderOutsideOfTransactionCanRead() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
let snapshot = try dbPool.unsafeRead { db -> DatabaseSnapshot in
|
|
XCTAssertFalse(db.isInsideTransaction)
|
|
return try dbPool.makeSnapshot()
|
|
}
|
|
try XCTAssertEqual(snapshot.read(counter.value), 0)
|
|
}
|
|
|
|
func testSnapshotCreatedFromTransactionObserver() throws {
|
|
// Creating a snapshot from a didCommit callback is an important use
|
|
// case. But we know SQLite snapshots created with
|
|
// sqlite3_snapshot_get() requires a transaction. This means that
|
|
// creating a snapshot will open a transaction. We must make sure this
|
|
// transaction does not create any deadlock of reentrancy issue with
|
|
// transaction observers.
|
|
class Observer: TransactionObserver {
|
|
let dbPool: DatabasePool
|
|
var snapshot: DatabaseSnapshot
|
|
init(dbPool: DatabasePool, snapshot: DatabaseSnapshot) {
|
|
self.dbPool = dbPool
|
|
self.snapshot = snapshot
|
|
}
|
|
|
|
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { false }
|
|
func databaseDidChange(with event: DatabaseEvent) { }
|
|
func databaseDidCommit(_ db: Database) {
|
|
snapshot = try! dbPool.makeSnapshot()
|
|
}
|
|
func databaseDidRollback(_ db: Database) { }
|
|
}
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
let observer = try Observer(dbPool: dbPool, snapshot: dbPool.makeSnapshot())
|
|
dbPool.add(transactionObserver: observer)
|
|
try XCTAssertEqual(observer.snapshot.read(counter.value), 0)
|
|
try dbPool.write(counter.increment)
|
|
try XCTAssertEqual(observer.snapshot.read(counter.value), 1)
|
|
}
|
|
|
|
// MARK: - Behavior
|
|
|
|
func testSnapshotIsReadOnly() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
do {
|
|
try snapshot.read { db in
|
|
try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY")
|
|
}
|
|
XCTFail("Expected error")
|
|
} catch is DatabaseError { }
|
|
}
|
|
|
|
func testSnapshotIsImmutable() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
try dbPool.writeWithoutTransaction { db in
|
|
try counter.increment(db)
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try counter.increment(db)
|
|
try XCTAssertEqual(counter.value(db), 2)
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
try XCTAssertEqual(dbPool.read(counter.value), 2)
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
try XCTAssertEqual(counter.value(db), 2)
|
|
try XCTAssertEqual(dbPool.read(counter.value), 2)
|
|
}
|
|
}
|
|
|
|
// MARK: - Functions
|
|
|
|
func testSnapshotInheritPoolFunctions() throws {
|
|
dbConfiguration.prepareDatabase { db in
|
|
let function = DatabaseFunction("foo", argumentCount: 0, pure: true) { _ in return "foo" }
|
|
db.add(function: function)
|
|
}
|
|
let dbPool = try makeDatabasePool()
|
|
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try snapshot.read { db in
|
|
try XCTAssertEqual(String.fetchOne(db, sql: "SELECT foo()")!, "foo")
|
|
}
|
|
}
|
|
|
|
// MARK: - Collations
|
|
|
|
func testSnapshotInheritPoolCollations() throws {
|
|
dbConfiguration.prepareDatabase { db in
|
|
let collation = DatabaseCollation("reverse") { (string1, string2) in
|
|
return (string1 == string2) ? .orderedSame : ((string1 < string2) ? .orderedDescending : .orderedAscending)
|
|
}
|
|
db.add(collation: collation)
|
|
}
|
|
let dbPool = try makeDatabasePool()
|
|
|
|
try dbPool.write { db in
|
|
try db.execute(sql: "CREATE TABLE items (text TEXT)")
|
|
try db.execute(sql: "INSERT INTO items (text) VALUES ('a')")
|
|
try db.execute(sql: "INSERT INTO items (text) VALUES ('b')")
|
|
try db.execute(sql: "INSERT INTO items (text) VALUES ('c')")
|
|
}
|
|
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try snapshot.read { db in
|
|
XCTAssertEqual(try String.fetchAll(db, sql: "SELECT text FROM items ORDER BY text COLLATE reverse"), ["c", "b", "a"])
|
|
}
|
|
}
|
|
|
|
// MARK: - Concurrency
|
|
|
|
func testReadBlockIsolationStartingWithRead() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
try dbPool.write { db in
|
|
try db.execute(sql: "CREATE TABLE items (id INTEGER PRIMARY KEY)")
|
|
}
|
|
|
|
// Block 1 Block 2
|
|
// dbSnapshot.read {
|
|
// >
|
|
let s1 = DispatchSemaphore(value: 0)
|
|
// INSERT INTO items (id) VALUES (NULL)
|
|
// <
|
|
let s2 = DispatchSemaphore(value: 0)
|
|
// SELECT COUNT(*) FROM items -> 0
|
|
// >
|
|
let s3 = DispatchSemaphore(value: 0)
|
|
// INSERT INTO items (id) VALUES (NULL)
|
|
// <
|
|
let s4 = DispatchSemaphore(value: 0)
|
|
// SELECT COUNT(*) FROM items -> 0
|
|
// }
|
|
|
|
let block1 = { () in
|
|
let snapshot = try! dbPool.makeSnapshot()
|
|
try! snapshot.read { db in
|
|
s1.signal()
|
|
_ = s2.wait(timeout: .distantFuture)
|
|
// We read 0 due to snaphot isolation which was acquired before
|
|
// `s1` could let the writer insert an item.
|
|
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0)
|
|
s3.signal()
|
|
_ = s4.wait(timeout: .distantFuture)
|
|
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items")!, 0)
|
|
}
|
|
}
|
|
let block2 = { () in
|
|
do {
|
|
_ = s1.wait(timeout: .distantFuture)
|
|
try dbPool.writeWithoutTransaction { db in
|
|
try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)")
|
|
s2.signal()
|
|
_ = s3.wait(timeout: .distantFuture)
|
|
try db.execute(sql: "INSERT INTO items (id) VALUES (NULL)")
|
|
s4.signal()
|
|
}
|
|
} catch {
|
|
XCTFail("error: \(error)")
|
|
}
|
|
}
|
|
let blocks = [block1, block2]
|
|
DispatchQueue.concurrentPerform(iterations: blocks.count) { index in
|
|
blocks[index]()
|
|
}
|
|
}
|
|
|
|
func testDefaultLabel() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
|
|
let snapshot1 = try dbPool.makeSnapshot()
|
|
snapshot1.unsafeRead { db in
|
|
XCTAssertEqual(db.configuration.label, nil)
|
|
XCTAssertEqual(db.description, "GRDB.DatabasePool.snapshot.1")
|
|
|
|
// 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.DatabasePool.snapshot.1")
|
|
}
|
|
|
|
let snapshot2 = try dbPool.makeSnapshot()
|
|
snapshot2.unsafeRead { db in
|
|
XCTAssertEqual(db.configuration.label, nil)
|
|
XCTAssertEqual(db.description, "GRDB.DatabasePool.snapshot.2")
|
|
|
|
// 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.DatabasePool.snapshot.2")
|
|
}
|
|
}
|
|
|
|
func testCustomLabel() throws {
|
|
dbConfiguration.label = "Toreador"
|
|
let dbPool = try makeDatabasePool()
|
|
|
|
let snapshot1 = try dbPool.makeSnapshot()
|
|
snapshot1.unsafeRead { db in
|
|
XCTAssertEqual(db.configuration.label, "Toreador")
|
|
XCTAssertEqual(db.description, "Toreador.snapshot.1")
|
|
|
|
// 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.snapshot.1")
|
|
}
|
|
|
|
let snapshot2 = try dbPool.makeSnapshot()
|
|
snapshot2.unsafeRead { db in
|
|
XCTAssertEqual(db.configuration.label, "Toreador")
|
|
XCTAssertEqual(db.description, "Toreador.snapshot.2")
|
|
|
|
// 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.snapshot.2")
|
|
}
|
|
}
|
|
|
|
// MARK: - Checkpoints
|
|
|
|
func testAutomaticCheckpointDoesNotInvalidateSnapshot() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
try dbPool.write(counter.increment)
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
try dbPool.writeWithoutTransaction { db in
|
|
// 1000 is enough to trigger automatic snapshot
|
|
for _ in 0..<1000 {
|
|
try counter.increment(db)
|
|
}
|
|
}
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
}
|
|
|
|
func testPassiveCheckpointDoesNotInvalidateSnapshot() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
try dbPool.write(counter.increment)
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try? dbPool.writeWithoutTransaction { _ = try $0.checkpoint(.passive) } // ignore if error or not, that's not the point
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
try dbPool.write(counter.increment)
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
}
|
|
|
|
func testFullCheckpointDoesNotInvalidateSnapshot() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
try dbPool.write(counter.increment)
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try? dbPool.writeWithoutTransaction { _ = try $0.checkpoint(.full) } // ignore if error or not, that's not the point
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
try dbPool.write(counter.increment)
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
}
|
|
|
|
func testRestartCheckpointDoesNotInvalidateSnapshot() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
try dbPool.write(counter.increment)
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try? dbPool.writeWithoutTransaction { _ = try $0.checkpoint(.restart) } // ignore if error or not, that's not the point
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
try dbPool.write(counter.increment)
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
}
|
|
|
|
func testTruncateCheckpointDoesNotInvalidateSnapshot() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
let counter = try Counter(dbPool: dbPool)
|
|
try dbPool.write(counter.increment)
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try? dbPool.writeWithoutTransaction { _ = try $0.checkpoint(.truncate) } // ignore if error or not, that's not the point
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
try dbPool.write(counter.increment)
|
|
try XCTAssertEqual(snapshot.read(counter.value), 1)
|
|
}
|
|
|
|
// MARK: - Schema Cache
|
|
|
|
func testSnapshotSchemaCache() throws {
|
|
let dbPool = try makeDatabasePool()
|
|
try dbPool.write { db in
|
|
try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY)")
|
|
}
|
|
let snapshot = try dbPool.makeSnapshot()
|
|
try snapshot.read { db in
|
|
// Schema cache is updated
|
|
XCTAssertNil(db.schemaCache[.main].primaryKey("t"))
|
|
_ = try db.primaryKey("t")
|
|
XCTAssertNotNil(db.schemaCache[.main].primaryKey("t"))
|
|
}
|
|
snapshot.read { db in
|
|
// Schema cache is not cleared between reads
|
|
XCTAssertNotNil(db.schemaCache[.main].primaryKey("t"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Closing
|
|
|
|
func testClose() throws {
|
|
let snapshot = try makeDatabasePool().makeSnapshot()
|
|
try snapshot.close()
|
|
|
|
// After close, access throws SQLITE_MISUSE
|
|
do {
|
|
try snapshot.read { db in
|
|
try db.execute(sql: "SELECT * FROM sqlite_master")
|
|
}
|
|
XCTFail("Expected Error")
|
|
} catch DatabaseError.SQLITE_MISUSE { }
|
|
|
|
// After close, closing is a noop
|
|
try snapshot.close()
|
|
}
|
|
|
|
func testCloseAfterUse() throws {
|
|
let snapshot = try makeDatabasePool().makeSnapshot()
|
|
try snapshot.read { db in
|
|
try db.execute(sql: "SELECT * FROM sqlite_master")
|
|
}
|
|
try snapshot.close()
|
|
|
|
// After close, access throws SQLITE_MISUSE
|
|
do {
|
|
try snapshot.read { db in
|
|
try db.execute(sql: "SELECT * FROM sqlite_master")
|
|
}
|
|
XCTFail("Expected Error")
|
|
} catch DatabaseError.SQLITE_MISUSE { }
|
|
|
|
// After close, closing is a noop
|
|
try snapshot.close()
|
|
}
|
|
|
|
func testCloseWithCachedStatement() throws {
|
|
let snapshot = try makeDatabasePool().makeSnapshot()
|
|
try snapshot.read { db in
|
|
_ = try db.cachedStatement(sql: "SELECT * FROM sqlite_master")
|
|
}
|
|
try snapshot.close()
|
|
|
|
// After close, access throws SQLITE_MISUSE
|
|
do {
|
|
try snapshot.read { db in
|
|
try db.execute(sql: "SELECT * FROM sqlite_master")
|
|
}
|
|
XCTFail("Expected Error")
|
|
} catch DatabaseError.SQLITE_MISUSE { }
|
|
|
|
// After close, closing is a noop
|
|
try snapshot.close()
|
|
}
|
|
|
|
func testFailedClose() throws {
|
|
let snapshot = try makeDatabasePool().makeSnapshot()
|
|
let statement = try snapshot.read { db in
|
|
try db.makeStatement(sql: "SELECT * FROM sqlite_master")
|
|
}
|
|
|
|
try withExtendedLifetime(statement) {
|
|
do {
|
|
try snapshot.close()
|
|
XCTFail("Expected Error")
|
|
} catch DatabaseError.SQLITE_BUSY { }
|
|
}
|
|
XCTAssert(lastMessage!.contains("unfinalized statement: SELECT * FROM sqlite_master"))
|
|
|
|
// Database is not closed: no error
|
|
try snapshot.read { db in
|
|
try db.execute(sql: "SELECT * FROM sqlite_master")
|
|
}
|
|
}
|
|
}
|