add iOS
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user