@@ -863,7 +863,16 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
863863 readOnlyDepth -= 1
864864 assert ( readOnlyDepth >= 0 , " unbalanced endReadOnly() " )
865865 if readOnlyDepth == 0 {
866- try internalCachedStatement ( sql: " PRAGMA query_only = 0 " ) . execute ( )
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 {
874+ try internalCachedStatement ( sql: " PRAGMA query_only = 0 " ) . execute ( )
875+ }
867876 }
868877 }
869878
@@ -1300,6 +1309,31 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
13001309 return try value ( )
13011310 }
13021311
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+
13031337 /// Support for `checkForSuspensionViolation(from:)`
13041338 private func journalMode( ) throws -> String {
13051339 if let journalMode = journalModeCache {
@@ -1764,7 +1798,16 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
17641798 // which rollback errors should be ignored, and which rollback errors
17651799 // should be exposed to the library user.
17661800 if isInsideTransaction {
1767- try execute ( sql: " ROLLBACK TRANSACTION " )
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 {
1809+ try execute ( sql: " ROLLBACK TRANSACTION " )
1810+ }
17681811 }
17691812 assert ( !isInsideTransaction)
17701813 }
@@ -1779,6 +1822,19 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
17791822 assert ( sqlite3_get_autocommit ( sqliteConnection) != 0 )
17801823 }
17811824
1825+ /// Resets all prepared statements.
1826+ ///
1827+ /// This method helps clearing the interrupted state, as a workaround
1828+ /// for https://sqlite.org/forum/forumpost/137c7662b3, discovered
1829+ /// in https://github.com/groue/GRDB.swift/issues/1838.
1830+ func resetAllPreparedStatements( ) {
1831+ var stmt : SQLiteStatement ? = sqlite3_next_stmt ( sqliteConnection, nil )
1832+ while stmt != nil {
1833+ sqlite3_reset ( stmt)
1834+ stmt = sqlite3_next_stmt ( sqliteConnection, stmt)
1835+ }
1836+ }
1837+
17821838 // MARK: - Memory Management
17831839
17841840 /// Frees as much memory as possible.
0 commit comments