Skip to content

Commit 3ee256e

Browse files
committed
Common API for statements that must ignore interruption errors
1 parent 361599a commit 3ee256e

File tree

1 file changed

+41
-40
lines changed

1 file changed

+41
-40
lines changed

GRDB/Core/Database.swift

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -863,27 +863,14 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
863863
readOnlyDepth -= 1
864864
assert(readOnlyDepth >= 0, "unbalanced endReadOnly()")
865865
if readOnlyDepth == 0 {
866-
do {
867-
try internalCachedStatement(sql: "PRAGMA query_only = 0").execute()
868-
} catch is CancellationError, DatabaseError.SQLITE_INTERRUPT {
869-
// Note: no test could take this path. Maybe `PRAGMA query_only` is
870-
// not sensitive to SQLite interrupt. But if it is, this code is necessary.
871-
//
872-
// Maybe we were unlucky, and user has interrupted the database
873-
// during the PRAGMA. CancellationError is thrown when
874-
// the PRAGMA is interrupted during execution. SQLITE_INTERRUPT
875-
// is thrown when the PRAGMA is interrupted during compilation.
876-
//
877-
// Let's run it again. This is a correct behavior, because:
878-
// 1. Since the interrupt was concurrent, we can reorder it
879-
// and pretend it occurred before or after the PRAGMA.
880-
// 2. If we do not PRAGMA, we might leave a database
881-
// transaction in the read-only state, with no other
882-
// opportunity to quit it.
883-
//
884-
// As a workaround for an FTS5 bug (https://sqlite.org/forum/forumpost/137c7662b3),
885-
// we must make sure we leave the interrupted state first:
886-
resetAllPreparedStatements()
866+
// We MUST ignore interruptions when we leave the read-only mode,
867+
// otherwise user could not write with this database
868+
// connection again.
869+
//
870+
// It's OK to ignore interruption, since interruption is
871+
// concurrent and we can pretend it occurred before or after
872+
// the PRAGMA.
873+
try ignoringInterruption {
887874
try internalCachedStatement(sql: "PRAGMA query_only = 0").execute()
888875
}
889876
}
@@ -1322,6 +1309,31 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
13221309
return try value()
13231310
}
13241311

1312+
/// Returns the result of `value`. If `value` throws an interruption
1313+
/// error, it is retried a second time.
1314+
func ignoringInterruption<T>(_ value: () throws -> T) rethrows -> T {
1315+
do {
1316+
return try value()
1317+
}
1318+
catch is CancellationError,
1319+
DatabaseError.SQLITE_INTERRUPT,
1320+
DatabaseError.SQLITE_ABORT
1321+
{
1322+
// Maybe we were unlucky, and user has interrupted the database
1323+
// during `value` execution.
1324+
//
1325+
// Another possible cause for this error is the FTS5 bug
1326+
// described at <https://sqlite.org/forum/forumpost/137c7662b3>,
1327+
// which leaves the database in a sticky interrupted state.
1328+
// To workaround this bug, we must leave the interrupted state
1329+
// before retrying:
1330+
resetAllPreparedStatements()
1331+
1332+
// Retry
1333+
return try value()
1334+
}
1335+
}
1336+
13251337
/// Support for `checkForSuspensionViolation(from:)`
13261338
private func journalMode() throws -> String {
13271339
if let journalMode = journalModeCache {
@@ -1786,25 +1798,14 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
17861798
// which rollback errors should be ignored, and which rollback errors
17871799
// should be exposed to the library user.
17881800
if isInsideTransaction {
1789-
do {
1790-
try execute(sql: "ROLLBACK TRANSACTION")
1791-
} catch is CancellationError, DatabaseError.SQLITE_INTERRUPT {
1792-
// Maybe we were unlucky, and user has interrupted the database
1793-
// during the rollback. CancellationError is thrown when
1794-
// the rollback is interrupted during execution. SQLITE_INTERRUPT
1795-
// is thrown when the rollback is interrupted during compilation.
1796-
//
1797-
// Let's rollback again. This is a correct behavior, because:
1798-
// 1. Since the interrupt was concurrent, we can reorder it
1799-
// and pretend it occurred before or after the rollback.
1800-
// 2. If we do not rollback, we might leave a database
1801-
// transaction open, with no other opportunity to close it.
1802-
// This might trigger a fatal error in
1803-
// `SerializedDatabase.preconditionNoUnsafeTransactionLeft`.
1804-
//
1805-
// As a workaround for an FTS5 bug (https://sqlite.org/forum/forumpost/137c7662b3),
1806-
// we must make sure we leave the interrupted state first:
1807-
resetAllPreparedStatements()
1801+
// We MUST ignore interruptions during the rollback, otherwise
1802+
// we could leave a transaction open, and trigger a fatal error in
1803+
// `SerializedDatabase.preconditionNoUnsafeTransactionLeft`.
1804+
//
1805+
// It's OK to ignore interruption, since interruption is
1806+
// concurrent and we can pretend it occurred before or after
1807+
// the rollback.
1808+
try ignoringInterruption {
18081809
try execute(sql: "ROLLBACK TRANSACTION")
18091810
}
18101811
}

0 commit comments

Comments
 (0)