@@ -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