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

586 lines
24 KiB
Swift

import XCTest
import GRDB
func insertItem(_ db: Database, name: String) throws {
try db.execute(sql: "INSERT INTO items (name) VALUES (?)", arguments: [name])
}
func fetchAllItemNames(_ dbReader: some DatabaseReader) throws -> [String] {
try dbReader.read { db in
try String.fetchAll(db, sql: "SELECT * FROM items ORDER BY name")
}
}
private class Observer : TransactionObserver {
var allRecordedEvents: [DatabaseEvent] = []
func reset() {
allRecordedEvents.removeAll()
#if SQLITE_ENABLE_PREUPDATE_HOOK
allRecordedPreUpdateEvents.removeAll()
#endif
}
#if SQLITE_ENABLE_PREUPDATE_HOOK
var allRecordedPreUpdateEvents: [DatabasePreUpdateEvent] = []
func databaseWillChange(with event: DatabasePreUpdateEvent) {
allRecordedPreUpdateEvents.append(event.copy())
}
#endif
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { true }
func databaseDidChange(with event: DatabaseEvent) {
allRecordedEvents.append(event.copy())
}
func databaseDidCommit(_ db: Database) {
}
func databaseDidRollback(_ db: Database) {
}
}
class DatabaseSavepointTests: GRDBTestCase {
override func setup(_ dbWriter: some DatabaseWriter) throws {
try dbWriter.write { db in
try db.execute(sql: "CREATE TABLE items (name TEXT)")
}
}
func testIsInsideTransaction() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.writeWithoutTransaction { db in
XCTAssertFalse(db.isInsideTransaction)
try db.inTransaction {
XCTAssertTrue(db.isInsideTransaction)
return .commit
}
XCTAssertFalse(db.isInsideTransaction)
try db.execute(sql: "BEGIN DEFERRED TRANSACTION")
XCTAssertTrue(db.isInsideTransaction)
try db.execute(sql: "COMMIT") // does not trigger sqlite3_commit_hook, because transaction was not open yet.
XCTAssertFalse(db.isInsideTransaction)
try db.execute(sql: "BEGIN DEFERRED TRANSACTION")
XCTAssertTrue(db.isInsideTransaction)
try db.execute(sql: "ROLLBACK") // does trigger sqlite3_rollback_hook
XCTAssertFalse(db.isInsideTransaction)
try db.execute(sql: "BEGIN IMMEDIATE TRANSACTION")
XCTAssertTrue(db.isInsideTransaction)
try db.execute(sql: "COMMIT") // does trigger sqlite3_commit_hook
XCTAssertFalse(db.isInsideTransaction)
try db.execute(sql: "BEGIN IMMEDIATE TRANSACTION")
XCTAssertTrue(db.isInsideTransaction)
try db.execute(sql: "ROLLBACK")
XCTAssertFalse(db.isInsideTransaction)
}
}
func testIsInsideTransactionWithImplicitRollback() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.writeWithoutTransaction { db in
try db.create(table: "test") { t in
t.column("value", .integer).unique()
}
try db.execute(sql: "BEGIN TRANSACTION")
XCTAssertTrue(db.isInsideTransaction)
try db.execute(sql: "INSERT INTO test (value) VALUES (?)", arguments: [1])
XCTAssertTrue(db.isInsideTransaction)
XCTAssertThrowsError(try db.execute(sql: "INSERT OR ROLLBACK INTO test (value) VALUES (?)", arguments: [1]))
XCTAssertFalse(db.isInsideTransaction)
XCTAssertThrowsError(try db.execute(sql: "COMMIT"))
}
}
func testReleaseTopLevelSavepointFromDatabaseWithDefaultDeferredTransactions() throws {
dbConfiguration.defaultTransactionKind = .deferred
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
return .commit
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item3")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN DEFERRED TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"COMMIT TRANSACTION",
"INSERT INTO items (name) VALUES ('item3')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item3"])
XCTAssertEqual(observer.allRecordedEvents.count, 3)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 3)
#endif
}
func testRollbackTopLevelSavepointFromDatabaseWithDefaultDeferredTransactions() throws {
dbConfiguration.defaultTransactionKind = .deferred
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
return .rollback
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item3")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN DEFERRED TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"ROLLBACK TRANSACTION",
"INSERT INTO items (name) VALUES ('item3')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item3"])
XCTAssertEqual(observer.allRecordedEvents.count, 3)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 3)
#endif
}
func testNestedSavepointFromDatabaseWithDefaultDeferredTransactions() throws {
dbConfiguration.defaultTransactionKind = .deferred
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item3")
return .commit
}
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item4")
return .commit
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item5")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN DEFERRED TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item3')",
"RELEASE SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item4')",
"COMMIT TRANSACTION",
"INSERT INTO items (name) VALUES ('item5')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item3", "item4", "item5"])
XCTAssertEqual(observer.allRecordedEvents.count, 5)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 5)
#endif
try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") }
observer.reset()
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item3")
return .commit
}
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item4")
return .rollback
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item5")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN DEFERRED TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item3')",
"RELEASE SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item4')",
"ROLLBACK TRANSACTION",
"INSERT INTO items (name) VALUES ('item5')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item5"])
XCTAssertEqual(observer.allRecordedEvents.count, 5)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 5)
#endif
try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") }
observer.reset()
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item3")
return .rollback
}
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item4")
return .commit
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item5")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN DEFERRED TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item3')",
"ROLLBACK TRANSACTION TO SAVEPOINT grdb",
"RELEASE SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item4')",
"COMMIT TRANSACTION",
"INSERT INTO items (name) VALUES ('item5')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item4", "item5"])
XCTAssertEqual(observer.allRecordedEvents.count, 4)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 4)
#endif
try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") }
observer.reset()
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item3")
return .rollback
}
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item4")
return .rollback
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item5")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN DEFERRED TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item3')",
"ROLLBACK TRANSACTION TO SAVEPOINT grdb",
"RELEASE SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item4')",
"ROLLBACK TRANSACTION",
"INSERT INTO items (name) VALUES ('item5')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item5"])
XCTAssertEqual(observer.allRecordedEvents.count, 4)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 4)
#endif
try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") }
observer.reset()
}
func testReleaseTopLevelSavepointFromDatabaseWithDefaultImmediateTransactions() throws {
dbConfiguration.defaultTransactionKind = .immediate
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
return .commit
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item3")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN IMMEDIATE TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"COMMIT TRANSACTION",
"INSERT INTO items (name) VALUES ('item3')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item3"])
XCTAssertEqual(observer.allRecordedEvents.count, 3)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 3)
#endif
}
func testRollbackTopLevelSavepointFromDatabaseWithDefaultImmediateTransactions() throws {
dbConfiguration.defaultTransactionKind = .immediate
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
return .rollback
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item3")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN IMMEDIATE TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"ROLLBACK TRANSACTION",
"INSERT INTO items (name) VALUES ('item3')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item3"])
XCTAssertEqual(observer.allRecordedEvents.count, 3)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 3)
#endif
}
func testNestedSavepointFromDatabaseWithDefaultImmediateTransactions() throws {
dbConfiguration.defaultTransactionKind = .immediate
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item3")
return .commit
}
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item4")
return .commit
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item5")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN IMMEDIATE TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item3')",
"RELEASE SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item4')",
"COMMIT TRANSACTION",
"INSERT INTO items (name) VALUES ('item5')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item3", "item4", "item5"])
XCTAssertEqual(observer.allRecordedEvents.count, 5)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 5)
#endif
try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") }
observer.reset()
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item3")
return .commit
}
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item4")
return .rollback
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item5")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN IMMEDIATE TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item3')",
"RELEASE SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item4')",
"ROLLBACK TRANSACTION",
"INSERT INTO items (name) VALUES ('item5')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item5"])
XCTAssertEqual(observer.allRecordedEvents.count, 5)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 5)
#endif
try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") }
observer.reset()
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item3")
return .rollback
}
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item4")
return .commit
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item5")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN IMMEDIATE TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item3')",
"ROLLBACK TRANSACTION TO SAVEPOINT grdb",
"RELEASE SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item4')",
"COMMIT TRANSACTION",
"INSERT INTO items (name) VALUES ('item5')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item2", "item4", "item5"])
XCTAssertEqual(observer.allRecordedEvents.count, 4)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 4)
#endif
try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") }
observer.reset()
sqlQueries.removeAll()
try dbQueue.writeWithoutTransaction { db in
try insertItem(db, name: "item1")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item2")
try db.inSavepoint {
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item3")
return .rollback
}
XCTAssertTrue(db.isInsideTransaction)
try insertItem(db, name: "item4")
return .rollback
}
XCTAssertFalse(db.isInsideTransaction)
try insertItem(db, name: "item5")
}
XCTAssertEqual(sqlQueries, [
"INSERT INTO items (name) VALUES ('item1')",
"BEGIN IMMEDIATE TRANSACTION",
"INSERT INTO items (name) VALUES ('item2')",
"SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item3')",
"ROLLBACK TRANSACTION TO SAVEPOINT grdb",
"RELEASE SAVEPOINT grdb",
"INSERT INTO items (name) VALUES ('item4')",
"ROLLBACK TRANSACTION",
"INSERT INTO items (name) VALUES ('item5')"
])
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item1", "item5"])
XCTAssertEqual(observer.allRecordedEvents.count, 4)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 4)
#endif
try dbQueue.inDatabase { db in try db.execute(sql: "DELETE FROM items") }
observer.reset()
}
func testSubsequentSavepoints() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.inTransaction { db in
try db.inSavepoint {
try insertItem(db, name: "item1")
return .rollback
}
try db.inSavepoint {
try insertItem(db, name: "item2")
return .commit
}
return .commit
}
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item2"])
XCTAssertEqual(observer.allRecordedEvents.count, 1)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 1)
#endif
}
func testSubsequentSavepointsWithErrors() throws {
let dbQueue = try makeDatabaseQueue()
let observer = Observer()
dbQueue.add(transactionObserver: observer)
try dbQueue.inTransaction { db in
do {
try db.inSavepoint {
try insertItem(db, name: "item1")
throw DatabaseError(resultCode: ResultCode(rawValue: 123))
}
XCTFail()
} catch let error as DatabaseError {
XCTAssertEqual(error.resultCode.rawValue, 123)
}
try db.inSavepoint {
try insertItem(db, name: "item2")
return .commit
}
return .commit
}
XCTAssertEqual(try fetchAllItemNames(dbQueue), ["item2"])
XCTAssertEqual(observer.allRecordedEvents.count, 1)
#if SQLITE_ENABLE_PREUPDATE_HOOK
XCTAssertEqual(observer.allRecordedPreUpdateEvents.count, 1)
#endif
}
}