Files

2601 lines
112 KiB
Swift

import XCTest
@testable import GRDB
private class Observer : TransactionObserver {
var lastCommittedEvents: [DatabaseEvent] = []
var events: [DatabaseEvent] = []
var commitError: Error?
var deinitBlock: (() -> ())?
var didCommitBlock: ((Database) -> ())?
var observesBlock: (DatabaseEventKind) -> Bool
init(observes observesBlock: @escaping (DatabaseEventKind) -> Bool = { _ in true }, didCommitBlock: ((Database) -> ())? = nil, deinitBlock: (() -> ())? = nil) {
self.observesBlock = observesBlock
self.didCommitBlock = didCommitBlock
self.deinitBlock = deinitBlock
}
deinit {
if let deinitBlock {
deinitBlock()
}
}
var didChangeCount: Int = 0
var didChangeWithEventCount: Int = 0
var willCommitCount: Int = 0
var didCommitCount: Int = 0
var didRollbackCount: Int = 0
func resetCounts() {
didChangeCount = 0
didChangeWithEventCount = 0
willCommitCount = 0
didCommitCount = 0
didRollbackCount = 0
#if SQLITE_ENABLE_PREUPDATE_HOOK
willChangeCount = 0
#endif
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
var willChangeCount: Int = 0
var lastCommittedPreUpdateEvents: [DatabasePreUpdateEvent] = []
var preUpdateEvents: [DatabasePreUpdateEvent] = []
func databaseWillChange(with event: DatabasePreUpdateEvent) {
willChangeCount += 1
preUpdateEvents.append(event.copy())
}
#endif
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
observesBlock(eventKind)
}
func databaseDidChange() {
didChangeCount += 1
}
func databaseDidChange(with event: DatabaseEvent) {
didChangeWithEventCount += 1
events.append(event.copy())
}
func databaseWillCommit() throws {
willCommitCount += 1
if let commitError {
throw commitError
}
}
func databaseDidCommit(_ db: Database) {
didCommitCount += 1
lastCommittedEvents = events
events = []
#if SQLITE_ENABLE_PREUPDATE_HOOK
lastCommittedPreUpdateEvents = preUpdateEvents
preUpdateEvents = []
#endif
didCommitBlock?(db)
}
func databaseDidRollback(_ db: Database) {
didRollbackCount += 1
lastCommittedEvents = []
events = []
#if SQLITE_ENABLE_PREUPDATE_HOOK
lastCommittedPreUpdateEvents = []
preUpdateEvents = []
#endif
}
}
private final class Artist: Codable {
var id: Int64?
var name: String?
init(id: Int64?, name: String?) {
self.id = id
self.name = name
}
}
extension Artist : FetchableRecord, PersistableRecord {
static let databaseTableName = "artists"
func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
private final class Artwork : Codable {
var id: Int64?
var artistId: Int64?
var title: String?
init(id: Int64?, artistId: Int64?, title: String?) {
self.id = id
self.artistId = artistId
self.title = title
}
}
extension Artwork : FetchableRecord, PersistableRecord {
static let databaseTableName = "artworks"
func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
class TransactionObserverTests: GRDBTestCase {
private func setupArtistDatabase(in dbWriter: some DatabaseWriter) throws {
try dbWriter.write { db in
try db.execute(sql: """
CREATE TABLE artists (
id INTEGER PRIMARY KEY,
name TEXT);
CREATE TABLE artworks (
id INTEGER PRIMARY KEY,
artistId INTEGER NOT NULL REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE,
title TEXT)
""")
}
}
private func match(event: DatabaseEvent, kind: DatabaseEvent.Kind, tableName: String, rowId: Int64) -> Bool {
(event.tableName == tableName) && (event.rowID == rowId) && (event.kind == kind)
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
private func match(preUpdateEvent event: DatabasePreUpdateEvent, kind: DatabasePreUpdateEvent.Kind, tableName: String, initialRowID: Int64?, finalRowID: Int64?, initialValues: [DatabaseValue]?, finalValues: [DatabaseValue]?, depth: CInt = 0) -> Bool {
func check(_ dbValues: [DatabaseValue]?, expected: [DatabaseValue]?) -> Bool {
if let dbValues {
guard let expected else { return false }
return dbValues == expected
}
else { return expected == nil }
}
var count : Int = 0
if let initialValues { count = initialValues.count }
if let finalValues { count = max(count, finalValues.count) }
guard (event.kind == kind) else { return false }
guard (event.tableName == tableName) else { return false }
guard (event.count == count) else { return false }
guard (event.depth == depth) else { return false }
guard (event.initialRowID == initialRowID) else { return false }
guard (event.finalRowID == finalRowID) else { return false }
guard check(event.initialDatabaseValues, expected: initialValues) else { return false }
guard check(event.finalDatabaseValues, expected: finalValues) else { return false }
return true
}
#endif
// MARK: - Transaction completion
func testTransactionCompletions() throws {
// implicit transaction
try assertTransaction(start: "", end: "CREATE TABLE t(a)", isNotifiedAs: .commit)
// explicit commit
try assertTransaction(start: "BEGIN DEFERRED TRANSACTION", end: "COMMIT", isNotifiedAs: .commit)
try assertTransaction(start: "BEGIN DEFERRED TRANSACTION; CREATE TABLE t(a)", end: "COMMIT", isNotifiedAs: .commit)
try assertTransaction(start: "BEGIN IMMEDIATE TRANSACTION", end: "COMMIT", isNotifiedAs: .commit)
try assertTransaction(start: "BEGIN IMMEDIATE TRANSACTION; CREATE TABLE t(a)", end: "COMMIT", isNotifiedAs: .commit)
try assertTransaction(start: "BEGIN EXCLUSIVE TRANSACTION", end: "COMMIT", isNotifiedAs: .commit)
try assertTransaction(start: "BEGIN EXCLUSIVE TRANSACTION; CREATE TABLE t(a)", end: "COMMIT", isNotifiedAs: .commit)
try assertTransaction(start: "SAVEPOINT test", end: "COMMIT", isNotifiedAs: .commit)
try assertTransaction(start: "SAVEPOINT test; CREATE TABLE t(a)", end: "COMMIT", isNotifiedAs: .commit)
try assertTransaction(start: "SAVEPOINT test; ROLLBACK TRANSACTION TO SAVEPOINT test", end: "RELEASE SAVEPOINT test", isNotifiedAs: .commit)
try assertTransaction(start: "SAVEPOINT test", end: "RELEASE SAVEPOINT test", isNotifiedAs: .commit)
// These tests fail with SQLCipher 4, and Xcode 12 beta (both debug and release configuration)
// TODO: is it GRDB or SQLCipher/SQLite?
// try assertTransaction(start: "SAVEPOINT test; CREATE TABLE t(a); ROLLBACK TRANSACTION TO SAVEPOINT test", end: "RELEASE SAVEPOINT test", isNotifiedAs: .commit)
// try assertTransaction(start: "SAVEPOINT test; CREATE TABLE t(a)", end: "RELEASE SAVEPOINT test", isNotifiedAs: .commit)
// explicit rollback
try assertTransaction(start: "BEGIN DEFERRED TRANSACTION", end: "ROLLBACK", isNotifiedAs: .rollback)
try assertTransaction(start: "BEGIN DEFERRED TRANSACTION; CREATE TABLE t(a)", end: "ROLLBACK", isNotifiedAs: .rollback)
try assertTransaction(start: "BEGIN IMMEDIATE TRANSACTION", end: "ROLLBACK", isNotifiedAs: .rollback)
try assertTransaction(start: "BEGIN IMMEDIATE TRANSACTION; CREATE TABLE t(a)", end: "ROLLBACK", isNotifiedAs: .rollback)
try assertTransaction(start: "BEGIN EXCLUSIVE TRANSACTION", end: "ROLLBACK", isNotifiedAs: .rollback)
try assertTransaction(start: "BEGIN EXCLUSIVE TRANSACTION; CREATE TABLE t(a)", end: "ROLLBACK", isNotifiedAs: .rollback)
try assertTransaction(start: "SAVEPOINT test", end: "ROLLBACK", isNotifiedAs: .rollback)
try assertTransaction(start: "SAVEPOINT test; CREATE TABLE t(a)", end: "ROLLBACK", isNotifiedAs: .rollback)
}
private func assertTransaction(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
try assertTransaction_registerBefore(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
try assertTransaction_registerBetween(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
try assertTransaction_extentDefault(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
try assertTransaction_extentObserverLifetime(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
try assertTransaction_extentUntilNextTransaction_weakObserver(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
try assertTransaction_extentUntilNextTransaction_retainedObserver(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
try assertTransaction_extentUntilNextTransaction_triggeredTransaction(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
try assertTransaction_extentDatabaseLifeTime(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
try assertTransaction_throwingObserver(start: startSQL, end: endSQL, isNotifiedAs: expectedCompletion)
}
private func assertTransaction_registerBefore(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
let extents: [Database.TransactionObservationExtent] = [.observerLifetime, .databaseLifetime, .nextTransaction]
for extent in extents {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer, extent: extent)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
}
}
private func assertTransaction_registerBetween(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
let extents: [Database.TransactionObservationExtent] = [.observerLifetime, .databaseLifetime, .nextTransaction]
for extent in extents {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
db.add(transactionObserver: observer, extent: extent)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
}
}
private func assertTransaction_extentDefault(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
weak var weakObserver: Observer? = nil
let dbQueue = try makeDatabaseQueue()
do {
let observer = Observer()
weakObserver = observer
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
try dbQueue.inTransaction { db in
try db.execute(sql: "DROP TABLE IF EXISTS t; CREATE TABLE t(a)")
return .commit
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 2, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 2, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
}
XCTAssert(weakObserver == nil)
}
func assertTransaction_extentObserverLifetime(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
weak var weakObserver: Observer? = nil
let dbQueue = try makeDatabaseQueue()
do {
let observer = Observer()
weakObserver = observer
dbQueue.add(transactionObserver: observer, extent: .observerLifetime)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
try dbQueue.inTransaction { db in
try db.execute(sql: "DROP TABLE IF EXISTS t; CREATE TABLE t(a)")
return .commit
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 2, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 2, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
}
XCTAssert(weakObserver == nil)
}
func assertTransaction_extentUntilNextTransaction_weakObserver(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
weak var weakObserver: Observer? = nil
let dbQueue = try makeDatabaseQueue()
do {
let observer = Observer()
weakObserver = observer
dbQueue.add(transactionObserver: observer, extent: .nextTransaction)
}
if let observer = weakObserver {
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
} else {
XCTFail("observer should not be deallocated until next transaction")
}
XCTAssert(weakObserver == nil)
}
func assertTransaction_extentUntilNextTransaction_retainedObserver(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer, extent: .nextTransaction)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
try dbQueue.inTransaction { db in
try db.execute(sql: "DROP TABLE IF EXISTS t; CREATE TABLE t(a)")
return .commit
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
}
func assertTransaction_extentUntilNextTransaction_triggeredTransaction(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
let dbQueue = try makeDatabaseQueue()
let witness = Observer()
let observer = Observer(didCommitBlock: { db in
try! db.inTransaction {
try db.execute(sql: "DROP TABLE IF EXISTS t; CREATE TABLE t(a)")
return .commit
}
})
dbQueue.add(transactionObserver: witness)
dbQueue.add(transactionObserver: observer, extent: .nextTransaction)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.willCommitCount, 2, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.didCommitCount, 2, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
try dbQueue.inTransaction { db in
try db.execute(sql: "DROP TABLE IF EXISTS t; CREATE TABLE t(a)")
return .commit
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.willCommitCount, 3, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.didCommitCount, 3, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(witness.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
}
func assertTransaction_extentDatabaseLifeTime(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
// Observer deallocation happens concurrently with database deallocation:
//
// 1. DatabaseQueue.deinit (main queue)
// 2. SerializedDatabase.deinit (main queue)
// 3. DispatchQueue.deinit (main queue)
// 4. SchedulingWatchDog.deinit (database queue)
// 5. Database.deinit (database queue)
// 6. Observer.deinit (database queue)
let deinitSemaphore = DispatchSemaphore(value: 0)
do {
let dbQueue = try makeDatabaseQueue()
weak var weakObserver: Observer? = nil
do {
let observer = Observer(deinitBlock: {
deinitSemaphore.signal()
})
weakObserver = observer
dbQueue.add(transactionObserver: observer, extent: .databaseLifetime)
}
if let observer = weakObserver {
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
} else {
XCTFail("observer should not be deallocated until database is closed")
}
if let observer = weakObserver {
try dbQueue.inTransaction { db in
try db.execute(sql: "DROP TABLE IF EXISTS t; CREATE TABLE t(a)")
return .commit
}
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer.willCommitCount, 2, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 2, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 0, "\(startSQL); \(endSQL)")
case .rollback:
XCTAssertEqual(observer.willCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didCommitCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
} else {
XCTFail("observer should not be deallocated until database is closed")
}
} // <- DatabaseQueue.deinit ... Observer.deinit
// Wait for observer deallocation
switch deinitSemaphore.wait(timeout: .now() + .seconds(60)) {
case .success:
break
case .timedOut:
XCTFail("Observer not deallocated")
}
}
func assertTransaction_throwingObserver(start startSQL: String, end endSQL: String, isNotifiedAs expectedCompletion: Database.TransactionCompletion) throws {
let extents: [Database.TransactionObservationExtent] = [.observerLifetime, .databaseLifetime, .nextTransaction]
for extent in extents {
let dbQueue = try makeDatabaseQueue()
let observer1 = Observer()
let observer2 = Observer()
let observer3 = Observer()
struct TestError : Error { }
observer2.commitError = TestError()
dbQueue.add(transactionObserver: observer1, extent: extent)
dbQueue.add(transactionObserver: observer2, extent: extent)
dbQueue.add(transactionObserver: observer3, extent: extent)
do {
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: startSQL)
try db.execute(sql: endSQL)
}
switch expectedCompletion {
case .commit:
XCTFail("Expected Error")
case .rollback:
XCTAssertEqual(observer1.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer1.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer1.didRollbackCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer2.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer2.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer2.didRollbackCount, 1, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer3.willCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer3.didCommitCount, 0, "\(startSQL); \(endSQL)")
XCTAssertEqual(observer3.didRollbackCount, 1, "\(startSQL); \(endSQL)")
}
} catch let error as TestError {
switch expectedCompletion {
case .commit:
XCTAssertEqual(observer1.willCommitCount, 1)
XCTAssertEqual(observer1.didCommitCount, 0)
XCTAssertEqual(observer1.didRollbackCount, 1)
XCTAssertEqual(observer2.willCommitCount, 1)
XCTAssertEqual(observer2.didCommitCount, 0)
XCTAssertEqual(observer2.didRollbackCount, 1)
XCTAssertEqual(observer3.willCommitCount, 0)
XCTAssertEqual(observer3.didCommitCount, 0)
XCTAssertEqual(observer3.didRollbackCount, 1)
case .rollback:
throw error
}
}
}
}
// MARK: - Events
func testInsertEvent() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
//
try artist.save(db)
XCTAssertEqual(observer.lastCommittedEvents.count, 1)
let event = observer.lastCommittedEvents.filter { event in
self.match(event: event, kind: .insert, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 1)
let preUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artists", initialRowID: nil, finalRowID: artist.id!, initialValues: nil,
finalValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
])
}.first
XCTAssertTrue(preUpdateEvent != nil)
#endif
}
}
func testInsertEventWithCursor() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
let insertedName = "Gerhard Richter"
let statement = try db.makeStatement(literal: "INSERT INTO artists (name) VALUES (\(insertedName))")
_ = try Row.fetchCursor(statement).next()
let insertedId = db.lastInsertedRowID
XCTAssertEqual(observer.lastCommittedEvents.count, 1)
let event = observer.lastCommittedEvents.filter { event in
self.match(event: event, kind: .insert, tableName: "artists", rowId: insertedId)
}.first
XCTAssertTrue(event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 1)
let preUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artists", initialRowID: nil, finalRowID: insertedId, initialValues: nil,
finalValues: [
insertedId.databaseValue,
insertedName.databaseValue
])
}.first
XCTAssertTrue(preUpdateEvent != nil)
#endif
}
}
func testUpdateEvent() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
try artist.save(db)
artist.name = "Vincent Fournier"
//
try artist.save(db)
XCTAssertEqual(observer.lastCommittedEvents.count, 1)
let event = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .update, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 1)
let preUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .update, tableName: "artists", initialRowID: artist.id!, finalRowID: artist.id!,
initialValues: [
artist.id!.databaseValue,
"Gerhard Richter".databaseValue
], finalValues: [
artist.id!.databaseValue,
"Vincent Fournier".databaseValue
])
}.first
XCTAssertTrue(preUpdateEvent != nil)
#endif
}
}
func testDeleteEvent() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
try artist.save(db)
//
try artist.delete(db)
XCTAssertEqual(observer.lastCommittedEvents.count, 1)
let event = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 1)
let preUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artists", initialRowID: artist.id!, finalRowID: nil,
initialValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
], finalValues: nil)
}.first
XCTAssertTrue(preUpdateEvent != nil)
#endif
}
}
func testCascadingDeleteEvents() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
try artist.save(db)
let artwork1 = Artwork(id: nil, artistId: artist.id, title: "Cloud")
try artwork1.save(db)
let artwork2 = Artwork(id: nil, artistId: artist.id, title: "Ema (Nude on a Staircase)")
try artwork2.save(db)
//
try artist.delete(db)
XCTAssertEqual(observer.lastCommittedEvents.count, 3)
let artistEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(artistEvent != nil)
let artwork1Event = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artworks", rowId: artwork1.id!)
}.first
XCTAssertTrue(artwork1Event != nil)
let artwork2Event = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artworks", rowId: artwork2.id!)
}.first
XCTAssertTrue(artwork2Event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 3)
let artistPreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artists", initialRowID: artist.id!, finalRowID: nil,
initialValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
], finalValues: nil)
}.first
XCTAssertTrue(artistPreUpdateEvent != nil)
let artwork1PreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artworks", initialRowID: artwork1.id!, finalRowID: nil,
initialValues: [
artwork1.id!.databaseValue,
artwork1.artistId!.databaseValue,
artwork1.title!.databaseValue
], finalValues: nil, depth: 1)
}.first
XCTAssertTrue(artwork1PreUpdateEvent != nil)
let artwork2PreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artworks", initialRowID: artwork2.id!, finalRowID: nil,
initialValues: [
artwork2.id!.databaseValue,
artwork2.artistId!.databaseValue,
artwork2.title!.databaseValue
], finalValues: nil, depth: 1)
}.first
XCTAssertTrue(artwork2PreUpdateEvent != nil)
#endif
}
}
// MARK: - Commits & Rollback
func testImplicitTransactionCommit() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
let artist = Artist(id: nil, name: "Gerhard Richter")
try dbQueue.writeWithoutTransaction { db in
observer.resetCounts()
try artist.save(db)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
}
}
func testCascadeWithImplicitTransactionCommit() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
let artist = Artist(id: nil, name: "Gerhard Richter")
let artwork1 = Artwork(id: nil, artistId: nil, title: "Cloud")
let artwork2 = Artwork(id: nil, artistId: nil, title: "Ema (Nude on a Staircase)")
try dbQueue.writeWithoutTransaction { db in
try artist.save(db)
artwork1.artistId = artist.id
artwork2.artistId = artist.id
try artwork1.save(db)
try artwork2.save(db)
//
observer.resetCounts()
try artist.delete(db)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 3) // 3 deletes
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 3) // 3 deletes
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
let artistDeleteEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(artistDeleteEvent != nil)
let artwork1DeleteEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artworks", rowId: artwork1.id!)
}.first
XCTAssertTrue(artwork1DeleteEvent != nil)
let artwork2DeleteEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artworks", rowId: artwork2.id!)
}.first
XCTAssertTrue(artwork2DeleteEvent != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 3)
let artistPreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artists", initialRowID: artist.id!, finalRowID: nil,
initialValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
], finalValues: nil)
}.first
XCTAssertTrue(artistPreUpdateEvent != nil)
let artwork1PreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artworks", initialRowID: artwork1.id!, finalRowID: nil,
initialValues: [
artwork1.id!.databaseValue,
artwork1.artistId!.databaseValue,
artwork1.title!.databaseValue
], finalValues: nil, depth: 1)
}.first
XCTAssertTrue(artwork1PreUpdateEvent != nil)
let artwork2PreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artworks", initialRowID: artwork2.id!, finalRowID: nil,
initialValues: [
artwork2.id!.databaseValue,
artwork2.artistId!.databaseValue,
artwork2.title!.databaseValue
], finalValues: nil, depth: 1)
}.first
XCTAssertTrue(artwork2PreUpdateEvent != nil)
#endif
}
}
func testExplicitTransactionCommit() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
let artist = Artist(id: nil, name: "Gerhard Richter")
let artwork1 = Artwork(id: nil, artistId: nil, title: "Cloud")
let artwork2 = Artwork(id: nil, artistId: nil, title: "Ema (Nude on a Staircase)")
try dbQueue.inTransaction { db in
observer.resetCounts()
try artist.save(db)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
artwork1.artistId = artist.id
artwork2.artistId = artist.id
observer.resetCounts()
try artwork1.save(db)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
observer.resetCounts()
try artwork2.save(db)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
observer.resetCounts()
return .commit
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 3)
let artistEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .insert, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(artistEvent != nil)
let artwork1Event = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .insert, tableName: "artworks", rowId: artwork1.id!)
}.first
XCTAssertTrue(artwork1Event != nil)
let artwork2Event = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .insert, tableName: "artworks", rowId: artwork2.id!)
}.first
XCTAssertTrue(artwork2Event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 3)
let artistPreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artists", initialRowID: nil, finalRowID: artist.id!,
initialValues: nil, finalValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
])
}.first
XCTAssertTrue(artistPreUpdateEvent != nil)
let artwork1PreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artworks", initialRowID: nil, finalRowID: artwork1.id!,
initialValues: nil, finalValues: [
artwork1.id!.databaseValue,
artwork1.artistId!.databaseValue,
artwork1.title!.databaseValue
])
}.first
XCTAssertTrue(artwork1PreUpdateEvent != nil)
let artwork2PreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artworks", initialRowID: nil, finalRowID: artwork2.id!,
initialValues: nil, finalValues: [
artwork2.id!.databaseValue,
artwork2.artistId!.databaseValue,
artwork2.title!.databaseValue
])
}.first
XCTAssertTrue(artwork2PreUpdateEvent != nil)
#endif
}
func testCascadeWithExplicitTransactionCommit() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
let artist = Artist(id: nil, name: "Gerhard Richter")
let artwork1 = Artwork(id: nil, artistId: nil, title: "Cloud")
let artwork2 = Artwork(id: nil, artistId: nil, title: "Ema (Nude on a Staircase)")
try dbQueue.inTransaction { db in
try artist.save(db)
artwork1.artistId = artist.id
artwork2.artistId = artist.id
try artwork1.save(db)
try artwork2.save(db)
//
observer.resetCounts()
try artist.delete(db)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 3) // 3 deletes
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 3) // 3 deletes
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
observer.resetCounts()
return .commit
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 6) // 3 inserts, and 3 deletes
let artistInsertEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .insert, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(artistInsertEvent != nil)
let artwork1InsertEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .insert, tableName: "artworks", rowId: artwork1.id!)
}.first
XCTAssertTrue(artwork1InsertEvent != nil)
let artwork2InsertEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .insert, tableName: "artworks", rowId: artwork2.id!)
}.first
XCTAssertTrue(artwork2InsertEvent != nil)
let artistDeleteEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(artistDeleteEvent != nil)
let artwork1DeleteEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artworks", rowId: artwork1.id!)
}.first
XCTAssertTrue(artwork1DeleteEvent != nil)
let artwork2DeleteEvent = observer.lastCommittedEvents.filter {
self.match(event: $0, kind: .delete, tableName: "artworks", rowId: artwork2.id!)
}.first
XCTAssertTrue(artwork2DeleteEvent != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 6) // 3 inserts, and 3 deletes
let artistInsertPreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artists", initialRowID: nil, finalRowID: artist.id!,
initialValues: nil, finalValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
])
}.first
XCTAssertTrue(artistInsertPreUpdateEvent != nil)
let artwork1InsertPreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artworks", initialRowID: nil, finalRowID: artwork1.id!,
initialValues: nil, finalValues: [
artwork1.id!.databaseValue,
artwork1.artistId!.databaseValue,
artwork1.title!.databaseValue
])
}.first
XCTAssertTrue(artwork1InsertPreUpdateEvent != nil)
let artwork2InsertPreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artworks", initialRowID: nil, finalRowID: artwork2.id!,
initialValues: nil, finalValues: [
artwork2.id!.databaseValue,
artwork2.artistId!.databaseValue,
artwork2.title!.databaseValue
])
}.first
XCTAssertTrue(artwork2InsertPreUpdateEvent != nil)
let artistDeletePreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artists", initialRowID: artist.id!, finalRowID: nil,
initialValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
], finalValues: nil)
}.first
XCTAssertTrue(artistDeletePreUpdateEvent != nil)
let artwork1DeletePreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artworks", initialRowID: artwork1.id!, finalRowID: nil,
initialValues: [
artwork1.id!.databaseValue,
artwork1.artistId!.databaseValue,
artwork1.title!.databaseValue
], finalValues: nil, depth: 1)
}.first
XCTAssertTrue(artwork1DeletePreUpdateEvent != nil)
let artwork2DeletePreUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .delete, tableName: "artworks", initialRowID: artwork2.id!, finalRowID: nil,
initialValues: [
artwork2.id!.databaseValue,
artwork2.artistId!.databaseValue,
artwork2.title!.databaseValue
], finalValues: nil, depth: 1)
}.first
XCTAssertTrue(artwork2DeletePreUpdateEvent != nil)
#endif
}
func testExplicitTransactionRollback() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
let artist = Artist(id: nil, name: "Gerhard Richter")
let artwork1 = Artwork(id: nil, artistId: nil, title: "Cloud")
let artwork2 = Artwork(id: nil, artistId: nil, title: "Ema (Nude on a Staircase)")
try dbQueue.inTransaction { db in
try artist.save(db)
artwork1.artistId = artist.id
artwork2.artistId = artist.id
try artwork1.save(db)
try artwork2.save(db)
observer.resetCounts()
return .rollback
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 1)
}
func testImplicitTransactionRollbackCausedByDatabaseError() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
do {
try dbQueue.writeWithoutTransaction { db in
do {
try Artwork(id: nil, artistId: nil, title: "meh").save(db)
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
throw error
}
}
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
}
}
func testExplicitTransactionRollbackCausedByDatabaseError() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
do {
try dbQueue.inTransaction { db in
do {
try Artwork(id: nil, artistId: nil, title: "meh").save(db)
XCTFail("Expected Error")
} catch let error as DatabaseError {
// Immediate constraint check has failed.
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
throw error
}
return .commit
}
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 1)
}
}
func testImplicitTransactionRollbackCausedByTransactionObserver() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
observer.commitError = NSError(domain: "foo", code: 0, userInfo: nil)
dbQueue.writeWithoutTransaction { db in
do {
try Artist(id: nil, name: "Gerhard Richter").save(db)
XCTFail("Expected Error")
} catch let error as NSError {
XCTAssertEqual(error.domain, "foo")
XCTAssertEqual(error.code, 0)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 1)
}
}
}
func testExplicitTransactionRollbackCausedByTransactionObserver() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
observer.commitError = NSError(domain: "foo", code: 0, userInfo: nil)
dbQueue.add(transactionObserver: observer)
do {
try dbQueue.inTransaction { db in
do {
try Artist(id: nil, name: "Gerhard Richter").save(db)
} catch {
XCTFail("Unexpected Error")
}
return .commit
}
XCTFail("Expected Error")
} catch let error as NSError {
XCTAssertEqual(error.domain, "foo")
XCTAssertEqual(error.code, 0)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 1)
}
}
func testImplicitTransactionRollbackCausedByDatabaseErrorSuperseedTransactionObserver() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
observer.commitError = NSError(domain: "foo", code: 0, userInfo: nil)
dbQueue.add(transactionObserver: observer)
do {
try dbQueue.writeWithoutTransaction { db in
do {
try Artwork(id: nil, artistId: nil, title: "meh").save(db)
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
throw error
}
}
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
}
}
func testExplicitTransactionRollbackCausedByDatabaseErrorSuperseedTransactionObserver() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
observer.commitError = NSError(domain: "foo", code: 0, userInfo: nil)
dbQueue.add(transactionObserver: observer)
do {
try dbQueue.inTransaction { db in
do {
try Artwork(id: nil, artistId: nil, title: "meh").save(db)
XCTFail("Expected Error")
} catch let error as DatabaseError {
// Immediate constraint check has failed.
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
throw error
}
return .commit
}
XCTFail("Expected Error")
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 1)
}
}
func testMinimalRowIDUpdateObservation() throws {
// Here we test that updating a Record made of a single primary key
// column performs an actual UPDATE statement, even though it is
// totally useless (UPDATE id = 1 FROM records WHERE id = 1).
//
// It is important to update something, so that TransactionObserver
// can observe a change.
//
// The goal is to be able to write tests with minimal tables,
// including tables made of a single primary key column. The less we
// have exceptions, the better it is.
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
try MinimalRowID.setup(inDatabase: db)
let record = MinimalRowID()
try record.save(db)
observer.resetCounts()
try record.update(db)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
}
}
// MARK: - Multiple observers
func testInsertEventIsNotifiedToAllObservers() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer1 = Observer()
let observer2 = Observer()
dbQueue.add(transactionObserver: observer1)
dbQueue.add(transactionObserver: observer2)
try dbQueue.writeWithoutTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
//
try artist.save(db)
do {
XCTAssertEqual(observer1.lastCommittedEvents.count, 1)
let event = observer1.lastCommittedEvents.filter { event in
self.match(event: event, kind: .insert, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer1.lastCommittedPreUpdateEvents.count, 1)
let preUpdateEvent = observer1.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artists", initialRowID: nil, finalRowID: artist.id!, initialValues: nil,
finalValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
])
}.first
XCTAssertTrue(preUpdateEvent != nil)
#endif
}
do {
XCTAssertEqual(observer2.lastCommittedEvents.count, 1)
let event = observer2.lastCommittedEvents.filter { event in
self.match(event: event, kind: .insert, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer2.lastCommittedPreUpdateEvents.count, 1)
let preUpdateEvent = observer2.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artists", initialRowID: nil, finalRowID: artist.id!, initialValues: nil,
finalValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
])
}.first
XCTAssertTrue(preUpdateEvent != nil)
#endif
}
}
}
func testTransactionObserverAddAndRemove() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
//
try artist.save(db)
XCTAssertEqual(observer.lastCommittedEvents.count, 1)
let event = observer.lastCommittedEvents.filter { event in
self.match(event: event, kind: .insert, tableName: "artists", rowId: artist.id!)
}.first
XCTAssertTrue(event != nil)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.lastCommittedPreUpdateEvents.count, 1)
let preUpdateEvent = observer.lastCommittedPreUpdateEvents.filter { event in
self.match(preUpdateEvent: event, kind: .insert, tableName: "artists", initialRowID: nil, finalRowID: artist.id!, initialValues: nil,
finalValues: [
artist.id!.databaseValue,
artist.name!.databaseValue
])
}.first
XCTAssertTrue(preUpdateEvent != nil)
#endif
}
observer.resetCounts()
dbQueue.remove(transactionObserver: observer)
try dbQueue.inTransaction { db in
do {
try Artist(id: nil, name: "Vincent Fournier").save(db)
} catch {
XCTFail("Unexpected Error")
}
return .commit
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
}
// MARK: - Filtered database events
func testFilterDatabaseEvents() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
do {
let observer = Observer(observes: { _ in false })
dbQueue.add(transactionObserver: observer)
try dbQueue.inTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
try artist.insert(db)
try artist.update(db)
try artist.delete(db)
return .commit
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { _ in true })
dbQueue.add(transactionObserver: observer)
try dbQueue.inTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
try artist.insert(db)
try artist.update(db)
try artist.delete(db)
return .commit
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 3)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 3)
}
do {
let observer = Observer(observes: { eventKind in
switch eventKind {
case .insert:
return true
case .update:
return false
case .delete:
return false
}
})
dbQueue.add(transactionObserver: observer)
try dbQueue.inTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
try artist.insert(db)
try artist.update(db)
try artist.delete(db)
return .commit
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 1)
}
do {
let observer = Observer(observes: { eventKind in
switch eventKind {
case .insert:
return true
case .update:
return true
case .delete:
return false
}
})
dbQueue.add(transactionObserver: observer)
try dbQueue.inTransaction { db in
let artist = Artist(id: nil, name: "Gerhard Richter")
try artist.insert(db)
try artist.update(db)
try artist.delete(db)
return .commit
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 2)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 2)
}
}
func testComplexFilteredDatabaseEvents() throws {
// When a statement impact several tables, only filtered events are
// notified
do {
let dbQueue = try makeDatabaseQueue()
// Observe deletions in table b
let observer = Observer(observes: { eventKind in
if case .delete(tableName: let tableName) = eventKind {
return tableName == "b"
}
return false
})
dbQueue.add(transactionObserver: observer)
// Delete from a and trigger b deletion
try dbQueue.inTransaction { db in
try db.execute(sql: """
CREATE TABLE a(id INTEGER PRIMARY KEY);
CREATE TABLE b(id INTEGER PRIMARY KEY REFERENCES a(id) ON DELETE CASCADE);
INSERT INTO a (id) VALUES (42);
INSERT INTO b (id) VALUES (42);
DELETE FROM a;
""")
return .commit
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 1)
XCTAssertTrue(match(event: observer.lastCommittedEvents[0], kind: .delete, tableName: "b", rowId: 42))
}
do {
let dbQueue = try makeDatabaseQueue()
// Observe deletions in table b
let observer = Observer(observes: { eventKind in
if case .delete(tableName: let tableName) = eventKind {
return tableName == "b"
}
return false
})
dbQueue.add(transactionObserver: observer)
// Insert into c and trigger b deletion
try dbQueue.inTransaction { db in
try db.execute(sql: """
CREATE TABLE a(id INTEGER PRIMARY KEY);
CREATE TABLE b(id INTEGER PRIMARY KEY);
CREATE TABLE c(id INTEGER);
INSERT INTO b (id) VALUES (42);
CREATE TRIGGER t AFTER INSERT ON c BEGIN DELETE FROM b; END;
INSERT INTO c (id) VALUES (1);
""")
return .commit
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 1)
XCTAssertTrue(match(event: observer.lastCommittedEvents[0], kind: .delete, tableName: "b", rowId: 42))
}
}
func testStopObservingDatabaseChangesUntilNextTransaction() throws {
let dbQueue = try makeDatabaseQueue()
class Observer: TransactionObserver {
var didChangeCount: Int = 0
var didChangeWithEventCount: Int = 0
var willCommitCount: Int = 0
var didCommitCount: Int = 0
var didRollbackCount: Int = 0
func resetCounts() {
didChangeCount = 0
didChangeWithEventCount = 0
willCommitCount = 0
didCommitCount = 0
didRollbackCount = 0
#if SQLITE_ENABLE_PREUPDATE_HOOK
willChangeCount = 0
#endif
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
var willChangeCount: Int = 0
func databaseWillChange(with event: DatabasePreUpdateEvent) { willChangeCount += 1 }
#endif
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { true }
func databaseDidChange() {
didChangeCount += 1
}
func databaseDidChange(with event: DatabaseEvent) {
didChangeWithEventCount += 1
if event.tableName == "ignore" {
stopObservingDatabaseChangesUntilNextTransaction()
}
}
func databaseWillCommit() throws { willCommitCount += 1 }
func databaseDidCommit(_ db: Database) { didCommitCount += 1 }
func databaseDidRollback(_ db: Database) { didRollbackCount += 1 }
}
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.inDatabase { db in
try db.create(table: "ignore") { t in
// `INSERT INTO ignore DEFAULT VALUES` triggers an
// "ignore event" for Observer.
t.column("c").defaults(to: 1)
}
try db.create(table: "persons") { t in
t.column("name", .text)
}
}
try dbQueue.writeWithoutTransaction { db in
// Don't ignore anything
do {
observer.resetCounts()
try db.inTransaction {
try db.execute(sql: "INSERT INTO persons (name) VALUES ('a')")
try db.execute(sql: "DELETE FROM persons")
return .commit
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 2)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 2)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
}
// Ignore 1
do {
observer.resetCounts()
try db.inTransaction {
try db.execute(sql: "INSERT INTO ignore DEFAULT VALUES")
try db.execute(sql: "INSERT INTO persons (name) VALUES ('a')")
try db.execute(sql: "DELETE FROM persons")
return .commit
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
}
// Ignore 2
do {
observer.resetCounts()
try db.inTransaction {
try db.execute(sql: "INSERT INTO persons (name) VALUES ('a')")
try db.execute(sql: "INSERT INTO ignore DEFAULT VALUES")
try db.execute(sql: "DELETE FROM persons")
return .commit
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 2)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 2)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
}
// Ignore 3
do {
observer.resetCounts()
try db.inTransaction {
try db.execute(sql: "INSERT INTO persons (name) VALUES ('a')")
try db.execute(sql: "DELETE FROM persons")
try db.execute(sql: "INSERT INTO ignore DEFAULT VALUES")
return .commit
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 3)
#endif
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 3)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
}
}
}
// MARK: - Unspecified changes
func testUnspecifiedChangeInFullDatabase() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
do {
let observer = Observer(observes: { _ in true })
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { _ in false })
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
switch eventKind {
case .insert:
return true
case .update:
return false
case .delete:
return false
}
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
switch eventKind {
case .insert:
return false
case .update:
return true
case .delete:
return false
}
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
switch eventKind {
case .insert:
return false
case .update:
return false
case .delete:
return true
}
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
eventKind.tableName == "artists"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
eventKind.tableName == "non_existing"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
}
func testUnspecifiedChangeInEmptyRegion() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer(observes: { _ in true })
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: DatabaseRegion())
}
// No change detected because changed region is empty
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
func testUnspecifiedChangeInEmptyDatabase() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer(observes: { _ in true })
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
// No change detected because there is no table
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
func testUnspecifiedChange_sqlite_master() throws {
do {
let dbQueue = try makeDatabaseQueue()
let observer = Observer(observes: { eventKind in
eventKind.tableName == "sqlite_master"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
// Undetected because the full database region does not include sqlite_master
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let dbQueue = try makeDatabaseQueue()
let observer = Observer(observes: { eventKind in
eventKind.tableName == "sqlite_master"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("sqlite_master"))
}
// Detected because explicit sqlite_master region
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
}
func testUnspecifiedChange_sqlite_temp_master() throws {
do {
let dbQueue = try makeDatabaseQueue()
let observer = Observer(observes: { eventKind in
eventKind.tableName == "sqlite_temp_master"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("sqlite_temp_master"))
}
// Undetected because there is no temp schema
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let dbQueue = try makeDatabaseQueue()
let observer = Observer(observes: { eventKind in
eventKind.tableName == "sqlite_temp_master"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
// Create temp schema
try db.execute(sql: "CREATE TEMPORARY TABLE t(a)")
// Explicit sqlite_temp_master
try db.notifyChanges(in: Table("sqlite_temp_master"))
}
// Detected because the temp schema exists.
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
}
func testUnspecifiedChangeToTable() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
do {
let observer = Observer(observes: { _ in true })
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("artists"))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { _ in false })
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("artists"))
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
switch eventKind {
case .insert:
return true
case .update:
return false
case .delete:
return false
}
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("artists"))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
switch eventKind {
case .insert:
return false
case .update:
return true
case .delete:
return false
}
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("artists"))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
switch eventKind {
case .insert:
return false
case .update:
return false
case .delete:
return true
}
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("artists"))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
eventKind.tableName == "artists"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("artists"))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
eventKind.tableName == "artists"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
// Case insensitivity (observer has to use the canonical name).
try db.notifyChanges(in: Table("ARTISTS"))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
eventKind.tableName == "artworks"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("artists"))
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
}
func testUnspecifiedChangeToColumn() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try db.create(table: "test") { t in
t.autoIncrementedPrimaryKey("id")
t.column("a")
t.column("b")
}
}
do {
let observer = Observer(observes: { eventKind in
if case .update("test", let columns) = eventKind, columns.contains("a") {
return true
}
return false
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
if case .update("test", let columns) = eventKind, columns.contains("a") {
return true
}
return false
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("test"))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
if case .update("test", let columns) = eventKind, columns.contains("a") {
return true
}
return false
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("test").select(Column("a")))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
if case .update("test", let columns) = eventKind, columns.contains("a") {
return true
}
return false
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
// Case insensitivity
try db.notifyChanges(in: Table("TEST").select(Column("A")))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
if case .update("test", let columns) = eventKind, columns.contains("a") {
return true
}
return false
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("test").select(Column("b")))
}
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
}
func testUnspecifiedChangeToTemporaryTable() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try db.create(table: "test", options: .temporary) { t in
t.autoIncrementedPrimaryKey("id")
}
}
do {
let observer = Observer(observes: { eventKind in
eventKind.tableName == "test"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: .fullDatabase)
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
do {
let observer = Observer(observes: { eventKind in
eventKind.tableName == "test"
})
dbQueue.add(transactionObserver: observer)
try dbQueue.write { db in
try db.notifyChanges(in: Table("test"))
}
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
}
func testUnspecifiedChangeFromReadOnlyAccess() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer(observes: { _ in true })
dbQueue.add(transactionObserver: observer)
try dbQueue.read { db in
try db.notifyChanges(in: .fullDatabase)
}
// No change detected from read-only access
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
XCTAssertEqual(observer.lastCommittedEvents.count, 0)
}
func test_stopObservingDatabaseChangesUntilNextTransaction_from_databaseDidChange() throws {
class Observer: TransactionObserver {
var didChangeCount: Int = 0
var didChangeWithEventCount: Int = 0
var willCommitCount: Int = 0
var didCommitCount: Int = 0
var didRollbackCount: Int = 0
#if SQLITE_ENABLE_PREUPDATE_HOOK
var willChangeCount: Int = 0
func databaseWillChange(with event: DatabasePreUpdateEvent) { willChangeCount += 1 }
#endif
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { true }
func databaseDidChange() {
didChangeCount += 1
stopObservingDatabaseChangesUntilNextTransaction()
}
func databaseDidChange(with event: DatabaseEvent) {
didChangeWithEventCount += 1
}
func databaseWillCommit() throws { willCommitCount += 1 }
func databaseDidCommit(_ db: Database) { didCommitCount += 1 }
func databaseDidRollback(_ db: Database) { didRollbackCount += 1 }
}
let dbQueue = try makeDatabaseQueue()
try dbQueue.write { db in
try db.execute(sql: "CREATE TABLE test(a)")
}
let observer = Observer()
dbQueue.add(transactionObserver: observer, extent: .databaseLifetime)
try dbQueue.write { db in
// detected
try db.execute(sql: "INSERT INTO test (a) VALUES (1)")
try db.notifyChanges(in: .fullDatabase)
// ignored
try db.execute(sql: "INSERT INTO test (a) VALUES (2)")
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 1)
#endif
XCTAssertEqual(observer.didChangeCount, 1)
XCTAssertEqual(observer.didChangeWithEventCount, 1)
XCTAssertEqual(observer.willCommitCount, 1)
XCTAssertEqual(observer.didCommitCount, 1)
XCTAssertEqual(observer.didRollbackCount, 0)
}
// MARK: - Read-Only Connection
func testReadOnlyConnection() throws {
let dbQueue = try makeDatabaseQueue(filename: "database.sqlite")
try setupArtistDatabase(in: dbQueue)
dbConfiguration.readonly = true
let readOnlyQueue = try makeDatabaseQueue(filename: "database.sqlite")
let observer = Observer()
readOnlyQueue.add(transactionObserver: observer, extent: .databaseLifetime)
try readOnlyQueue.inDatabase { db in
do {
try db.execute(sql: """
BEGIN;
COMMIT;
""")
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
}
do {
try db.execute(sql: """
BEGIN;
SELECT * FROM artists;
COMMIT;
""")
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
}
}
}
func testReadOnlyBlock() throws {
let dbQueue = try makeDatabaseQueue()
try setupArtistDatabase(in: dbQueue)
let observer = Observer()
dbQueue.add(transactionObserver: observer, extent: .databaseLifetime)
try dbQueue.inDatabase { db in
try db.readOnly {
do {
try db.execute(sql: """
BEGIN;
COMMIT;
""")
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
}
do {
try db.execute(sql: """
BEGIN;
SELECT * FROM artists;
COMMIT;
""")
XCTAssertEqual(observer.didChangeCount, 0)
XCTAssertEqual(observer.didChangeWithEventCount, 0)
XCTAssertEqual(observer.willCommitCount, 0)
XCTAssertEqual(observer.didCommitCount, 0)
XCTAssertEqual(observer.didRollbackCount, 0)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.willChangeCount, 0)
#endif
}
}
}
}
}