From 0a83848cfc75af12b00dd09240a0776e5d35893d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 15:35:11 +0100 Subject: [PATCH 01/15] DatabaseReader.interrupt() --- GRDB/Core/Database.swift | 4 ++++ GRDB/Core/DatabasePool.swift | 7 +++++++ GRDB/Core/DatabaseQueue.swift | 6 ++++++ GRDB/Core/DatabaseReader.swift | 10 ++++++++++ GRDB/Core/DatabaseSnapshot.swift | 6 ++++++ GRDB/Core/DatabaseWriter.swift | 7 +++++++ GRDB/Core/SerializedDatabase.swift | 5 +++++ 7 files changed, 45 insertions(+) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 01aae0eb54..f27317014f 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -635,6 +635,10 @@ extension Database { throw DatabaseError(resultCode: code, message: lastErrorMessage) } } + + func interrupt() { + sqlite3_interrupt(sqliteConnection) + } } extension Database { diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 98baa755e1..57c03ae2e3 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -252,6 +252,13 @@ extension DatabasePool { extension DatabasePool: DatabaseReader { + // MARK: - Interrupting Database Operations + + public func interrupt() { + writer.interrupt() + readerPool.forEach { $0.interrupt() } + } + // MARK: - Reading from Database /// Synchronously executes a read-only block in a protected dispatch queue, diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index fd9ae3f35d..b194d4b7c9 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -149,6 +149,12 @@ extension DatabaseQueue { extension DatabaseQueue { + // MARK: - Interrupting Database Operations + + public func interrupt() { + writer.interrupt() + } + // MARK: - Reading from Database /// Synchronously executes a read-only block in a protected dispatch queue, diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index ffad142a8d..74316a3495 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -26,6 +26,11 @@ public protocol DatabaseReader: AnyObject { /// The database configuration var configuration: Configuration { get } + // MARK: - Interrupting Database Operations + + // TODO: doc + func interrupt() + // MARK: - Read From Database /// Synchronously executes a read-only block that takes a database @@ -250,6 +255,11 @@ public final class AnyDatabaseReader: DatabaseReader { return base.configuration } + /// :nodoc: + public func interrupt() { + base.interrupt() + } + // MARK: - Reading from Database /// :nodoc: diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 77acfe18b7..df92854f96 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -47,6 +47,12 @@ public class DatabaseSnapshot: DatabaseReader { // DatabaseReader extension DatabaseSnapshot { + // MARK: - Interrupting Database Operations + + public func interrupt() { + serializedDatabase.interrupt() + } + // MARK: - Reading from Database /// Synchronously executes a read-only block that takes a database diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index ec6d88065e..a433b4a93e 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -601,6 +601,13 @@ public final class AnyDatabaseWriter: DatabaseWriter { return base.configuration } + // MARK: - Interrupting Database Operations + + /// :nodoc: + public func interrupt() { + base.interrupt() + } + // MARK: - Reading from Database /// :nodoc: diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index c08b1445af..0b09b52b49 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -199,6 +199,11 @@ final class SerializedDatabase { return try block(db) } + func interrupt() { + // Intentionally not scheduled in our serial queue + db.interrupt() + } + /// Fatal error if current dispatch queue is not valid. func preconditionValidQueue( _ message: @autoclosure() -> String = "Database was not used on the correct thread.", From 3ed717483809a234879026623550fe4aa68a2009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 15:35:29 +0100 Subject: [PATCH 02/15] DatabaseReader.interrupt() tests --- Tests/GRDBTests/DatabaseReaderTests.swift | 41 +++++++++++++++++++++ Tests/GRDBTests/DatabaseWriterTests.swift | 43 +++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index 34f02c91df..dd14963efe 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -224,4 +224,45 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabasePool(configuration: Configuration()))) try test(setup(makeDatabasePool(configuration: Configuration())).makeSnapshot()) } + + // MARK: Interrupt + + func testInterruptWhileReading() throws { + func test(_ dbReader: DatabaseReader) throws { + let semaphore1 = DispatchSemaphore(value: 0) + let semaphore2 = DispatchSemaphore(value: 0) + + dbReader.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in + semaphore1.signal() + semaphore2.wait() + return nil + }) + let block1 = { + do { + _ = try dbReader.read { + try Row.fetchAll($0, sql: "SELECT wait()") + } + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + let block2 = { + semaphore1.wait() + dbReader.interrupt() + semaphore2.signal() + } + let blocks = [block1, block2] + DispatchQueue.concurrentPerform(iterations: blocks.count) { index in + blocks[index]() + } + } + + try test(DatabaseQueue()) + try test(makeDatabaseQueue()) + try test(makeDatabasePool()) + try test(makeDatabasePool().makeSnapshot()) + } } diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 908188dcb7..8eeefb10e1 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -231,4 +231,47 @@ class DatabaseWriterTests : GRDBTestCase { } try DatabaseQueue().backup(to: dbQueue) } + + // MARK: Interrupt + + func testInterruptWhileWriting() throws { + func test(_ dbWriter: DatabaseWriter) throws { + let semaphore1 = DispatchSemaphore(value: 0) + let semaphore2 = DispatchSemaphore(value: 0) + + dbWriter.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in + semaphore1.signal() + semaphore2.wait() + return nil + }) + let block1 = { + do { + _ = try dbWriter.write { db in + try db.execute(sql: """ + CREATE TABLE t(a); + INSERT INTO t (a) VALUES (wait()) + """) + } + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + let block2 = { + semaphore1.wait() + dbWriter.interrupt() + semaphore2.signal() + } + let blocks = [block1, block2] + DispatchQueue.concurrentPerform(iterations: blocks.count) { index in + blocks[index]() + } + } + + try test(DatabaseQueue()) + try test(makeDatabaseQueue()) + try test(makeDatabasePool()) + } } From f3e9c29f82debc2b5ddc0a52e28dd38f87ce9274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 19:51:55 +0100 Subject: [PATCH 03/15] Monitor aborted transactions --- GRDB/Core/Database+Statements.swift | 10 ++- GRDB/Core/Database.swift | 73 +++++++++++++++++++++- GRDB/Core/DatabaseValueConvertible.swift | 4 +- GRDB/Core/Row.swift | 2 +- GRDB/Core/Statement.swift | 9 +-- GRDB/Core/StatementColumnConvertible.swift | 4 +- GRDB/Record/FetchableRecord.swift | 2 +- Tests/GRDBTests/SelectStatementTests.swift | 2 +- 8 files changed, 92 insertions(+), 14 deletions(-) diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index 859882b916..d42bbc0d5e 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -222,6 +222,12 @@ extension Database { extension Database { func executeUpdateStatement(_ statement: UpdateStatement) throws { + // In aborted transaction, forbid all statements but statements that + // manage transaction. + if statement.transactionEffect == nil { + try assertNotInsideAbortedTransactionBlock(sql: statement.sql, arguments: statement.arguments) + } + let authorizer = observationBroker.updateStatementWillExecute(statement) let sqliteStatement = statement.sqliteStatement var code: Int32 = SQLITE_OK @@ -289,7 +295,9 @@ extension Database { } @inline(__always) - func selectStatementWillExecute(_ statement: SelectStatement) { + func selectStatementWillExecute(_ statement: SelectStatement) throws { + try assertNotInsideAbortedTransactionBlock(sql: statement.sql, arguments: statement.arguments) + if _isRecordingSelectedRegion { // Don't record schema introspection queries, which may be // run, or not, depending on the state of the schema cache. diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index f27317014f..d1458596b4 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -146,6 +146,38 @@ public final class Database { /// Use recordingSelectedRegion(_:), see `selectStatementWillExecute(_:)` var _isRecordingSelectedRegion: Bool = false var _selectedRegion = DatabaseRegion() + + /// True inside `inTransaction { ... }` and `inSavepoint { ... }` + @usableFromInline var isInsideTransactionBlock = false + + /// When true, a transaction has been aborted (for example, by + /// `sqlite3_interrupt`, or a `ON CONFLICT ROLLBACK` clause. + /// + /// try db.inTransaction { + /// do { + /// // Aborted by sqlite3_interrupt or any other + /// // SQLite error which leaves transaction + /// ... + /// } catch { ... } + /// + /// // <- Here we're inside an aborted transaction. + /// ... + /// + /// return .commit + /// } + /// + /// When a transaction has been aborted, we throw SQLITE_ABORT for all + /// database accesses, because we don't want the user to think they are + /// safely wrapped by a transaction. + /// + /// We only monitor aborted transaction for `inTransaction` and + /// `inSavepoint` methods. When you use explicit methods like + /// `beginTransaction`, `commit`, or raw SQL statements, aborted + /// transactions are not monitored, and you won't get SQLITE_ABORT for + /// database accesses that run after a transaction has been aborted. + @usableFromInline var isInsideAbortedTransactionBlock: Bool { + return isInsideTransactionBlock && !isInsideTransaction + } // MARK: - Private properties @@ -644,6 +676,12 @@ extension Database { extension Database { // MARK: - Transactions & Savepoint + func assertNotInsideAbortedTransactionBlock(sql: String? = nil, arguments: StatementArguments? = nil) throws { + if isInsideAbortedTransactionBlock { + throw DatabaseError(resultCode: SQLITE_ABORT, message: "Transaction was aborted", sql: sql, arguments: arguments) + } + } + /// Executes a block inside a database transaction. /// /// try dbQueue.inDatabase do { @@ -670,6 +708,12 @@ extension Database { // Begin transaction try beginTransaction(kind) + let wasInsideTransactionBlock = isInsideTransactionBlock + isInsideTransactionBlock = true + defer { + isInsideTransactionBlock = wasInsideTransactionBlock + } + // Now that transaction has begun, we'll rollback in case of error. // But we'll throw the first caught error, so that user knows // what happened. @@ -679,6 +723,15 @@ extension Database { let completion = try block() switch completion { case .commit: + // In case of aborted transaction, throw SQLITE_ABORT instead + // of the generic SQLITE_ERROR "cannot commit - no transaction is active" + try assertNotInsideAbortedTransactionBlock() + + // Leave transaction block now, so that transaction observers + // can execute statements without getting errors from + // assertNotInsideAbortedTransactionBlock. + isInsideTransactionBlock = wasInsideTransactionBlock + try commit() needsRollback = false case .rollback: @@ -730,7 +783,8 @@ extension Database { // So when the default GRDB transaction kind is not deferred, we open a // transaction instead if !isInsideTransaction && configuration.defaultTransactionKind != .deferred { - return try inTransaction(configuration.defaultTransactionKind, block) + try inTransaction(configuration.defaultTransactionKind, block) + return } // If the savepoint is top-level, we'll use ROLLBACK TRANSACTION in @@ -746,6 +800,12 @@ extension Database { // the user uses "grdb" as a savepoint name. try execute(sql: "SAVEPOINT grdb") + let wasInsideTransactionBlock = isInsideTransactionBlock + isInsideTransactionBlock = true + defer { + isInsideTransactionBlock = wasInsideTransactionBlock + } + // Now that savepoint has begun, we'll rollback in case of error. // But we'll throw the first caught error, so that user knows // what happened. @@ -755,6 +815,15 @@ extension Database { let completion = try block() switch completion { case .commit: + // In case of aborted transaction, throw SQLITE_ABORT instead + // of the generic SQLITE_ERROR "cannot commit - no transaction is active" + try assertNotInsideAbortedTransactionBlock() + + // Leave transaction block now, so that transaction observers + // can execute statements without getting errors from + // assertNotInsideAbortedTransactionBlock. + isInsideTransactionBlock = wasInsideTransactionBlock + try execute(sql: "RELEASE SAVEPOINT grdb") assert(!topLevelSavepoint || !isInsideTransaction) needsRollback = false @@ -893,7 +962,7 @@ extension Database { // which rollback errors should be ignored, and which rollback errors // should be exposed to the library user. SchedulingWatchdog.preconditionValidQueue(self) // guard sqlite3_get_autocommit - if sqlite3_get_autocommit(sqliteConnection) == 0 { + if isInsideTransaction { try execute(sql: "ROLLBACK TRANSACTION") } assert(!isInsideTransaction) diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index 6836906f67..301904d3ca 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -67,7 +67,7 @@ public final class DatabaseValueCursor: Cursor _statement.reset(withArguments: arguments) // Assume cursor is created for iteration - statement.database.selectStatementWillExecute(statement) + try statement.database.selectStatementWillExecute(statement) } deinit { @@ -123,7 +123,7 @@ public final class NullableDatabaseValueCursor: _statement.reset(withArguments: arguments) // Assume cursor is created for iteration - statement.database.selectStatementWillExecute(statement) + try statement.database.selectStatementWillExecute(statement) } deinit { diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index e1c4d3ad1a..13bfdf89e2 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -779,7 +779,7 @@ public final class RowCursor: Cursor { statement.reset(withArguments: arguments) // Assume cursor is created for iteration - statement.database.selectStatementWillExecute(statement) + try statement.database.selectStatementWillExecute(statement) } deinit { diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index 82346a6fe3..d3db3c25f2 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -31,6 +31,7 @@ public class Statement { .trimmingCharacters(in: .sqlStatementSeparators) } + @usableFromInline unowned let database: Database /// Creates a prepared statement. Returns nil if the compiled string is @@ -346,8 +347,8 @@ public final class SelectStatement: Statement { /// Creates a cursor over the statement which does not produce any /// value. Each call to the next() cursor method calls the sqlite3_step() /// C function. - func makeCursor(arguments: StatementArguments? = nil) -> StatementCursor { - return StatementCursor(statement: self, arguments: arguments) + func makeCursor(arguments: StatementArguments? = nil) throws -> StatementCursor { + return try StatementCursor(statement: self, arguments: arguments) } /// Utility function for cursors @@ -384,13 +385,13 @@ final class StatementCursor: Cursor { var _done = false // Use SelectStatement.makeCursor() instead - init(statement: SelectStatement, arguments: StatementArguments? = nil) { + init(statement: SelectStatement, arguments: StatementArguments? = nil) throws { _statement = statement _sqliteStatement = statement.sqliteStatement _statement.reset(withArguments: arguments) // Assume cursor is created for iteration - statement.database.selectStatementWillExecute(statement) + try statement.database.selectStatementWillExecute(statement) } deinit { diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index dd0077f59f..95aa91a86f 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -79,7 +79,7 @@ public final class FastDatabaseValueCursor: Cursor _statement.reset(withArguments: arguments) // Assume cursor is created for iteration - statement.database.selectStatementWillExecute(statement) + try statement.database.selectStatementWillExecute(statement) } deinit { diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index 7dbb8b73f1..ebf27ca89d 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -431,7 +431,7 @@ public final class RecordCursor: Cursor { _statement.reset(withArguments: arguments) // Assume cursor is created for iteration - statement.database.selectStatementWillExecute(statement) + try statement.database.selectStatementWillExecute(statement) } deinit { diff --git a/Tests/GRDBTests/SelectStatementTests.swift b/Tests/GRDBTests/SelectStatementTests.swift index b2b0884784..d5039de87b 100644 --- a/Tests/GRDBTests/SelectStatementTests.swift +++ b/Tests/GRDBTests/SelectStatementTests.swift @@ -37,7 +37,7 @@ class SelectStatementTests : GRDBTestCase { try dbQueue.inDatabase { db in let sql = "SELECT 'Arthur' AS firstName, 'Martin' AS lastName UNION ALL SELECT 'Barbara', 'Gourde'" let statement = try db.makeSelectStatement(sql: sql) - let cursor = statement.makeCursor() + let cursor = try statement.makeCursor() // Check that StatementCursor gives access to the raw SQLite API XCTAssertEqual(String(cString: sqlite3_column_name(cursor._statement.sqliteStatement, 0)), "firstName") From f90a4086309d25e16a5ee206437999d0f485f937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 19:52:25 +0100 Subject: [PATCH 04/15] Tests for aborted transactions --- GRDB.xcodeproj/project.pbxproj | 8 + GRDBCustom.xcodeproj/project.pbxproj | 6 + .../GRDBTests.xcodeproj/project.pbxproj | 6 + .../GRDBTests.xcodeproj/project.pbxproj | 6 + .../DatabaseAbortedTransactionTests.swift | 310 ++++++++++++++++++ Tests/GRDBTests/DatabaseReaderTests.swift | 41 --- Tests/GRDBTests/DatabaseWriterTests.swift | 43 --- 7 files changed, 336 insertions(+), 84 deletions(-) create mode 100644 Tests/GRDBTests/DatabaseAbortedTransactionTests.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 79eb2bb844..f0dc530690 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -812,6 +812,9 @@ 56DF001C228DDBA300D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0019228DDBA200D611F3 /* AssociationPrefetchingRowTests.swift */; }; 56DF001D228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF001A228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; }; 56DF001E228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF001A228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; }; + 56E4F7EE2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */; }; + 56E4F7EF2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */; }; + 56E4F7F02392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */; }; 56E5D7D41B4D3FEE00430942 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56E5D7CA1B4D3FED00430942 /* GRDB.framework */; }; 56E5D7FE1B4D422E00430942 /* GRDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC3773F319C8CBB3004FCF85 /* GRDB.framework */; }; 56E5D8041B4D424400430942 /* GRDBTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */; }; @@ -1655,6 +1658,7 @@ 56DE7B361C42BBBB00861EB8 /* PerformanceCoreDataTests.sqlite */ = {isa = PBXFileReference; lastKnownFileType = file; path = PerformanceCoreDataTests.sqlite; sourceTree = ""; }; 56DF0019228DDBA200D611F3 /* AssociationPrefetchingRowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRowTests.swift; sourceTree = ""; }; 56DF001A228DDBA300D611F3 /* AssociationPrefetchingCodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingCodableRecordTests.swift; sourceTree = ""; }; + 56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseAbortedTransactionTests.swift; sourceTree = ""; }; 56E5D7CA1B4D3FED00430942 /* GRDB.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GRDB.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 56E5D7D31B4D3FEE00430942 /* GRDBiOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBiOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 56E5D7F91B4D422D00430942 /* GRDBOSXTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBOSXTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2218,6 +2222,7 @@ children = ( 5665F85C203EE6030084C6C0 /* ColumnInfoTests.swift */, 56DAA2C41DE99D8D006E10C8 /* Cursor */, + 56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */, 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */, 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */, 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */, @@ -3461,6 +3466,7 @@ 56AF746F1D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift in Sources */, 569BBA3722905FFA00478429 /* InflectionsTests.swift in Sources */, 5672DE6A1CDB751D0022BA81 /* DatabasePoolBackupTests.swift in Sources */, + 56E4F7EF2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 561CFA992376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */, 5698AC4D1DA2D48A0056AF8C /* FTS3RecordTests.swift in Sources */, 56057C632291C7C600A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */, @@ -3666,6 +3672,7 @@ 5698AC491DA2D48A0056AF8C /* FTS3RecordTests.swift in Sources */, 569BBA3622905FFA00478429 /* InflectionsTests.swift in Sources */, 56D496931D81316E008276D7 /* FetchableRecordTests.swift in Sources */, + 56E4F7EE2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 561CFA982376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */, 56D496881D81316E008276D7 /* DatabaseValueConversionTests.swift in Sources */, 56057C622291C7C600A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */, @@ -4004,6 +4011,7 @@ AAA4DD3B230F262000C74B15 /* DatabaseValueConvertibleEscapingTests.swift in Sources */, AAA4DD3C230F262000C74B15 /* InflectionsTests.swift in Sources */, AAA4DD3D230F262000C74B15 /* DatabasePoolBackupTests.swift in Sources */, + 56E4F7F02392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 561CFA9A2376E546000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */, AAA4DD3E230F262000C74B15 /* FTS3RecordTests.swift in Sources */, AAA4DD3F230F262000C74B15 /* AssociationHasManyThroughRowScopeTests.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 2c4004df6a..4e04ba7f98 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -458,6 +458,8 @@ 56DF0016228DDB8300D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0013228DDB8200D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; }; 56DF0017228DDB8300D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0014228DDB8200D611F3 /* AssociationPrefetchingRowTests.swift */; }; 56DF0018228DDB8300D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0014228DDB8200D611F3 /* AssociationPrefetchingRowTests.swift */; }; + 56E4F7F92392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7F72392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift */; }; + 56E4F7FA2392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7F72392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift */; }; 56E9FAC42210468500C703A8 /* SQLInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FAC32210468500C703A8 /* SQLInterpolation.swift */; }; 56E9FAC52210468500C703A8 /* SQLInterpolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FAC32210468500C703A8 /* SQLInterpolation.swift */; }; 56E9FAD52210538400C703A8 /* SQLLiteral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E9FAD42210538400C703A8 /* SQLLiteral.swift */; }; @@ -1028,6 +1030,7 @@ 56DE7B101C3D93ED00861EB8 /* StatementArgumentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementArgumentsTests.swift; sourceTree = ""; }; 56DF0013228DDB8200D611F3 /* AssociationPrefetchingCodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingCodableRecordTests.swift; sourceTree = ""; }; 56DF0014228DDB8200D611F3 /* AssociationPrefetchingRowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRowTests.swift; sourceTree = ""; }; + 56E4F7F72392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAbortedTransactionTests.swift; sourceTree = ""; }; 56E8CE0C1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleFetchTests.swift; sourceTree = ""; }; 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertibleFetchTests.swift; sourceTree = ""; }; 56E9FAC32210468500C703A8 /* SQLInterpolation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLInterpolation.swift; sourceTree = ""; }; @@ -1548,6 +1551,7 @@ children = ( 5665F865203EF4590084C6C0 /* ColumnInfoTests.swift */, 56DAA2C41DE99D8D006E10C8 /* Cursor */, + 56E4F7F72392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift */, 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */, 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */, 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */, @@ -2337,6 +2341,7 @@ F3BA812A1CFB3063003DC1BA /* RecordSubClassTests.swift in Sources */, 569BBA40229065CF00478429 /* InflectionsTests.swift in Sources */, 5653EB7F20961FB200F46237 /* AssociationChainSQLTests.swift in Sources */, + 56E4F7FA2392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 561CFAA22376EF4F000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */, 6340BF871E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift in Sources */, 56057C662291C7E500A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */, @@ -2675,6 +2680,7 @@ F3BA80D01CFB2FEC003DC1BA /* SchedulingWatchdogTests.swift in Sources */, 569BBA41229065CF00478429 /* InflectionsTests.swift in Sources */, 5653EB7E20961FB200F46237 /* AssociationChainSQLTests.swift in Sources */, + 56E4F7F92392E2EE00A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 561CFAA12376EF4F000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */, F3BA80EC1CFB3017003DC1BA /* RowCopiedFromStatementTests.swift in Sources */, 56057C652291C7E500A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */, diff --git a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj index ea7567292e..bff9133519 100644 --- a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj @@ -401,6 +401,8 @@ 56DF0028228DE00900D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0025228DE00900D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; }; 56DF0029228DE00900D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0026228DE00900D611F3 /* AssociationPrefetchingRowTests.swift */; }; 56DF002A228DE00900D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0026228DE00900D611F3 /* AssociationPrefetchingRowTests.swift */; }; + 56E4F7FF2392E6D200A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7FE2392E6D200A611F6 /* DatabaseAbortedTransactionTests.swift */; }; + 56E4F8002392E6D200A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7FE2392E6D200A611F6 /* DatabaseAbortedTransactionTests.swift */; }; 5B33E6E34F941B4C839A714F /* (null) in Frameworks */ = {isa = PBXBuildFile; }; 98AB0B01EB11B33719AE412E /* Pods_GRDBTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE2436BF42B9FCD6552E7076 /* Pods_GRDBTests.framework */; }; F2B3C4250D67969FF3948955 /* Pods_GRDBTestsEncrypted.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D5C7999E7D9CE7145687F5D /* Pods_GRDBTestsEncrypted.framework */; }; @@ -610,6 +612,7 @@ 569BBA30228DF91000478429 /* AssociationPrefetchingFetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingFetchableRecordTests.swift; sourceTree = ""; }; 56DF0025228DE00900D611F3 /* AssociationPrefetchingCodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingCodableRecordTests.swift; sourceTree = ""; }; 56DF0026228DE00900D611F3 /* AssociationPrefetchingRowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRowTests.swift; sourceTree = ""; }; + 56E4F7FE2392E6D200A611F6 /* DatabaseAbortedTransactionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAbortedTransactionTests.swift; sourceTree = ""; }; 6A4788C0F815F6C5E4EBDE12 /* Pods-GRDBTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GRDBTests.debug.xcconfig"; path = "Target Support Files/Pods-GRDBTests/Pods-GRDBTests.debug.xcconfig"; sourceTree = ""; }; 7D5C7999E7D9CE7145687F5D /* Pods_GRDBTestsEncrypted.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GRDBTestsEncrypted.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 83BFB5733A86DAA3D0BEE684 /* Pods-GRDBTestsEncrypted.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GRDBTestsEncrypted.debug.xcconfig"; path = "Target Support Files/Pods-GRDBTestsEncrypted/Pods-GRDBTestsEncrypted.debug.xcconfig"; sourceTree = ""; }; @@ -699,6 +702,7 @@ 564A1F36226B89CF001F64F1 /* CompilationProtocolTests.swift */, 564A1F74226B89D6001F64F1 /* CompilationSubClassTests.swift */, 564A1F6B226B89D5001F64F1 /* CursorTests.swift */, + 56E4F7FE2392E6D200A611F6 /* DatabaseAbortedTransactionTests.swift */, 564A1FD7226B89E0001F64F1 /* DatabaseAfterNextTransactionCommitTests.swift */, 564A1F31226B89CF001F64F1 /* DatabaseAggregateTests.swift */, 564A1F53226B89D2001F64F1 /* DatabaseCollationTests.swift */, @@ -1246,6 +1250,7 @@ 564A206D226B89E1001F64F1 /* ValueObservationCombineTests.swift in Sources */, 564A2022226B89E1001F64F1 /* CursorTests.swift in Sources */, 564A203A226B89E1001F64F1 /* DatabaseDateDecodingStrategyTests.swift in Sources */, + 56E4F7FF2392E6D200A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 560432A9228F1752009D3FE2 /* AssociationPrefetchingObservationTests.swift in Sources */, 564A2036226B89E1001F64F1 /* SchedulingWatchdogTests.swift in Sources */, 564A2052226B89E1001F64F1 /* RowTestCase.swift in Sources */, @@ -1449,6 +1454,7 @@ 564A213C226B8E18001F64F1 /* ValueObservationCombineTests.swift in Sources */, 564A213D226B8E18001F64F1 /* CursorTests.swift in Sources */, 564A213E226B8E18001F64F1 /* DatabaseDateDecodingStrategyTests.swift in Sources */, + 56E4F8002392E6D200A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 560432AA228F1752009D3FE2 /* AssociationPrefetchingObservationTests.swift in Sources */, 564A213F226B8E18001F64F1 /* SchedulingWatchdogTests.swift in Sources */, 564A2140226B8E18001F64F1 /* RowTestCase.swift in Sources */, diff --git a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj index 7096268b20..563a277f17 100644 --- a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj @@ -403,6 +403,8 @@ 56DF0022228DDFF000D611F3 /* AssociationPrefetchingRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF001F228DDFF000D611F3 /* AssociationPrefetchingRowTests.swift */; }; 56DF0023228DDFF000D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0020228DDFF000D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; }; 56DF0024228DDFF000D611F3 /* AssociationPrefetchingCodableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DF0020228DDFF000D611F3 /* AssociationPrefetchingCodableRecordTests.swift */; }; + 56E4F7FC2392E6C400A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7FB2392E6C300A611F6 /* DatabaseAbortedTransactionTests.swift */; }; + 56E4F7FD2392E6C400A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E4F7FB2392E6C300A611F6 /* DatabaseAbortedTransactionTests.swift */; }; 5B33E6E34F941B4C839A714F /* (null) in Frameworks */ = {isa = PBXBuildFile; }; E158370AAEED49ECD53CE24A /* Pods_GRDBTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 67377FE4CDAD675809F93AD4 /* Pods_GRDBTests.framework */; }; F2B3C4250D67969FF3948955 /* Pods_GRDBTestsEncrypted.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D5C7999E7D9CE7145687F5D /* Pods_GRDBTestsEncrypted.framework */; }; @@ -613,6 +615,7 @@ 569BBA2D228DF90200478429 /* AssociationPrefetchingFetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingFetchableRecordTests.swift; sourceTree = ""; }; 56DF001F228DDFF000D611F3 /* AssociationPrefetchingRowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingRowTests.swift; sourceTree = ""; }; 56DF0020228DDFF000D611F3 /* AssociationPrefetchingCodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationPrefetchingCodableRecordTests.swift; sourceTree = ""; }; + 56E4F7FB2392E6C300A611F6 /* DatabaseAbortedTransactionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAbortedTransactionTests.swift; sourceTree = ""; }; 67377FE4CDAD675809F93AD4 /* Pods_GRDBTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GRDBTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6A4788C0F815F6C5E4EBDE12 /* Pods-GRDBTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GRDBTests.debug.xcconfig"; path = "Target Support Files/Pods-GRDBTests/Pods-GRDBTests.debug.xcconfig"; sourceTree = ""; }; 7D5C7999E7D9CE7145687F5D /* Pods_GRDBTestsEncrypted.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GRDBTestsEncrypted.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -702,6 +705,7 @@ 564A1F36226B89CF001F64F1 /* CompilationProtocolTests.swift */, 564A1F74226B89D6001F64F1 /* CompilationSubClassTests.swift */, 564A1F6B226B89D5001F64F1 /* CursorTests.swift */, + 56E4F7FB2392E6C300A611F6 /* DatabaseAbortedTransactionTests.swift */, 564A1FD7226B89E0001F64F1 /* DatabaseAfterNextTransactionCommitTests.swift */, 564A1F31226B89CF001F64F1 /* DatabaseAggregateTests.swift */, 564A1F53226B89D2001F64F1 /* DatabaseCollationTests.swift */, @@ -1252,6 +1256,7 @@ 564A2022226B89E1001F64F1 /* CursorTests.swift in Sources */, 564A203A226B89E1001F64F1 /* DatabaseDateDecodingStrategyTests.swift in Sources */, 564A2036226B89E1001F64F1 /* SchedulingWatchdogTests.swift in Sources */, + 56E4F7FC2392E6C400A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 560432AC228F1761009D3FE2 /* AssociationPrefetchingObservationTests.swift in Sources */, 564A2052226B89E1001F64F1 /* RowTestCase.swift in Sources */, 564A1FF3226B89E1001F64F1 /* TruncateOptimizationTests.swift in Sources */, @@ -1455,6 +1460,7 @@ 564A213D226B8E18001F64F1 /* CursorTests.swift in Sources */, 564A213E226B8E18001F64F1 /* DatabaseDateDecodingStrategyTests.swift in Sources */, 564A213F226B8E18001F64F1 /* SchedulingWatchdogTests.swift in Sources */, + 56E4F7FD2392E6C400A611F6 /* DatabaseAbortedTransactionTests.swift in Sources */, 560432AD228F1761009D3FE2 /* AssociationPrefetchingObservationTests.swift in Sources */, 564A2140226B8E18001F64F1 /* RowTestCase.swift in Sources */, 564A2141226B8E18001F64F1 /* TruncateOptimizationTests.swift in Sources */, diff --git a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift new file mode 100644 index 0000000000..98d1d78528 --- /dev/null +++ b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift @@ -0,0 +1,310 @@ +import XCTest +#if GRDBCUSTOMSQLITE +import GRDBCustomSQLite +#else +import GRDB +#endif + +class DatabaseAbortedTransactionTests : GRDBTestCase { + + func testReadTransactionAbortedByInterrupt() throws { + func test(_ dbReader: DatabaseReader) throws { + let semaphore1 = DispatchSemaphore(value: 0) + let semaphore2 = DispatchSemaphore(value: 0) + + dbReader.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in + semaphore1.signal() + semaphore2.wait() + return nil + }) + let block1 = { + do { + _ = try dbReader.read { + try Row.fetchAll($0, sql: "SELECT wait()") + } + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + let block2 = { + semaphore1.wait() + dbReader.interrupt() + semaphore2.signal() + } + let blocks = [block1, block2] + DispatchQueue.concurrentPerform(iterations: blocks.count) { index in + blocks[index]() + } + } + + try test(DatabaseQueue()) + try test(makeDatabaseQueue()) + try test(makeDatabasePool()) + try test(makeDatabasePool().makeSnapshot()) + } + + func testReadTransactionAbortedByInterruptDoesNotPreventFurtherDatabaseAccess() throws { + func test(_ dbReader: DatabaseReader) throws { + let semaphore1 = DispatchSemaphore(value: 0) + let semaphore2 = DispatchSemaphore(value: 0) + + dbReader.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in + semaphore1.signal() + semaphore2.wait() + return nil + }) + let block1 = { + try! dbReader.read { db in + do { + _ = try Row.fetchAll(db, sql: "SELECT wait()") + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) + } catch { + XCTFail("Unexpected error: \(error)") + } + dbReader.interrupt() + try XCTAssertTrue(Bool.fetchOne(db, sql: "SELECT 1")!) + } + } + let block2 = { + semaphore1.wait() + dbReader.interrupt() + semaphore2.signal() + } + let blocks = [block1, block2] + DispatchQueue.concurrentPerform(iterations: blocks.count) { index in + blocks[index]() + } + } + + try test(DatabaseQueue()) + try test(makeDatabaseQueue()) + try test(makeDatabasePool()) + try test(makeDatabasePool().makeSnapshot()) + } + + func testWriteTransactionAbortedByInterrupt() throws { + func setup(_ dbWriter: T) throws -> T { + try dbWriter.write { db in + try db.execute(sql: "CREATE TABLE t(a);") + } + return dbWriter + } + func test(_ dbWriter: DatabaseWriter) throws { + let semaphore1 = DispatchSemaphore(value: 0) + let semaphore2 = DispatchSemaphore(value: 0) + + dbWriter.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in + semaphore1.signal() + semaphore2.wait() + return nil + }) + let block1 = { + do { + try dbWriter.write { db in + try db.execute(sql: "INSERT INTO t (a) VALUES (wait())") + } + XCTFail("Expected error") + } catch let error as DatabaseError { + // Transactions throw the first uncatched error: SQLITE_INTERRUPT + XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) + XCTAssertEqual(error.message, "interrupted") + XCTAssertEqual(error.sql, "INSERT INTO t (a) VALUES (wait())") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + let block2 = { + semaphore1.wait() + dbWriter.interrupt() + semaphore2.signal() + } + let blocks = [block1, block2] + DispatchQueue.concurrentPerform(iterations: blocks.count) { index in + blocks[index]() + } + } + + try test(setup(DatabaseQueue())) + try test(setup(makeDatabaseQueue())) + try test(setup(makeDatabasePool())) + } + + func testWriteTransactionAbortedByInterruptPreventsFurtherDatabaseAccess() throws { + func setup(_ dbWriter: T) throws -> T { + try dbWriter.write { db in + try db.execute(sql: "CREATE TABLE t(a);") + } + return dbWriter + } + func test(_ dbWriter: DatabaseWriter) throws { + let semaphore1 = DispatchSemaphore(value: 0) + let semaphore2 = DispatchSemaphore(value: 0) + + dbWriter.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in + semaphore1.signal() + semaphore2.wait() + return nil + }) + let block1 = { + do { + try dbWriter.write { db in + do { + try db.execute(sql: "INSERT INTO t (a) VALUES (wait())") + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) + } catch { + XCTFail("Unexpected error: \(error)") + } + + XCTAssertFalse(db.isInsideTransaction) + + do { + try db.execute(sql: "INSERT INTO t (a) VALUES (0)") + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_ABORT) + XCTAssertEqual(error.message, "Transaction was aborted") + XCTAssertEqual(error.sql, "INSERT INTO t (a) VALUES (0)") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + XCTFail("Expected error") + } catch let error as DatabaseError { + // SQLITE_INTERRUPT has been caught. So we get SQLITE_ABORT + // from the last commit. + XCTAssertEqual(error.resultCode, .SQLITE_ABORT) + XCTAssertEqual(error.message, "Transaction was aborted") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + let block2 = { + semaphore1.wait() + dbWriter.interrupt() + semaphore2.signal() + } + let blocks = [block1, block2] + DispatchQueue.concurrentPerform(iterations: blocks.count) { index in + blocks[index]() + } + } + + try test(setup(DatabaseQueue())) + try test(setup(makeDatabaseQueue())) + try test(setup(makeDatabasePool())) + } + + func testTransactionAbortedByConflictPreventsFurtherDatabaseAccess() throws { + func setup(_ dbWriter: T) throws -> T { + try dbWriter.write { db in + try db.execute(sql: """ + CREATE TABLE t(a UNIQUE ON CONFLICT ROLLBACK); + """) + } + return dbWriter + } + func test(_ dbWriter: DatabaseWriter) throws { + do { + try dbWriter.write { db in + do { + try db.execute(sql: """ + INSERT INTO t (a) VALUES (1); + INSERT INTO t (a) VALUES (1); + """) + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_CONSTRAINT) + XCTAssertEqual(error.message, "UNIQUE constraint failed: t.a") + XCTAssertEqual(error.sql, "INSERT INTO t (a) VALUES (1)") + } catch { + XCTFail("Unexpected error: \(error)") + } + + XCTAssertFalse(db.isInsideTransaction) + + try db.execute(sql: "INSERT INTO t (a) VALUES (2)") + } + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_ABORT) + XCTAssertEqual(error.message, "Transaction was aborted") + XCTAssertEqual(error.sql, "INSERT INTO t (a) VALUES (2)") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + try test(setup(DatabaseQueue())) + try test(setup(makeDatabaseQueue())) + try test(setup(makeDatabasePool())) + } + + func testTransactionAbortedByUser() throws { + func setup(_ dbWriter: T) throws -> T { + try dbWriter.write { db in + try db.execute(sql: "CREATE TABLE t(a);") + } + return dbWriter + } + func test(_ dbReader: DatabaseReader) throws { + do { + try dbReader.unsafeRead { db in + try db.inTransaction { + try db.execute(sql: """ + SELECT * FROM t; + ROLLBACK; + SELECT * FROM t; + """) + return .commit + } + } + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_ABORT) + XCTAssertEqual(error.message, "Transaction was aborted") + XCTAssertEqual(error.sql, "SELECT * FROM t") + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + try test(setup(DatabaseQueue())) + try test(setup(makeDatabaseQueue())) + try test(setup(makeDatabasePool())) + } + + func testReadTransactionRestartHack() throws { + // Here we test that the "ROLLBACK; BEGIN TRANSACTION;" hack which + // "refreshes" a DatabaseSnaphot works. + // See https://github.com/groue/GRDB.swift/issues/619 + // This hack puts temporarily the transaction in the aborded + // state. Here we test that we don't throw SQLITE_ABORT. + + let dbPool = try makeDatabasePool() + try dbPool.write { db in + try db.execute(sql: "CREATE TABLE t(a);") + } + let snapshot = try dbPool.makeSnapshot() + try snapshot.read { db in + try db.execute(sql: """ + ROLLBACK; + BEGIN TRANSACTION; + """) + } + try snapshot.read { db in + try db.execute(sql: """ + SELECT * FROM t; + ROLLBACK; + BEGIN TRANSACTION; + SELECT * FROM t; + """) + } + } +} diff --git a/Tests/GRDBTests/DatabaseReaderTests.swift b/Tests/GRDBTests/DatabaseReaderTests.swift index dd14963efe..34f02c91df 100644 --- a/Tests/GRDBTests/DatabaseReaderTests.swift +++ b/Tests/GRDBTests/DatabaseReaderTests.swift @@ -224,45 +224,4 @@ class DatabaseReaderTests : GRDBTestCase { try test(setup(makeDatabasePool(configuration: Configuration()))) try test(setup(makeDatabasePool(configuration: Configuration())).makeSnapshot()) } - - // MARK: Interrupt - - func testInterruptWhileReading() throws { - func test(_ dbReader: DatabaseReader) throws { - let semaphore1 = DispatchSemaphore(value: 0) - let semaphore2 = DispatchSemaphore(value: 0) - - dbReader.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in - semaphore1.signal() - semaphore2.wait() - return nil - }) - let block1 = { - do { - _ = try dbReader.read { - try Row.fetchAll($0, sql: "SELECT wait()") - } - XCTFail("Expected error") - } catch let error as DatabaseError { - XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) - } catch { - XCTFail("Unexpected error: \(error)") - } - } - let block2 = { - semaphore1.wait() - dbReader.interrupt() - semaphore2.signal() - } - let blocks = [block1, block2] - DispatchQueue.concurrentPerform(iterations: blocks.count) { index in - blocks[index]() - } - } - - try test(DatabaseQueue()) - try test(makeDatabaseQueue()) - try test(makeDatabasePool()) - try test(makeDatabasePool().makeSnapshot()) - } } diff --git a/Tests/GRDBTests/DatabaseWriterTests.swift b/Tests/GRDBTests/DatabaseWriterTests.swift index 8eeefb10e1..908188dcb7 100644 --- a/Tests/GRDBTests/DatabaseWriterTests.swift +++ b/Tests/GRDBTests/DatabaseWriterTests.swift @@ -231,47 +231,4 @@ class DatabaseWriterTests : GRDBTestCase { } try DatabaseQueue().backup(to: dbQueue) } - - // MARK: Interrupt - - func testInterruptWhileWriting() throws { - func test(_ dbWriter: DatabaseWriter) throws { - let semaphore1 = DispatchSemaphore(value: 0) - let semaphore2 = DispatchSemaphore(value: 0) - - dbWriter.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in - semaphore1.signal() - semaphore2.wait() - return nil - }) - let block1 = { - do { - _ = try dbWriter.write { db in - try db.execute(sql: """ - CREATE TABLE t(a); - INSERT INTO t (a) VALUES (wait()) - """) - } - XCTFail("Expected error") - } catch let error as DatabaseError { - XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) - } catch { - XCTFail("Unexpected error: \(error)") - } - } - let block2 = { - semaphore1.wait() - dbWriter.interrupt() - semaphore2.signal() - } - let blocks = [block1, block2] - DispatchQueue.concurrentPerform(iterations: blocks.count) { index in - blocks[index]() - } - } - - try test(DatabaseQueue()) - try test(makeDatabaseQueue()) - try test(makeDatabasePool()) - } } From af00fd0f305bebedfeb7dfc012d8a7e7cdf92847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 19:53:04 +0100 Subject: [PATCH 05/15] Less TODO --- TODO.md | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/TODO.md b/TODO.md index c4a9a5601c..76d9e3139d 100644 --- a/TODO.md +++ b/TODO.md @@ -74,45 +74,6 @@ ``` - [ ] new.updateChanges(from: old) vs. old.updateChanges(with: { old.a = new.a }). This is confusing. -- [ ] Support for OR ROLLBACK, and mismatch between the Swift depth and the SQLite depth of nested transactions/savepoint: - - ```swift - try db.inTransaction { // Swift depth: 1, SQLite depth: 1 - try db.execute("COMMIT") // Swift depth: 1, SQLite depth: 0 - try db.execute("INSERT ...") // Should throw an error since this statement is no longer protected by a transaction - try db.execute("SELECT ...") // Should throw an error since this statement is no longer protected by a transaction - return .commit - } - ``` - - ```swift - try db.inTransaction { - try db.execute("INSERT OR ROLLBACK ...") // throws - return .commit // not executed because of error - } // Should not ROLLBACK since transaction has already been rollbacked - ``` - - ```swift - try db.inTransaction { - do { - try db.execute("INSERT OR ROLLBACK ...") // throws - } catch { - } - try db.execute("INSERT ...") // Should throw an error since this statement is no longer protected by a transaction - try db.execute("SELECT ...") // Should throw an error since this statement is no longer protected by a transaction - return .commit - } - ``` - - ```swift - try db.inTransaction { - do { - try db.execute("INSERT OR ROLLBACK ...") // throws - } catch { - } - return .commit // Should throw an error since transaction has been rollbacked and user's intent can not be applied - } - ``` ## Reading list From fa1abdff2bd3ada6f8bbd7b44129b7dc831c8024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 19:56:26 +0100 Subject: [PATCH 06/15] Fix Swiftlint warnings --- GRDB/Core/Database.swift | 6 +++++- GRDB/Core/Statement.swift | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index d1458596b4..8607a42112 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -678,7 +678,11 @@ extension Database { func assertNotInsideAbortedTransactionBlock(sql: String? = nil, arguments: StatementArguments? = nil) throws { if isInsideAbortedTransactionBlock { - throw DatabaseError(resultCode: SQLITE_ABORT, message: "Transaction was aborted", sql: sql, arguments: arguments) + throw DatabaseError( + resultCode: SQLITE_ABORT, + message: "Transaction was aborted", + sql: sql, + arguments: arguments) } } diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index d3db3c25f2..05564bb0f7 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -31,8 +31,7 @@ public class Statement { .trimmingCharacters(in: .sqlStatementSeparators) } - @usableFromInline - unowned let database: Database + @usableFromInline unowned let database: Database /// Creates a prepared statement. Returns nil if the compiled string is /// blank or empty. From 79871e2d00c3a3e1b99cd810931a28ce681960fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 20:26:18 +0100 Subject: [PATCH 07/15] Documentation --- GRDB/Core/DatabaseReader.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 74316a3495..a851c52680 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -255,6 +255,8 @@ public final class AnyDatabaseReader: DatabaseReader { return base.configuration } + // MARK: - Interrupting Database Operations + /// :nodoc: public func interrupt() { base.interrupt() From cae66fd5e0d74fa1e7ad25ea4f5fc7a400d3ad23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 20:51:24 +0100 Subject: [PATCH 08/15] Documentation --- GRDB/Core/DatabaseReader.swift | 42 +++++++++++++++- .../DatabaseAbortedTransactionTests.swift | 49 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index a851c52680..e5bd0736a6 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -28,7 +28,47 @@ public protocol DatabaseReader: AnyObject { // MARK: - Interrupting Database Operations - // TODO: doc + /// This method causes any pending database operation to abort and return at + /// its earliest opportunity. + /// + /// It can be called from any thread. + /// + /// A call to `interrupt()` that occurs when there are no running SQL + /// statements is a no-op and has no effect on SQL statements that are + /// started after `interrupt()` returns. + /// + /// A database operation that is interrupted will throw a DatabaseError with + /// code SQLITE_INTERRUPT. If the interrupted SQL operation is an INSERT, + /// UPDATE, or DELETE that is inside an explicit transaction, then the + /// entire transaction will be rolled back automatically. If the rolled back + /// transaction was created by a method which wraps your database accesses, + /// such as `DatabaseWriter.write` or `Database.inTransaction`, then all + /// database accesses will throw a DatabaseError with code SQLITE_ABORT + /// until the wrapping method returns. + /// + /// For example: + /// + /// try dbQueue.write { db in + /// // interrupted: + /// try Player(...).insert(db) // throws SQLITE_INTERRUPT + /// // not executed: + /// try Player(...).insert(db) + /// } // throws SQLITE_INTERRUPT + /// + /// try dbQueue.write { db in + /// do { + /// // interrupted: + /// try Player(...).insert(db) // throws SQLITE_INTERRUPT + /// } catch { } + /// try Player(...).insert(db) // throws SQLITE_ABORT + /// } // throws SQLITE_ABORT + /// + /// try dbQueue.write { db in + /// do { + /// // interrupted: + /// try Player(...).insert(db) // throws SQLITE_INTERRUPT + /// } catch { } + /// } // throws SQLITE_ABORT func interrupt() // MARK: - Read From Database diff --git a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift index 98d1d78528..ecc96ef41a 100644 --- a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift +++ b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift @@ -201,6 +201,55 @@ class DatabaseAbortedTransactionTests : GRDBTestCase { try test(setup(makeDatabasePool())) } + func testWriteTransactionAbortedByInterruptDoesNotPreventRollback() throws { + func setup(_ dbWriter: T) throws -> T { + try dbWriter.write { db in + try db.execute(sql: "CREATE TABLE t(a);") + } + return dbWriter + } + func test(_ dbWriter: DatabaseWriter) throws { + let semaphore1 = DispatchSemaphore(value: 0) + let semaphore2 = DispatchSemaphore(value: 0) + + dbWriter.add(function: DatabaseFunction("wait", argumentCount: 0, pure: true) { _ in + semaphore1.signal() + semaphore2.wait() + return nil + }) + let block1 = { + try! dbWriter.writeWithoutTransaction { db in + try db.inTransaction { + do { + try db.execute(sql: "INSERT INTO t (a) VALUES (wait())") + XCTFail("Expected error") + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) + } catch { + XCTFail("Unexpected error: \(error)") + } + + XCTAssertFalse(db.isInsideTransaction) + return .rollback + } + } + } + let block2 = { + semaphore1.wait() + dbWriter.interrupt() + semaphore2.signal() + } + let blocks = [block1, block2] + DispatchQueue.concurrentPerform(iterations: blocks.count) { index in + blocks[index]() + } + } + + try test(setup(DatabaseQueue())) + try test(setup(makeDatabaseQueue())) + try test(setup(makeDatabasePool())) + } + func testTransactionAbortedByConflictPreventsFurtherDatabaseAccess() throws { func setup(_ dbWriter: T) throws -> T { try dbWriter.write { db in From b5ff80e858732be1c6f4ed7999d860c9cbea5d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 21:12:47 +0100 Subject: [PATCH 09/15] Documentation --- GRDB/Core/DatabaseReader.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index e5bd0736a6..5a88f6bea2 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -41,10 +41,10 @@ public protocol DatabaseReader: AnyObject { /// code SQLITE_INTERRUPT. If the interrupted SQL operation is an INSERT, /// UPDATE, or DELETE that is inside an explicit transaction, then the /// entire transaction will be rolled back automatically. If the rolled back - /// transaction was created by a method which wraps your database accesses, - /// such as `DatabaseWriter.write` or `Database.inTransaction`, then all - /// database accesses will throw a DatabaseError with code SQLITE_ABORT - /// until the wrapping method returns. + /// transaction was started by a transaction-wrapping method such as + /// `DatabaseWriter.write` or `Database.inTransaction`, then all database + /// accesses will throw a DatabaseError with code SQLITE_ABORT until the + /// wrapping method returns. /// /// For example: /// From 64cb3091cf73c90ef85a2b8a6d318196df2bd2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 21:12:53 +0100 Subject: [PATCH 10/15] white space --- GRDB/Core/DatabaseWriter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GRDB/Core/DatabaseWriter.swift b/GRDB/Core/DatabaseWriter.swift index a433b4a93e..bc730c4069 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -380,7 +380,7 @@ extension DatabaseWriter { if let value = observer.reducer.value(fetchedValue) { startValue = value } - + db.add(transactionObserver: observer, extent: .observerLifetime) } } catch { From d4fa4c9d5ed476813e1b449c9e35bb74651d7f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 21:14:06 +0100 Subject: [PATCH 11/15] CHANGELOG --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7b2a34ce7..b59970bfa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,7 @@ All notable changes to this project will be documented in this file. GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: APIs flagged [**:fire: EXPERIMENTAL**](README.md#what-are-experimental-features). Those are unstable, and may break between any two minor releases of the library. - #### 4.x Releases @@ -59,9 +57,11 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: - [0.110.0](#01100), ... - + +**New** + +- [#659](https://github.com/groue/GRDB.swift/pull/659): Database interruption ## 4.6.2 From 62ef81c5fb6d01d5b15f71500a24344e44c7d7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 30 Nov 2019 21:16:48 +0100 Subject: [PATCH 12/15] Remove useless code --- GRDB/Core/Database.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index 8607a42112..f11ee46bb4 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -965,7 +965,6 @@ extension Database { // The second technique is more robust, because we don't have to guess // which rollback errors should be ignored, and which rollback errors // should be exposed to the library user. - SchedulingWatchdog.preconditionValidQueue(self) // guard sqlite3_get_autocommit if isInsideTransaction { try execute(sql: "ROLLBACK TRANSACTION") } From 8145915d19c61c1cf46af411165424cf6d2b6d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Dec 2019 04:01:03 +0100 Subject: [PATCH 13/15] Fix aborted transaction tests for iOS 10.3.1 --- Tests/GRDBTests/DatabaseAbortedTransactionTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift index ecc96ef41a..5111f18d5a 100644 --- a/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift +++ b/Tests/GRDBTests/DatabaseAbortedTransactionTests.swift @@ -106,14 +106,14 @@ class DatabaseAbortedTransactionTests : GRDBTestCase { let block1 = { do { try dbWriter.write { db in - try db.execute(sql: "INSERT INTO t (a) VALUES (wait())") + try db.execute(sql: "INSERT INTO t SELECT wait()") } XCTFail("Expected error") } catch let error as DatabaseError { // Transactions throw the first uncatched error: SQLITE_INTERRUPT XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) XCTAssertEqual(error.message, "interrupted") - XCTAssertEqual(error.sql, "INSERT INTO t (a) VALUES (wait())") + XCTAssertEqual(error.sql, "INSERT INTO t SELECT wait()") } catch { XCTFail("Unexpected error: \(error)") } @@ -154,7 +154,7 @@ class DatabaseAbortedTransactionTests : GRDBTestCase { do { try dbWriter.write { db in do { - try db.execute(sql: "INSERT INTO t (a) VALUES (wait())") + try db.execute(sql: "INSERT INTO t SELECT wait()") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) @@ -221,7 +221,7 @@ class DatabaseAbortedTransactionTests : GRDBTestCase { try! dbWriter.writeWithoutTransaction { db in try db.inTransaction { do { - try db.execute(sql: "INSERT INTO t (a) VALUES (wait())") + try db.execute(sql: "INSERT INTO t SELECT wait()") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_INTERRUPT) From eb05c9b87d91412fcbb5bf8dfcb7d7df624e9df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Dec 2019 05:19:56 +0100 Subject: [PATCH 14/15] Documentation --- README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/README.md b/README.md index bb52fc4a9d..65ead94aa8 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,7 @@ Documentation - [Database Changes Observation](#database-changes-observation): Observe database changes and transactions. - [Encryption](#encryption): Encrypt your database with SQLCipher. - [Backup](#backup): Dump the content of a database to another. +- [Interrupt a Database](#interrupt-a-database): Abort any pending database operation. #### Good to Know @@ -6640,6 +6641,48 @@ The `backup` method blocks the current thread until the destination database con When the source is a [database pool](#database-pools), concurrent writes can happen during the backup. Those writes may, or may not, be reflected in the backup, but they won't trigger any error. +## Interrupt a Database + +**The `interrupt()` method** causes any pending database operation to abort and return at its earliest opportunity. + +It can be called from any thread. + +```swift +dbQueue.interrupt() +dbPool.interrupt() +``` + +A call to `interrupt()` that occurs when there are no running SQL statements is a no-op and has no effect on SQL statements that are started after `interrupt()` returns. + +A database operation that is interrupted will throw a DatabaseError with code `SQLITE_INTERRUPT`. If the interrupted SQL operation is an INSERT, UPDATE, or DELETE that is inside an explicit transaction, then the entire transaction will be rolled back automatically. If the rolled back transaction was started by a transaction-wrapping method such as `DatabaseWriter.write` or `Database.inTransaction`, then all database accesses will throw a DatabaseError with code `SQLITE_ABORT` until the wrapping method returns. + +For example: + +```swift +try dbQueue.write { db in + // interrupted: + try Player(...).insert(db) // throws SQLITE_INTERRUPT + // not executed: + try Player(...).insert(db) +} // throws SQLITE_INTERRUPT + +try dbQueue.write { db in + do { + // interrupted: + try Player(...).insert(db) // throws SQLITE_INTERRUPT + } catch { } + try Player(...).insert(db) // throws SQLITE_ABORT +} // throws SQLITE_ABORT + +try dbQueue.write { db in + do { + // interrupted: + try Player(...).insert(db) // throws SQLITE_INTERRUPT + } catch { } +} // throws SQLITE_ABORT +``` + + ## Avoiding SQL Injection SQL injection is a technique that lets an attacker nuke your database. From e3d25bda616e75e944c885669187304b75ecada1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 1 Dec 2019 05:23:19 +0100 Subject: [PATCH 15/15] CHANGELOG --- CHANGELOG.md | 4 ++++ README.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b59970bfa9..d1f8c92ee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,10 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: - [#659](https://github.com/groue/GRDB.swift/pull/659): Database interruption +### Documentation Diff + +A new [Interrupt a Database](README.md#interrupt-a-database) chapter documents the new `interrupt()` method. + ## 4.6.2 diff --git a/README.md b/README.md index 65ead94aa8..e6007c61d1 100644 --- a/README.md +++ b/README.md @@ -6682,6 +6682,8 @@ try dbQueue.write { db in } // throws SQLITE_ABORT ``` +For more information, see [Interrupt A Long-Running Query](https://www.sqlite.org/c3ref/interrupt.html). + ## Avoiding SQL Injection