Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/Tests/GRDBTests/TransactionObserverSavepointsTests.swift
T

312 lines
18 KiB
Swift

import XCTest
import GRDB
private class Observer : TransactionObserver {
var lastCommittedEvents: [DatabaseEvent] = []
var events: [DatabaseEvent] = []
#if SQLITE_ENABLE_PREUPDATE_HOOK
var preUpdateEvents: [DatabasePreUpdateEvent] = []
func databaseWillChange(with event: DatabasePreUpdateEvent) {
preUpdateEvents.append(event.copy())
}
#endif
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { true }
func databaseDidChange(with event: DatabaseEvent) {
events.append(event.copy())
}
func databaseDidCommit(_ db: Database) {
lastCommittedEvents = events
events = []
}
func databaseDidRollback(_ db: Database) {
lastCommittedEvents = []
events = []
}
}
class TransactionObserverSavepointsTests: GRDBTestCase {
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: - Events
func testSavepointAsTransaction() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE items1 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items2 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "SAVEPOINT sp1")
XCTAssertTrue(db.isInsideTransaction)
try db.execute(sql: "INSERT INTO items1 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 0)
try db.execute(sql: "INSERT INTO items2 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 0)
try db.execute(sql: "RELEASE SAVEPOINT sp1")
XCTAssertFalse(db.isInsideTransaction)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items1"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items2"), 1)
}
XCTAssertEqual(observer.lastCommittedEvents.count, 2)
XCTAssertTrue(match(event: observer.lastCommittedEvents[0], kind: .insert, tableName: "items1", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[1], kind: .insert, tableName: "items2", rowId: 1))
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.preUpdateEvents.count, 2)
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[0], kind: .insert, tableName: "items1", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[1], kind: .insert, tableName: "items2", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
#endif
}
func testSavepointInsideTransaction() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE items1 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items2 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "BEGIN TRANSACTION")
try db.execute(sql: "INSERT INTO items1 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "SAVEPOINT sp1")
try db.execute(sql: "INSERT INTO items2 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "COMMIT")
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items1"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items2"), 1)
}
XCTAssertEqual(observer.lastCommittedEvents.count, 2)
XCTAssertTrue(match(event: observer.lastCommittedEvents[0], kind: .insert, tableName: "items1", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[1], kind: .insert, tableName: "items2", rowId: 1))
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.preUpdateEvents.count, 2)
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[0], kind: .insert, tableName: "items1", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[1], kind: .insert, tableName: "items2", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
#endif
}
func testSavepointWithIdenticalName() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE items1 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items2 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items3 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items4 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "BEGIN TRANSACTION")
try db.execute(sql: "INSERT INTO items1 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "SAVEPOINT sp1")
try db.execute(sql: "INSERT INTO items2 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "SAVEPOINT sp1")
try db.execute(sql: "INSERT INTO items3 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "RELEASE SAVEPOINT sp1")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "RELEASE SAVEPOINT sp1")
XCTAssertEqual(observer.events.count, 3)
try db.execute(sql: "INSERT INTO items4 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 4)
try db.execute(sql: "COMMIT")
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items1"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items2"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items3"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items4"), 1)
}
XCTAssertEqual(observer.lastCommittedEvents.count, 4)
XCTAssertTrue(match(event: observer.lastCommittedEvents[0], kind: .insert, tableName: "items1", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[1], kind: .insert, tableName: "items2", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[2], kind: .insert, tableName: "items3", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[3], kind: .insert, tableName: "items4", rowId: 1))
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.preUpdateEvents.count, 4)
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[0], kind: .insert, tableName: "items1", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[1], kind: .insert, tableName: "items2", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[2], kind: .insert, tableName: "items3", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[3], kind: .insert, tableName: "items4", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
#endif
}
func testMultipleRollbackOfSavepoint() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE items1 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items2 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items3 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items4 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "BEGIN TRANSACTION")
try db.execute(sql: "INSERT INTO items1 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "SAVEPOINT sp1")
try db.execute(sql: "INSERT INTO items2 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "INSERT INTO items3 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "ROLLBACK TO SAVEPOINT sp1")
try db.execute(sql: "INSERT INTO items4 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "ROLLBACK TO SAVEPOINT sp1")
try db.execute(sql: "INSERT INTO items4 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "COMMIT")
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items1"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items2"), 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items3"), 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items4"), 1)
}
XCTAssertEqual(observer.lastCommittedEvents.count, 2)
XCTAssertTrue(match(event: observer.lastCommittedEvents[0], kind: .insert, tableName: "items1", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[1], kind: .insert, tableName: "items4", rowId: 1))
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.preUpdateEvents.count, 2)
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[0], kind: .insert, tableName: "items1", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[1], kind: .insert, tableName: "items4", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
#endif
}
func testReleaseSavepoint() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE items1 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items2 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items3 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items4 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "BEGIN TRANSACTION")
try db.execute(sql: "INSERT INTO items1 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "SAVEPOINT sp1")
try db.execute(sql: "INSERT INTO items2 (id) VALUES (NULL)")
try db.execute(sql: "INSERT INTO items3 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "RELEASE SAVEPOINT sp1")
XCTAssertEqual(observer.events.count, 3)
try db.execute(sql: "INSERT INTO items4 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 4)
try db.execute(sql: "COMMIT")
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items1"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items2"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items3"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items4"), 1)
}
XCTAssertEqual(observer.lastCommittedEvents.count, 4)
XCTAssertTrue(match(event: observer.lastCommittedEvents[0], kind: .insert, tableName: "items1", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[1], kind: .insert, tableName: "items2", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[2], kind: .insert, tableName: "items3", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[3], kind: .insert, tableName: "items4", rowId: 1))
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.preUpdateEvents.count, 4)
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[0], kind: .insert, tableName: "items1", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[1], kind: .insert, tableName: "items2", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[2], kind: .insert, tableName: "items3", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[3], kind: .insert, tableName: "items4", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
#endif
}
func testRollbackNonNestedSavepointInsideTransaction() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "CREATE TABLE items1 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items2 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items3 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "CREATE TABLE items4 (id INTEGER PRIMARY KEY)")
try db.execute(sql: "BEGIN TRANSACTION")
try db.execute(sql: "INSERT INTO items1 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "SAVEPOINT sp1")
try db.execute(sql: "INSERT INTO items2 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "SAVEPOINT sp2")
try db.execute(sql: "INSERT INTO items3 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "RELEASE SAVEPOINT sp2")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "ROLLBACK TO SAVEPOINT sp1")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "INSERT INTO items4 (id) VALUES (NULL)")
XCTAssertEqual(observer.events.count, 1)
try db.execute(sql: "COMMIT")
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items1"), 1)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items2"), 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items3"), 0)
XCTAssertEqual(try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM items4"), 1)
}
XCTAssertEqual(observer.lastCommittedEvents.count, 2)
XCTAssertTrue(match(event: observer.lastCommittedEvents[0], kind: .insert, tableName: "items1", rowId: 1))
XCTAssertTrue(match(event: observer.lastCommittedEvents[1], kind: .insert, tableName: "items4", rowId: 1))
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.preUpdateEvents.count, 2)
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[0], kind: .insert, tableName: "items1", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
XCTAssertTrue(match(preUpdateEvent: observer.preUpdateEvents[1], kind: .insert, tableName: "items4", initialRowID: nil, finalRowID: 1, initialValues: nil, finalValues: [Int(1).databaseValue]))
#endif
}
}