diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c20d39eac..92b2acf376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception: - [#659](https://github.com/groue/GRDB.swift/pull/659): Database interruption - [#660](https://github.com/groue/GRDB.swift/pull/660): Database Lock Prevention - [#662](https://github.com/groue/GRDB.swift/pull/662): Upgrade custom SQLite builds to version 3.30.1 (thanks to [@swiftlyfalling](https://github.com/swiftlyfalling/SQLiteLib)) +- [#668](https://github.com/groue/GRDB.swift/pull/668): Database Suspension ### Breaking Changes @@ -80,7 +81,39 @@ let dbQueue = try DatabaseQueue(path: ..., configuration: configuration) ### Documentation Diff -A new [Interrupt a Database](README.md#interrupt-a-database) chapter documents the new `interrupt()` method. +The new [Interrupt a Database](README.md#interrupt-a-database) chapter documents the new `interrupt()` method. + +The new [Sharing a Datatase in an App Group Container](Documentation/AppGroupContainers.md) guide explains how to setup GRDB when you share a database in an iOS App Group container. + + +### API Diff + +```diff + struct Configuration { ++ var observesSuspensionNotifications: Bool // Experimental ++ var acceptsDoubleQuotedStringLiterals: Bool + } + + class Database { ++ static let suspendNotification: Notification.Name // Experimental ++ static let resumeNotification: Notification.Name // Experimental + } + + extension DatabaseError { ++ var isInterruptionError: Bool { get } + } + + protocol DatabaseReader { ++ func interrupt() + } + + extension SQLSpecificExpressible { ++ #if GRDBCUSTOMSQLITE ++ var ascNullsLast: SQLOrderingTerm { get } ++ var descNullsFirst: SQLOrderingTerm { get } ++ #endif + } +``` ## 4.6.2 diff --git a/Documentation/AppGroupContainers.md b/Documentation/AppGroupContainers.md new file mode 100644 index 0000000000..2d618132ca --- /dev/null +++ b/Documentation/AppGroupContainers.md @@ -0,0 +1,239 @@ +Sharing a Datatase in an App Group Container +============================================ + +On iOS, you can share database files between multiple processes by storing them in an [App Group Container](https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati). + +A shared database is accessed from several SQLite connections, from several processes. This creates challenges at various levels: + +1. **Database setup** may be attempted by multiple processes, concurrently. +2. **SQLite** may throw `SQLITE_BUSY` errors, code 5, "database is locked". +3. **iOS** may kill your application with a `0xDEAD10CC` exception. +4. **GRDB** database observation misses changes performed by external processes. + +We'll address all of those challenges below. + +- [Use a Database Pool] +- [How to limit the `SQLITE_BUSY` error] +- [How to limit the `0xDEAD10CC` exception] +- [How to perform cross-process database observation] + + +## Use a Database Pool + +In order to access a shared database, use a [Database Pool]. It opens the database in the [WAL mode](https://www.sqlite.org/wal.html), which helps sharing a database. + +Since several processes may open the database at the same time, protect the creation of the database pool with an [NSFileCoordinator]. + +- In a process that can create and write in the database, use this sample code: + + ```swift + /// Returns an initialized database pool at the shared location databaseURL + func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool { + let coordinator = NSFileCoordinator(filePresenter: nil) + var coordinatorError: NSError? + var dbPool: DatabasePool? + var dbError: Error? + coordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError, byAccessor: { url in + do { + dbPool = try openDatabase(at: url) + } catch { + dbError = error + } + }) + if let error = dbError ?? coordinatorError { + throw error + } + return dbPool! + } + + private func openDatabase(at databaseURL: URL) throws -> DatabasePool { + let dbPool = try DatabasePool(path: databaseURL.path) + // Perform here other database setups, such as defining + // the database schema with a DatabaseMigrator. + return dbPool + } + ``` + +- In a process that only reads in the database, use this sample code: + + ```swift + /// Returns an initialized database pool at the shared location databaseURL, + /// or nil if the database was not created yet. + func openSharedReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? { + let coordinator = NSFileCoordinator(filePresenter: nil) + var coordinatorError: NSError? + var dbPool: DatabasePool? + var dbError: Error? + coordinator.coordinate(readingItemAt: databaseURL, options: .withoutChanges, error: &coordinatorError, byAccessor: { url in + do { + dbPool = try openReadOnlyDatabase(at: url) + } catch { + dbError = error + } + }) + if let error = dbError ?? coordinatorError { + throw error + } + return dbPool + } + + private func openReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? { + do { + var configuration = Configuration() + configuration.readonly = true + return try DatabasePool(path: databaseURL.path, configuration: configuration) + } catch { + if FileManager.default.fileExists(atPath: databaseURL.path) { + throw error + } else { + return nil + } + } + } + ``` + + +## How to limit the `SQLITE_BUSY` error + +> The SQLITE_BUSY result code indicates that the database file could not be written (or in some cases read) because of concurrent activity by some other database connection, usually a database connection in a separate process. + +See https://www.sqlite.org/rescode.html#busy for more information about this error. + +If several processes want to write in the database, configure the database pool of each process that wants to write: + +```swift +var configuration = Configuration() +configuration.busyMode = .timeout(/* a TimeInterval */) +configuration.defaultTransactionKind = .immediate +let dbPool = try DatabasePool(path: ..., configuration: configuration) +``` + +With such a setup, you may still get `SQLITE_BUSY` (5, "database is locked") errors from all write operations. They will occur if the database remains locked by another process for longer than the specified timeout. + +```swift +do { + try dbPool.write { db in ... } +} catch let error as DatabaseError where error.resultCode == .SQLITE_BUSY { + // Another process won't let you write. Deal with it. +} +``` + +> :bulb: **Tip**: In order to be nice to other processes, measure the duration of your longest writes, and attempt at optimizing the ones that last for too long. + + +## How to limit the `0xDEAD10CC` exception + +> The exception code 0xDEAD10CC indicates that an application has been terminated by the OS because it held on to a file lock or sqlite database lock during suspension. + +See https://developer.apple.com/library/archive/technotes/tn2151/_index.html for more information about this exception. + +1. If you use SQLCipher, use SQLCipher 4+, and call the `cipher_plaintext_header_size` pragma from your database preparation function: + + ```swift + var configuration = Configuration() + configuration.prepareDatabase = { (db: Database) in + try db.usePassphrase("secret") + try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32") + } + let dbPool = try DatabasePool(path: ..., configuration: configuration) + ``` + + This will avoid https://github.com/sqlcipher/sqlcipher/issues/255. + +2. [**:fire: EXPERIMENTAL**](README.md#what-are-experimental-features) In each process that wants to write in the database: + + Set the `observesSuspensionNotifications` configuration flag: + + ```swift + var configuration = Configuration() + configuration.suspendsOnBackgroundTimeExpiration = true + let dbPool = try DatabasePool(path: ..., configuration: configuration) + ``` + + Post `Database.suspendNotification` when the application is about to be [suspended](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle). You can for example post this notification from `UIApplicationDelegate.applicationDidEnterBackground(_:)`, or in the expiration handler of a [background task](https://forums.developer.apple.com/thread/85066). + + ```swift + @UIApplicationMain + class AppDelegate: UIResponder, UIApplicationDelegate { + func applicationDidEnterBackground(_ application: UIApplication) { + // Suspend databases + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } + } + ``` + + Post `Database.resumeNotification` from `UIApplicationDelegate.applicationWillEnterForeground(_:)` (or `SceneDelegate.sceneWillEnterForeground(_:)` for scene-based applications): + + ```swift + @UIApplicationMain + class AppDelegate: UIResponder, UIApplicationDelegate { + func applicationWillEnterForeground(_ application: UIApplication) { + // Resume databases + NotificationCenter.default.post(name: Database.resumeNotification, object: self) + } + } + ``` + + If the application uses the background modes supported by iOS, post `Database.resumeNotification` method from each and every background mode callback that may use the database. For example, if your application supports background fetches: + + ```swift + @UIApplicationMain + class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + // Resume databases + NotificationCenter.default.post(name: Database.resumeNotification, object: self) + // Proceed with background fetch + ... + } + } + ``` + + Suspended databases greatly reduce the odds of `0xDEAD10CC` exception are greatly reduced. If you see one in your crash logs, please open an issue! + + In exchange, you will get `SQLITE_INTERRUPT` (9) or `SQLITE_ABORT` (4) errors, with messages "Database is suspended", "Transaction was aborted", or "interrupted", for any attempt at writing in the database when it is suspended. + + You can catch those errors: + + ```swift + do { + try dbPool.write { db in ... } + } catch let error as DatabaseError where error.isInterruptionError { + // Oops, the database is suspended. + // Maybe try again after database is resumed? + } + ``` + + +## How to perform cross-process database observation + +GRDB [Database Observation] features, as well as [GRDBCombine] and [RxGRDB], are not able to notify database changes performed by other processes. + +Whenever you need to notify other processes that the database has been changed, you will have to use a cross-process notification mechanism such as [NSFileCoordinator] or [CFNotificationCenterGetDarwinNotifyCenter]. + +You can trigger those notifications automatically with [DatabaseRegionObservation]: + +```swift +// Notify all changes made to the "player" and "team" database tables +let observation = DatabaseRegionObservation(tracking: Player.all(), Team.all()) +let observer = try observation.start(in: dbPool) { (db: Database) in + // Notify other processes +} + +// Notify all changes made to the database +let observation = DatabaseRegionObservation(tracking: DatabaseRegion.fullDatabase) +let observer = try observation.start(in: dbPool) { (db: Database) in + // Notify other processes +} +``` + +[Use a Database Pool]: #use-a-database-pool +[How to limit the `SQLITE_BUSY` error]: #how-to-limit-the-sqlite_busy-error +[How to limit the `0xDEAD10CC` exception]: #how-to-limit-the-0xdead10cc-exception +[How to perform cross-process database observation]: #how-to-perform-cross-process-database-observation +[Database Pool]: ../README.md#database-pools +[Database Observation]: ../README.md#database-changes-observation +[GRDBCombine]: http://github.com/groue/GRDBCombine +[RxGRDB]: https://github.com/RxSwiftCommunity/RxGRDB +[NSFileCoordinator]: https://developer.apple.com/documentation/foundation/nsfilecoordinator +[CFNotificationCenterGetDarwinNotifyCenter]: https://developer.apple.com/documentation/corefoundation/1542572-cfnotificationcentergetdarwinnot +[DatabaseRegionObservation]: ../README.md#databaseregionobservation diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 22222e100c..538aa73fe4 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -494,9 +494,9 @@ 567F45AC1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 567F45A71F888B2600030B59 /* TruncateOptimizationTests.swift */; }; 568068311EBBA26100EFB8AA /* SQLRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */; }; 568068351EBBA26100EFB8AA /* SQLRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */; }; - 5682D721239582AA004B58C4 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D71A239582AA004B58C4 /* DatabaseLockPreventionTests.swift */; }; - 5682D722239582AA004B58C4 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D71A239582AA004B58C4 /* DatabaseLockPreventionTests.swift */; }; - 5682D723239582AA004B58C4 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D71A239582AA004B58C4 /* DatabaseLockPreventionTests.swift */; }; + 5682D721239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D71A239582AA004B58C4 /* DatabaseSuspensionTests.swift */; }; + 5682D722239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D71A239582AA004B58C4 /* DatabaseSuspensionTests.swift */; }; + 5682D723239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D71A239582AA004B58C4 /* DatabaseSuspensionTests.swift */; }; 568735A21CEDE16C009B9116 /* Betty.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 5687359E1CEDE16C009B9116 /* Betty.jpeg */; }; 568D131F2207213E00674B58 /* SQLQueryGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568D13182207213E00674B58 /* SQLQueryGenerator.swift */; }; 568D13202207213E00674B58 /* SQLQueryGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568D13182207213E00674B58 /* SQLQueryGenerator.swift */; }; @@ -1500,7 +1500,7 @@ 567F0B2C220F0E2E00D111FB /* SQLInterpolationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLInterpolationTests.swift; sourceTree = ""; }; 567F45A71F888B2600030B59 /* TruncateOptimizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncateOptimizationTests.swift; sourceTree = ""; }; 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLRequestTests.swift; sourceTree = ""; }; - 5682D71A239582AA004B58C4 /* DatabaseLockPreventionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseLockPreventionTests.swift; sourceTree = ""; }; + 5682D71A239582AA004B58C4 /* DatabaseSuspensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSuspensionTests.swift; sourceTree = ""; }; 5687359E1CEDE16C009B9116 /* Betty.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Betty.jpeg; sourceTree = ""; }; 568D13182207213E00674B58 /* SQLQueryGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLQueryGenerator.swift; sourceTree = ""; }; 5690AFD72120589A001530EA /* InsertRecordCodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsertRecordCodableTests.swift; sourceTree = ""; }; @@ -2233,7 +2233,6 @@ 563DE4EC231A91E2005081B7 /* DatabaseConfigurationTests.swift */, 56A238161B9C74A90082EB20 /* DatabaseErrorTests.swift */, 560C97C61BFD0B8400BF8471 /* DatabaseFunctionTests.swift */, - 5682D71A239582AA004B58C4 /* DatabaseLockPreventionTests.swift */, 567DAF341EAB789800FC0928 /* DatabaseLogErrorTests.swift */, 560A37A91C90084600949E71 /* DatabasePool */, 563363BB1C93FD32000BE133 /* DatabaseQueue */, @@ -2241,6 +2240,7 @@ 564CE5BD21B8FFA300652B19 /* DatabaseRegionObservationTests.swift */, 56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */, 566A8424204120B700E50BFD /* DatabaseSnapshotTests.swift */, + 5682D71A239582AA004B58C4 /* DatabaseSuspensionTests.swift */, 56A238131B9C74A90082EB20 /* DatabaseTests.swift */, 564FCE5D20F7E11A00202B90 /* DatabaseValueConversionErrorTests.swift */, 56A2381B1B9C74A90082EB20 /* DatabaseValueConversionTests.swift */, @@ -3491,7 +3491,7 @@ 56EA869F1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift in Sources */, 5615B288222B17C000061C1C /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */, 567F45AC1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */, - 5682D722239582AA004B58C4 /* DatabaseLockPreventionTests.swift in Sources */, + 5682D722239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */, 5657AB621D108BA9006283EF /* FoundationNSURLTests.swift in Sources */, 5657AB6A1D108BA9006283EF /* FoundationURLTests.swift in Sources */, 563B06C42185D29F00B38F35 /* ValueObservationExtentTests.swift in Sources */, @@ -3698,7 +3698,7 @@ 56176C591EACCCC7000F3F2B /* FTS5CustomTokenizerTests.swift in Sources */, 5615B289222B17C000061C1C /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */, 56D4968A1D81316E008276D7 /* DatabaseValueConvertibleSubclassTests.swift in Sources */, - 5682D721239582AA004B58C4 /* DatabaseLockPreventionTests.swift in Sources */, + 5682D721239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */, 56D496601D81304E008276D7 /* FoundationNSUUIDTests.swift in Sources */, 567F45A81F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */, 563B06C32185D29F00B38F35 /* ValueObservationExtentTests.swift in Sources */, @@ -4038,7 +4038,7 @@ AAA4DD4D230F262000C74B15 /* DatabasePoolReadOnlyTests.swift in Sources */, AAA4DD4E230F262000C74B15 /* AssociationHasOneThroughDecodableRecordTests.swift in Sources */, AAA4DD4F230F262000C74B15 /* TruncateOptimizationTests.swift in Sources */, - 5682D723239582AA004B58C4 /* DatabaseLockPreventionTests.swift in Sources */, + 5682D723239582AA004B58C4 /* DatabaseSuspensionTests.swift in Sources */, AAA4DD50230F262000C74B15 /* FoundationNSURLTests.swift in Sources */, AAA4DD51230F262000C74B15 /* FoundationURLTests.swift in Sources */, AAA4DD52230F262000C74B15 /* ValueObservationExtentTests.swift in Sources */, diff --git a/GRDB/Core/Configuration.swift b/GRDB/Core/Configuration.swift index e9d3c985c2..8a08bae7ac 100644 --- a/GRDB/Core/Configuration.swift +++ b/GRDB/Core/Configuration.swift @@ -98,6 +98,19 @@ public struct Configuration { /// - Default value: false public var acceptsDoubleQuotedStringLiterals = false + /// When true, the `Database.suspendNotification` and + /// `Database.resumeNotification` suspend and resume the database. Database + /// suspension helps avoiding the [`0xdead10cc` + /// exception](https://developer.apple.com/library/archive/technotes/tn2151/_index.html). + /// + /// During suspension, all database accesses but reads in WAL mode may throw + /// a DatabaseError of code `SQLITE_INTERRUPT`, or `SQLITE_ABORT`. You can + /// check for those error codes with the + /// `DatabaseError.isInterruptionError` property. + /// + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public var observesSuspensionNotifications = false + // MARK: - Encryption #if SQLITE_HAS_CODEC diff --git a/GRDB/Core/Database+Statements.swift b/GRDB/Core/Database+Statements.swift index ab1fe6af24..11046d99e7 100644 --- a/GRDB/Core/Database+Statements.swift +++ b/GRDB/Core/Database+Statements.swift @@ -221,8 +221,10 @@ extension Database { extension Database { func executeUpdateStatement(_ statement: UpdateStatement) throws { + // Two things must prevent the statement from executing: aborted + // transactions, and database suspension. try checkForAbortedTransaction(sql: statement.sql, arguments: statement.arguments) - try checkForLockPrevention(from: statement) + try checkForSuspensionViolation(from: statement) let authorizer = observationBroker.updateStatementWillExecute(statement) let sqliteStatement = statement.sqliteStatement @@ -292,8 +294,10 @@ extension Database { @inline(__always) func selectStatementWillExecute(_ statement: SelectStatement) throws { + // Two things must prevent the statement from executing: aborted + // transactions, and database suspension. try checkForAbortedTransaction(sql: statement.sql, arguments: statement.arguments) - try checkForLockPrevention(from: statement) + try checkForSuspensionViolation(from: statement) if _isRecordingSelectedRegion { // Don't record schema introspection queries, which may be diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index c8ea618e55..d4256b4d82 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -150,10 +150,10 @@ public final class Database { /// Support for checkForAbortedTransaction() var isInsideTransactionBlock = false - /// Support for checkForLockPrevention(from:) - var preventsLock = LockedBox(value: false) + /// Support for checkForSuspensionViolation(from:) + var isSuspended = LockedBox(value: false) - /// Support for checkForLockPrevention(from:) + /// Support for checkForSuspensionViolation(from:) /// This cache is never cleared: we assume journal mode never changes. var journalModeCache: String? @@ -640,69 +640,73 @@ public final class Database { sqlite3_interrupt(sqliteConnection) } - // MARK: - Lock Prevention + // MARK: - Database Suspension - /// Starts preventing database locks. + /// When this notification is posted, databases which were opened with the + /// `Configuration.observesSuspensionNotifications` flag are suspended. /// - /// This method can be called from any thread. + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public static let suspendNotification = Notification.Name("GRDB.Database.Suspend") + + /// When this notification is posted, databases which were opened with the + /// `Configuration.observesSuspensionNotifications` flag are resumed. /// - /// During lock prevention, any lock is released as soon as possible, and - /// lock acquisition is prevented. + /// [**Experimental**](http://github.com/groue/GRDB.swift#what-are-experimental-features) + public static let resumeNotification = Notification.Name("GRDB.Database.Resume") + + /// Suspends the database. A suspended database prevents database locks in + /// order to avoid the [`0xdead10cc` + /// exception](https://developer.apple.com/library/archive/technotes/tn2151/_index.html). + /// + /// This method can be called from any thread. /// - /// All database accesses may throw a DatabaseError of code - /// `SQLITE_INTERRUPT`, or `SQLITE_ABORT`, except reads in WAL mode. + /// During suspension, any lock is released as soon as possible, and + /// lock acquisition is prevented. All database accesses may throw a + /// DatabaseError of code `SQLITE_INTERRUPT`, or `SQLITE_ABORT`, except + /// reads in WAL mode. /// - /// Lock prevention ends with stopPreventingLock(). - func startPreventingLock() { - preventsLock.write { preventsLock in - if preventsLock { + /// Suspension ends with resume(). + func suspend() { + isSuspended.write { isSuspended in + if isSuspended { return } // Prevent future lock acquisition - preventsLock = true + isSuspended = true // Interrupt the database because this may trigger an // SQLITE_INTERRUPT error which may itself abort a transaction and // release a lock. See https://www.sqlite.org/c3ref/interrupt.html interrupt() - // What about the eventual remaining lock? - // If the database had been opened with the SQLITE_OPEN_FULLMUTEX - // flag, we could safely execute a rollback statement: - // - // if configuration.SQLiteOpenFlags & SQLITE_OPEN_FULLMUTEX != 0 { - // sqlite3_exec(sqliteConnection, "ROLLBACK", nil, nil, nil) - // } - // - // But: - // - // - we currently use SQLITE_OPEN_NOMUTEX instead of SQLITE_OPEN_FULLMUTEX. - // - the rollback may fail, and a lock could remain. - // - // We work around this situation by issuing a rollback on next - // database access which requires a lock, - // in preventExclusiveLock(from:). + // Now what about the eventual remaining lock? We'll issue a + // rollback on next database access which requires a lock, in + // checkForSuspensionViolation(from:). } } - /// Ends lock prevention. See startPreventingLock(). + /// Resumes the database. A resumed database stops preventing database locks + /// in order to avoid the [`0xdead10cc` + /// exception](https://developer.apple.com/library/archive/technotes/tn2151/_index.html). /// /// This method can be called from any thread. - func stopPreventingLock() { - preventsLock.write { preventsLock in - preventsLock = false + /// + /// See suspend(). + func resume() { + isSuspended.write { isSuspended in + isSuspended = false } } - /// Support for checkForLockPrevention(from:) + /// Support for checkForSuspensionViolation(from:) private func journalMode() throws -> String { if let journalMode = journalModeCache { return journalMode } // Don't return String.fetchOne(self, sql: "PRAGMA journal_mode"), so - // that we don't create an infinite loop in checkForLockPrevention(from:) + // that we don't create an infinite loop in checkForSuspensionViolation(from:) var statement: SQLiteStatement? = nil let sql = "PRAGMA journal_mode" sqlite3_prepare_v2(sqliteConnection, sql, -1, &statement, nil) @@ -716,11 +720,12 @@ public final class Database { return journalMode } - /// Throws SQLITE_ABORT during lock prevention, if statement would lock - /// the database. - func checkForLockPrevention(from statement: Statement) throws { - try preventsLock.read { preventsLock in - guard preventsLock else { + /// Throws SQLITE_ABORT for suspended databases, if statement would lock + /// the database, in order to avoid the [`0xdead10cc` + /// exception](https://developer.apple.com/library/archive/technotes/tn2151/_index.html). + func checkForSuspensionViolation(from statement: Statement) throws { + try isSuspended.read { isSuspended in + guard isSuspended else { return } @@ -748,15 +753,15 @@ public final class Database { } // Attempt at releasing an eventual lock with ROLLBACk, - // as explained in Database.startPreventingLock(). + // as explained in Database.suspend(). // // Use sqlite3_exec instead of `try? rollback()` in order to avoid - // an infinite loop in checkForLockPrevention(from:) + // an infinite loop in checkForSuspensionViolation(from:) _ = sqlite3_exec(sqliteConnection, "ROLLBACK", nil, nil, nil) throw DatabaseError( resultCode: .SQLITE_ABORT, - message: "Aborted due to lock prevention", + message: "Database is suspended", sql: statement.sql, arguments: statement.arguments) } diff --git a/GRDB/Core/DatabaseError.swift b/GRDB/Core/DatabaseError.swift index c9140cd4f3..10b04be4cb 100644 --- a/GRDB/Core/DatabaseError.swift +++ b/GRDB/Core/DatabaseError.swift @@ -250,6 +250,25 @@ public struct DatabaseError: Error, CustomStringConvertible, CustomNSError { let arguments: StatementArguments? } +extension DatabaseError { + // TODO: test + /// Returns true if the error has code `SQLITE_ABORT` or `SQLITE_INTERRUPT`. + /// + /// Such an error can be thrown when a database has been interrupted, or + /// when the database is suspended. + /// + /// See `DatabaseReader.interrupt()` and `DatabaseReader.suspend()` for + /// more information. + public var isInterruptionError: Bool { + switch resultCode { + case .SQLITE_ABORT, .SQLITE_INTERRUPT: + return true + default: + return false + } + } +} + // CustomStringConvertible extension DatabaseError { /// :nodoc: diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index 31a7743a8e..50653f20f9 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -15,7 +15,8 @@ import UIKit public final class DatabasePool: DatabaseWriter { private let writer: SerializedDatabase private var readerPool: Pool! - var readerConfiguration: Configuration + // TODO: remove when the deprecated change(passphrase:) method turns unavailable. + private var readerConfiguration: Configuration private var functions = Set() private var collations = Set() @@ -64,13 +65,36 @@ public final class DatabasePool: DatabaseWriter { // Readers readerConfiguration = configuration readerConfiguration.readonly = true + // Readers use deferred transactions by default. // Other transaction kinds are forbidden by SQLite in read-only connections. readerConfiguration.defaultTransactionKind = .deferred + // Readers can't allow dangling transactions because there's no // guarantee that one can get the same reader later in order to close // an opened transaction. readerConfiguration.allowsUnsafeTransactions = false + + // https://www.sqlite.org/wal.html#sometimes_queries_return_sqlite_busy_in_wal_mode + // > But there are some obscure cases where a query against a WAL-mode + // > database can return SQLITE_BUSY, so applications should be prepared + // > for that happenstance. + // > + // > - If another database connection has the database mode open in + // > exclusive locking mode [...] + // > - When the last connection to a particular database is closing, + // > that connection will acquire an exclusive lock for a short time + // > while it cleans up the WAL and shared-memory files [...] + // > - If the last connection to a database crashed, then the first new + // > connection to open the database will start a recovery process. An + // > exclusive lock is held during recovery. [...] + // + // The whole point of WAL readers is to avoid SQLITE_BUSY, so let's + // setup a busy handler for pool readers, in order to workaround those + // "obscure cases" that may happen when the database is shared between + // multiple processes. + readerConfiguration.busyMode = .timeout(10) + var readerCount = 0 readerPool = Pool(maximumCount: configuration.maximumReaderCount, makeElement: { [unowned self] in readerCount += 1 // protected by pool (TODO: documented this protection behavior) @@ -112,9 +136,10 @@ public final class DatabasePool: DatabaseWriter { } } } + + setupSuspension() } - #if os(iOS) deinit { // Undo job done in setupMemoryManagement() // @@ -122,7 +147,6 @@ public final class DatabasePool: DatabaseWriter { // Explicit unregistration is required before iOS 9 and OS X 10.11. NotificationCenter.default.removeObserver(self) } - #endif private func setupDatabase(_ db: Database) { for function in functions { @@ -259,22 +283,50 @@ extension DatabasePool: DatabaseReader { readerPool.forEach { $0.interrupt() } } - // MARK: - Lock Prevention + // MARK: - Database Suspension - public func startPreventingLock() { + func suspend() { if configuration.readonly { - // read-only WAL connections can't acquire locks + // read-only WAL connections can't acquire locks and do not need to + // be suspended. return } - writer.startPreventingLock() + writer.suspend() } - public func stopPreventingLock() { + func resume() { if configuration.readonly { - // read-only WAL connections can't acquire locks + // read-only WAL connections can't acquire locks and do not need to + // be suspended. return } - writer.stopPreventingLock() + writer.resume() + } + + private func setupSuspension() { + if configuration.observesSuspensionNotifications { + let center = NotificationCenter.default + center.addObserver( + self, + selector: #selector(DatabasePool.suspend(_:)), + name: Database.suspendNotification, + object: nil) + center.addObserver( + self, + selector: #selector(DatabasePool.resume(_:)), + name: Database.resumeNotification, + object: nil) + } + } + + @objc + private func suspend(_ notification: Notification) { + suspend() + } + + @objc + private func resume(_ notification: Notification) { + resume() } // MARK: - Reading from Database diff --git a/GRDB/Core/DatabaseQueue.swift b/GRDB/Core/DatabaseQueue.swift index 933e507736..99c3928a8d 100644 --- a/GRDB/Core/DatabaseQueue.swift +++ b/GRDB/Core/DatabaseQueue.swift @@ -41,6 +41,8 @@ public final class DatabaseQueue: DatabaseWriter { configuration: configuration, schemaCache: SimpleDatabaseSchemaCache(), defaultLabel: "GRDB.DatabaseQueue") + + setupSuspension() } /// Opens an in-memory SQLite database. @@ -59,7 +61,6 @@ public final class DatabaseQueue: DatabaseWriter { defaultLabel: "GRDB.DatabaseQueue") } - #if os(iOS) deinit { // Undo job done in setupMemoryManagement() // @@ -67,7 +68,6 @@ public final class DatabaseQueue: DatabaseWriter { // Explicit unregistration is required before iOS 9 and OS X 10.11. NotificationCenter.default.removeObserver(self) } - #endif } extension DatabaseQueue { @@ -155,14 +155,40 @@ extension DatabaseQueue { writer.interrupt() } - // MARK: - Lock Prevention + // MARK: - Database Suspension + + func suspend() { + writer.suspend() + } + + func resume() { + writer.resume() + } + + private func setupSuspension() { + if configuration.observesSuspensionNotifications { + let center = NotificationCenter.default + center.addObserver( + self, + selector: #selector(DatabaseQueue.suspend(_:)), + name: Database.suspendNotification, + object: nil) + center.addObserver( + self, + selector: #selector(DatabaseQueue.resume(_:)), + name: Database.resumeNotification, + object: nil) + } + } - public func startPreventingLock() { - writer.startPreventingLock() + @objc + private func suspend(_ notification: Notification) { + suspend() } - public func stopPreventingLock() { - writer.stopPreventingLock() + @objc + private func resume(_ notification: Notification) { + resume() } // MARK: - Reading from Database diff --git a/GRDB/Core/DatabaseReader.swift b/GRDB/Core/DatabaseReader.swift index 62cce61951..e1564e2588 100644 --- a/GRDB/Core/DatabaseReader.swift +++ b/GRDB/Core/DatabaseReader.swift @@ -82,27 +82,10 @@ public protocol DatabaseReader: AnyObject { /// try Player(...).insert(db) // success /// try db.commit() // throws SQLITE_ERROR "cannot commit - no transaction is active" /// } - func interrupt() - - // MARK: - Lock Prevention - - /// Starts preventing database locks. - /// - /// This method can be called from any thread. - /// - /// During lock prevention, any lock is released as soon as possible, and - /// lock acquisition is prevented. - /// - /// All database accesses may throw a DatabaseError of code - /// `SQLITE_INTERRUPT`, or `SQLITE_ABORT`, except reads in WAL mode. - /// - /// Lock prevention ends with stopPreventingLock(). - func startPreventingLock() - - /// Ends lock prevention. See startPreventingLock(). /// - /// This method can be called from any thread. - func stopPreventingLock() + /// Both SQLITE_ABORT and SQLITE_INTERRUPT errors can be checked with the + /// `DatabaseError.isInterruptionError` property. + func interrupt() // MARK: - Read From Database @@ -335,18 +318,6 @@ public final class AnyDatabaseReader: DatabaseReader { base.interrupt() } - // MARK: - Lock Prevention - - /// :nodoc: - public func startPreventingLock() { - base.startPreventingLock() - } - - /// :nodoc: - public func stopPreventingLock() { - base.stopPreventingLock() - } - // MARK: - Reading from Database /// :nodoc: diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index 4972ce11d4..df92854f96 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -53,16 +53,6 @@ extension DatabaseSnapshot { serializedDatabase.interrupt() } - // MARK: - Lock Prevention - - public func startPreventingLock() { - // read-only WAL connections can't acquire locks - } - - public func stopPreventingLock() { - // read-only WAL connections can't acquire locks - } - // 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 5834a6e507..bc730c4069 100644 --- a/GRDB/Core/DatabaseWriter.swift +++ b/GRDB/Core/DatabaseWriter.swift @@ -608,18 +608,6 @@ public final class AnyDatabaseWriter: DatabaseWriter { base.interrupt() } - // MARK: - Lock Prevention - - /// :nodoc: - public func startPreventingLock() { - base.startPreventingLock() - } - - /// :nodoc: - public func stopPreventingLock() { - base.stopPreventingLock() - } - // MARK: - Reading from Database /// :nodoc: diff --git a/GRDB/Core/SerializedDatabase.swift b/GRDB/Core/SerializedDatabase.swift index 4e197c6397..e90724d493 100644 --- a/GRDB/Core/SerializedDatabase.swift +++ b/GRDB/Core/SerializedDatabase.swift @@ -204,14 +204,14 @@ final class SerializedDatabase { db.interrupt() } - func startPreventingLock() { + func suspend() { // Intentionally not scheduled in our serial queue - db.startPreventingLock() + db.suspend() } - func stopPreventingLock() { + func resume() { // Intentionally not scheduled in our serial queue - db.stopPreventingLock() + db.resume() } /// Fatal error if current dispatch queue is not valid. diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 6002b53110..8c52baee62 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -113,6 +113,8 @@ 5644DE7920F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */; }; 5644DE7F20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */; }; 5644DE8020F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */; }; + 564B3D72239BDBD6007BF308 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564B3D70239BDBD6007BF308 /* DatabaseSuspensionTests.swift */; }; + 564B3D73239BDBD6007BF308 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564B3D70239BDBD6007BF308 /* DatabaseSuspensionTests.swift */; }; 564CE43C21AA955B00652B19 /* ValueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE43B21AA955B00652B19 /* ValueObserver.swift */; }; 564CE43D21AA955B00652B19 /* ValueObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE43B21AA955B00652B19 /* ValueObserver.swift */; }; 564CE4E021B2E04500652B19 /* ValueObservationCompactMapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564CE4DF21B2E04500652B19 /* ValueObservationCompactMapTests.swift */; }; @@ -131,8 +133,6 @@ 564F9C251F069B4E00877A00 /* DatabaseAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */; }; 564F9C301F07611500877A00 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */; }; 564F9C331F07611800877A00 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */; }; - 565050E123940CE400A7F660 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565050DF23940CE400A7F660 /* DatabaseLockPreventionTests.swift */; }; - 565050E223940CE400A7F660 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565050DF23940CE400A7F660 /* DatabaseLockPreventionTests.swift */; }; 5653EB6820961FB200F46237 /* AssociationParallelSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EB5820961FB000F46237 /* AssociationParallelSQLTests.swift */; }; 5653EB6920961FB200F46237 /* AssociationParallelSQLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EB5820961FB000F46237 /* AssociationParallelSQLTests.swift */; }; 5653EB6A20961FB200F46237 /* AssociationBelongsToFetchableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EB5920961FB100F46237 /* AssociationBelongsToFetchableRecordTests.swift */; }; @@ -800,6 +800,7 @@ 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = ""; }; 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversionErrorTests.swift; sourceTree = ""; }; 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = ""; }; + 564B3D70239BDBD6007BF308 /* DatabaseSuspensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSuspensionTests.swift; sourceTree = ""; }; 564CE43B21AA955B00652B19 /* ValueObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObserver.swift; sourceTree = ""; }; 564CE4DF21B2E04500652B19 /* ValueObservationCompactMapTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationCompactMapTests.swift; sourceTree = ""; }; 564CE4E221B2E05400652B19 /* ValueObservationMapTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationMapTests.swift; sourceTree = ""; }; @@ -809,7 +810,6 @@ 564E73E7203DA278000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseFunction.swift; sourceTree = ""; }; - 565050DF23940CE400A7F660 /* DatabaseLockPreventionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseLockPreventionTests.swift; sourceTree = ""; }; 5653EB5820961FB000F46237 /* AssociationParallelSQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationParallelSQLTests.swift; sourceTree = ""; }; 5653EB5920961FB100F46237 /* AssociationBelongsToFetchableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationBelongsToFetchableRecordTests.swift; sourceTree = ""; }; 5653EB5A20961FB100F46237 /* AssociationParallelDecodableRecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationParallelDecodableRecordTests.swift; sourceTree = ""; }; @@ -1561,7 +1561,6 @@ 563DE4F6231A91F6005081B7 /* DatabaseConfigurationTests.swift */, 56A238161B9C74A90082EB20 /* DatabaseErrorTests.swift */, 560C97C61BFD0B8400BF8471 /* DatabaseFunctionTests.swift */, - 565050DF23940CE400A7F660 /* DatabaseLockPreventionTests.swift */, 567DAF341EAB789800FC0928 /* DatabaseLogErrorTests.swift */, 560A37A91C90084600949E71 /* DatabasePool */, 563363BB1C93FD32000BE133 /* DatabaseQueue */, @@ -1569,6 +1568,7 @@ 564CE5C521B8FFE500652B19 /* DatabaseRegionObservationTests.swift */, 56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */, 566A843420413DE400E50BFD /* DatabaseSnapshotTests.swift */, + 564B3D70239BDBD6007BF308 /* DatabaseSuspensionTests.swift */, 56A238131B9C74A90082EB20 /* DatabaseTests.swift */, 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */, 56A2381B1B9C74A90082EB20 /* DatabaseValueConversionTests.swift */, @@ -2350,7 +2350,6 @@ 6340BF871E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift in Sources */, 56057C662291C7E500A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */, 56AF74721D41FB9C005E9FF3 /* DatabaseValueConvertibleEscapingTests.swift in Sources */, - 565050E123940CE400A7F660 /* DatabaseLockPreventionTests.swift in Sources */, F3BA80D21CFB2FF3003DC1BA /* PersistableRecordTests.swift in Sources */, 5656A7F722946B1B001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift in Sources */, 5615B259222AE19100061C1C /* AssociationHasOneThroughSQLDerivationTests.swift in Sources */, @@ -2359,6 +2358,7 @@ F3BA80FD1CFB3024003DC1BA /* TransactionObserverSavepointsTests.swift in Sources */, F3BA811F1CFB3063003DC1BA /* RecordMinimalPrimaryKeySingleTests.swift in Sources */, 5657AB451D108BA9006283EF /* FoundationNSDataTests.swift in Sources */, + 564B3D73239BDBD6007BF308 /* DatabaseSuspensionTests.swift in Sources */, 56CC9255201E094600CB597E /* PrefixCursorTests.swift in Sources */, F3BA80EA1CFB3016003DC1BA /* RowFromStatementTests.swift in Sources */, F3BA80AF1CFB2FB1003DC1BA /* DatabaseCollationTests.swift in Sources */, @@ -2690,7 +2690,6 @@ F3BA80EC1CFB3017003DC1BA /* RowCopiedFromStatementTests.swift in Sources */, 56057C652291C7E500A7CB10 /* AssociationHasManyThroughRowScopeTests.swift in Sources */, F3BA80D41CFB2FF4003DC1BA /* PersistableRecordTests.swift in Sources */, - 565050E223940CE400A7F660 /* DatabaseLockPreventionTests.swift in Sources */, 6340BF831E5E3F7900832805 /* RecordPersistenceConflictPolicy.swift in Sources */, 5656A7F622946B1B001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift in Sources */, 5615B258222AE19100061C1C /* AssociationHasOneThroughSQLDerivationTests.swift in Sources */, @@ -2699,6 +2698,7 @@ 5698AC4C1DA2D48A0056AF8C /* FTS3RecordTests.swift in Sources */, F3BA80EF1CFB3017003DC1BA /* RowFromStatementTests.swift in Sources */, 5657AB511D108BA9006283EF /* FoundationNSNumberTests.swift in Sources */, + 564B3D72239BDBD6007BF308 /* DatabaseSuspensionTests.swift in Sources */, 56CC9254201E094600CB597E /* PrefixCursorTests.swift in Sources */, 562393331DEDFC5700A6B01F /* AnyCursorTests.swift in Sources */, 56B964DB1DA5216B0002DA19 /* FTS5RecordTests.swift in Sources */, diff --git a/README.md b/README.md index 61c1af921b..304d0e6b9b 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,7 @@ Documentation - [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. +- [App Group Containers] #### Good to Know @@ -6674,28 +6675,34 @@ For example: ```swift try dbQueue.write { db in - // interrupted: try Player(...).insert(db) // throws SQLITE_INTERRUPT - // not executed: - try Player(...).insert(db) + try Player(...).insert(db) // not executed } // 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 { } + try Player(...).insert(db) // throws SQLITE_ABORT } // throws SQLITE_ABORT ``` +You can catch both `SQLITE_INTERRUPT` and `SQLITE_ABORT` errors with the `DatabaseError.isInterruptionError` property: + +```swift +do { + try dbPool.write { db in ... } +} catch let error as DatabaseError where error.isInterruptionError { + // Oops, the database was interrupted. +} +``` + For more information, see [Interrupt A Long-Running Query](https://www.sqlite.org/c3ref/interrupt.html). @@ -7140,7 +7147,7 @@ You can catch those errors and wait for [UIApplicationDelegate.applicationProtec - [DatabaseWriter and DatabaseReader Protocols](#databasewriter-and-databasereader-protocols) - [Asynchronous APIs](#asynchronous-apis) - [Unsafe Concurrency APIs](#unsafe-concurrency-apis) -- [Dealing with External Connections](#dealing-with-external-connections) +- [App Group Containers] ### Guarantees and Rules @@ -7181,7 +7188,7 @@ Those guarantees hold as long as you follow three rules: See the [Demo Application] for a sample app that sets up a single database queue that is available throughout the application. - If there are several instances of database queues or pools that write in the same database, a multi-threaded application will eventually face "database is locked" errors. See [Dealing with External Connections](#dealing-with-external-connections). + See [App Group Containers] for the specific setup required by applications that share their database files. ```swift // SAFE CONCURRENCY @@ -7683,24 +7690,6 @@ try writer.asyncWriteWithoutTransaction { db in There is a single valid use case for reentrant methods, which is when you are unable to control database access scheduling. -### Dealing with External Connections - -The first rule of GRDB is: - -- **[Rule 1](#guarantees-and-rules)**: Have a unique instance of DatabaseQueue or DatabasePool connected to any database file. - -This means that dealing with external connections is not a focus of GRDB. [Guarantees](#guarantees-and-rules) of GRDB may or may not hold as soon as some external connection modifies a database. - -If you absolutely need multiple connections, then: - -- Reconsider your position -- Read about [isolation in SQLite](https://www.sqlite.org/isolation.html) -- Learn about [locks and transactions](https://www.sqlite.org/lang_transaction.html) -- Become a master of the [WAL mode](https://www.sqlite.org/wal.html) -- Prepare to setup a [busy handler](https://www.sqlite.org/c3ref/busy_handler.html) with [Configuration.busyMode](http://groue.github.io/GRDB.swift/docs/4.6/Structs/Configuration.html) -- [Ask questions](https://github.com/groue/GRDB.swift/issues) - - ## Performance GRDB is a reasonably fast library, and can deliver quite efficient SQLite access. See [Comparing the Performances of Swift SQLite libraries](https://github.com/groue/GRDB.swift/wiki/Performance) for an overview. @@ -8186,6 +8175,10 @@ This chapter has [moved](Documentation/FullTextSearch.md). This chapter has [moved](Documentation/FullTextSearch.md#enabling-fts5-support). +#### Dealing with External Connections + +This chapter has been superseded by [App Group Containers]. + [Associations]: Documentation/AssociationsBasics.md [Beyond FetchableRecord]: #beyond-fetchablerecord [Codable Records]: #codable-records @@ -8215,3 +8208,4 @@ This chapter has [moved](Documentation/FullTextSearch.md#enabling-fts5-support). [custom SQLite build]: Documentation/CustomSQLiteBuilds.md [Combine]: https://developer.apple.com/documentation/combine [Demo Application]: Documentation/DemoApps/GRDBDemoiOS/README.md +[App Group Containers]: Documentation/AppGroupContainers.md diff --git a/TODO.md b/TODO.md index a345e5bd83..3d8e5841d5 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,7 @@ ## Features +- [ ] Measure the duration of transactions - [ ] Make deleteAll and updateAll work even for complex queries, with `DELETE FROM xxx WHERE rowid IN (SELECT rowid ...)` - [ ] Improve SQL generation for `Player.....fetchCount(db)`, especially with distinct. Try to avoid `SELECT COUNT(*) FROM (SELECT DISTINCT player.* ...)` - [ ] Alternative technique for custom SQLite builds: see the Podfile at https://github.com/CocoaPods/CocoaPods/issues/9104, and https://github.com/clemensg/sqlite3pod @@ -98,3 +99,4 @@ - File protection: https://github.com/ccgus/fmdb/issues/262 - File protection: https://lists.apple.com/archives/cocoa-dev/2012/Aug/msg00527.html - [iOS apps are terminated every time they enter the background if they share an encrypted database with an app extension](https://github.com/sqlcipher/sqlcipher/issues/255) +- [Cross-Process notifications with CFNotificationCenterGetDarwinNotifyCenter](https://www.avanderlee.com/swift/core-data-app-extension-data-sharing/) diff --git a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj index aa03a319f6..23fae76797 100644 --- a/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher3/GRDBTests.xcodeproj/project.pbxproj @@ -389,12 +389,12 @@ 564A214C226B8E18001F64F1 /* NumericOverflowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564A1F2E226B89CF001F64F1 /* NumericOverflowTests.swift */; }; 564A214D226B8E18001F64F1 /* RecordMinimalPrimaryKeySingleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564A1FAB226B89DC001F64F1 /* RecordMinimalPrimaryKeySingleTests.swift */; }; 564A2151226B8E18001F64F1 /* Betty.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 564A1F6F226B89D6001F64F1 /* Betty.jpeg */; }; + 564B3D78239BDC00007BF308 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564B3D77239BDC00007BF308 /* DatabaseSuspensionTests.swift */; }; + 564B3D79239BDC00007BF308 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564B3D77239BDC00007BF308 /* DatabaseSuspensionTests.swift */; }; 5656A802229474DD001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5656A801229474DC001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift */; }; 5656A803229474DD001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5656A801229474DC001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift */; }; 5676FBB022F5CF04004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5676FBAF22F5CF04004717D9 /* ValueObservationRegionRecordingTests.swift */; }; 5676FBB122F5CF04004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5676FBAF22F5CF04004717D9 /* ValueObservationRegionRecordingTests.swift */; }; - 5682D725239582C6004B58C4 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D724239582C6004B58C4 /* DatabaseLockPreventionTests.swift */; }; - 5682D726239582C6004B58C4 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D724239582C6004B58C4 /* DatabaseLockPreventionTests.swift */; }; 56915793231C0D6A00E1D237 /* PoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56915792231C0D6A00E1D237 /* PoolTests.swift */; }; 56915794231C0D6A00E1D237 /* PoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56915792231C0D6A00E1D237 /* PoolTests.swift */; }; 569BBA31228DF91000478429 /* AssociationPrefetchingFetchableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569BBA30228DF91000478429 /* AssociationPrefetchingFetchableRecordTests.swift */; }; @@ -608,9 +608,9 @@ 564A1FDC226B89E1001F64F1 /* AssociationParallelSQLTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationParallelSQLTests.swift; sourceTree = ""; }; 564A1FDD226B89E1001F64F1 /* FTS5RecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5RecordTests.swift; sourceTree = ""; }; 564A2156226B8E18001F64F1 /* GRDBTestsEncrypted.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBTestsEncrypted.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 564B3D77239BDC00007BF308 /* DatabaseSuspensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSuspensionTests.swift; sourceTree = ""; }; 5656A801229474DC001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationQueryInterfaceRequestTests.swift; sourceTree = ""; }; 5676FBAF22F5CF04004717D9 /* ValueObservationRegionRecordingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationRegionRecordingTests.swift; sourceTree = ""; }; - 5682D724239582C6004B58C4 /* DatabaseLockPreventionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseLockPreventionTests.swift; sourceTree = ""; }; 56915792231C0D6A00E1D237 /* PoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PoolTests.swift; sourceTree = ""; }; 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 = ""; }; @@ -715,7 +715,6 @@ 564A1F54226B89D2001F64F1 /* DatabaseDateEncodingStrategyTests.swift */, 564A1F65226B89D5001F64F1 /* DatabaseErrorTests.swift */, 564A1F37226B89CF001F64F1 /* DatabaseFunctionTests.swift */, - 5682D724239582C6004B58C4 /* DatabaseLockPreventionTests.swift */, 564A1F5C226B89D3001F64F1 /* DatabaseLogErrorTests.swift */, 564A1FD4226B89E0001F64F1 /* DatabaseMigratorTests.swift */, 564A1F6A226B89D5001F64F1 /* DatabasePoolBackupTests.swift */, @@ -737,6 +736,7 @@ 564A1F69226B89D5001F64F1 /* DatabaseRegionTests.swift */, 564A1F79226B89D7001F64F1 /* DatabaseSavepointTests.swift */, 564A1F3F226B89D0001F64F1 /* DatabaseSnapshotTests.swift */, + 564B3D77239BDC00007BF308 /* DatabaseSuspensionTests.swift */, 564A1FC7226B89DF001F64F1 /* DatabaseTests.swift */, 564A1FAF226B89DC001F64F1 /* DatabaseTimestampTests.swift */, 564A1FD6226B89E0001F64F1 /* DatabaseUUIDEncodingStrategyTests.swift */, @@ -1137,6 +1137,7 @@ 564A2091226B89E1001F64F1 /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */, 5676FBB022F5CF04004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */, 564A208F226B89E1001F64F1 /* AssociationParallelRowScopesTests.swift in Sources */, + 564B3D78239BDC00007BF308 /* DatabaseSuspensionTests.swift in Sources */, 564A2047226B89E1001F64F1 /* AnyCursorTests.swift in Sources */, 564A1FFC226B89E1001F64F1 /* DatabaseValueTests.swift in Sources */, 564A200E226B89E1001F64F1 /* RowFromStatementTests.swift in Sources */, @@ -1251,7 +1252,6 @@ 564A2027226B89E1001F64F1 /* AssociationParallelDecodableRecordTests.swift in Sources */, 564A1FEC226B89E1001F64F1 /* AssociationBelongsToDecodableRecordTests.swift in Sources */, 564A208B226B89E1001F64F1 /* DatabaseMigratorTests.swift in Sources */, - 5682D725239582C6004B58C4 /* DatabaseLockPreventionTests.swift in Sources */, 564A206D226B89E1001F64F1 /* ValueObservationCombineTests.swift in Sources */, 564A2022226B89E1001F64F1 /* CursorTests.swift in Sources */, 564A203A226B89E1001F64F1 /* DatabaseDateDecodingStrategyTests.swift in Sources */, @@ -1342,6 +1342,7 @@ 564A20D0226B8E18001F64F1 /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */, 5676FBB122F5CF04004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */, 564A20D1226B8E18001F64F1 /* AssociationParallelRowScopesTests.swift in Sources */, + 564B3D79239BDC00007BF308 /* DatabaseSuspensionTests.swift in Sources */, 564A20D2226B8E18001F64F1 /* AnyCursorTests.swift in Sources */, 564A20D3226B8E18001F64F1 /* DatabaseValueTests.swift in Sources */, 564A20D4226B8E18001F64F1 /* RowFromStatementTests.swift in Sources */, @@ -1456,7 +1457,6 @@ 564A2139226B8E18001F64F1 /* AssociationParallelDecodableRecordTests.swift in Sources */, 564A213A226B8E18001F64F1 /* AssociationBelongsToDecodableRecordTests.swift in Sources */, 564A213B226B8E18001F64F1 /* DatabaseMigratorTests.swift in Sources */, - 5682D726239582C6004B58C4 /* DatabaseLockPreventionTests.swift in Sources */, 564A213C226B8E18001F64F1 /* ValueObservationCombineTests.swift in Sources */, 564A213D226B8E18001F64F1 /* CursorTests.swift in Sources */, 564A213E226B8E18001F64F1 /* DatabaseDateDecodingStrategyTests.swift in Sources */, diff --git a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj index 791fa99440..6b2bbf765a 100644 --- a/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj +++ b/Tests/CocoaPods/SQLCipher4/GRDBTests.xcodeproj/project.pbxproj @@ -391,12 +391,12 @@ 564A2151226B8E18001F64F1 /* Betty.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 564A1F6F226B89D6001F64F1 /* Betty.jpeg */; }; 564A2159226C8F25001F64F1 /* db.SQLCipher3 in Resources */ = {isa = PBXBuildFile; fileRef = 564A2158226C8F24001F64F1 /* db.SQLCipher3 */; }; 564A215A226C8F25001F64F1 /* db.SQLCipher3 in Resources */ = {isa = PBXBuildFile; fileRef = 564A2158226C8F24001F64F1 /* db.SQLCipher3 */; }; + 564B3D75239BDBED007BF308 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564B3D74239BDBEC007BF308 /* DatabaseSuspensionTests.swift */; }; + 564B3D76239BDBED007BF308 /* DatabaseSuspensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564B3D74239BDBEC007BF308 /* DatabaseSuspensionTests.swift */; }; 5656A805229474F4001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5656A804229474F4001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift */; }; 5656A806229474F4001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5656A804229474F4001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift */; }; 5676FBAD22F5CEF6004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5676FBAC22F5CEF5004717D9 /* ValueObservationRegionRecordingTests.swift */; }; 5676FBAE22F5CEF6004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5676FBAC22F5CEF5004717D9 /* ValueObservationRegionRecordingTests.swift */; }; - 5682D728239582E3004B58C4 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D727239582E2004B58C4 /* DatabaseLockPreventionTests.swift */; }; - 5682D729239582E3004B58C4 /* DatabaseLockPreventionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5682D727239582E2004B58C4 /* DatabaseLockPreventionTests.swift */; }; 56915790231C0D5100E1D237 /* PoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691578F231C0D5100E1D237 /* PoolTests.swift */; }; 56915791231C0D5100E1D237 /* PoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5691578F231C0D5100E1D237 /* PoolTests.swift */; }; 569BBA2E228DF90200478429 /* AssociationPrefetchingFetchableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569BBA2D228DF90200478429 /* AssociationPrefetchingFetchableRecordTests.swift */; }; @@ -611,9 +611,9 @@ 564A1FDD226B89E1001F64F1 /* FTS5RecordTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5RecordTests.swift; sourceTree = ""; }; 564A2156226B8E18001F64F1 /* GRDBTestsEncrypted.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBTestsEncrypted.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 564A2158226C8F24001F64F1 /* db.SQLCipher3 */ = {isa = PBXFileReference; lastKnownFileType = file; path = db.SQLCipher3; sourceTree = SOURCE_ROOT; }; + 564B3D74239BDBEC007BF308 /* DatabaseSuspensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSuspensionTests.swift; sourceTree = ""; }; 5656A804229474F4001FF3FF /* ValueObservationQueryInterfaceRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationQueryInterfaceRequestTests.swift; sourceTree = ""; }; 5676FBAC22F5CEF5004717D9 /* ValueObservationRegionRecordingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueObservationRegionRecordingTests.swift; sourceTree = ""; }; - 5682D727239582E2004B58C4 /* DatabaseLockPreventionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseLockPreventionTests.swift; sourceTree = ""; }; 5691578F231C0D5100E1D237 /* PoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PoolTests.swift; sourceTree = ""; }; 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 = ""; }; @@ -718,7 +718,6 @@ 564A1F54226B89D2001F64F1 /* DatabaseDateEncodingStrategyTests.swift */, 564A1F65226B89D5001F64F1 /* DatabaseErrorTests.swift */, 564A1F37226B89CF001F64F1 /* DatabaseFunctionTests.swift */, - 5682D727239582E2004B58C4 /* DatabaseLockPreventionTests.swift */, 564A1F5C226B89D3001F64F1 /* DatabaseLogErrorTests.swift */, 564A1FD4226B89E0001F64F1 /* DatabaseMigratorTests.swift */, 564A1F6A226B89D5001F64F1 /* DatabasePoolBackupTests.swift */, @@ -740,6 +739,7 @@ 564A1F69226B89D5001F64F1 /* DatabaseRegionTests.swift */, 564A1F79226B89D7001F64F1 /* DatabaseSavepointTests.swift */, 564A1F3F226B89D0001F64F1 /* DatabaseSnapshotTests.swift */, + 564B3D74239BDBEC007BF308 /* DatabaseSuspensionTests.swift */, 564A1FC7226B89DF001F64F1 /* DatabaseTests.swift */, 564A1FAF226B89DC001F64F1 /* DatabaseTimestampTests.swift */, 564A1FD6226B89E0001F64F1 /* DatabaseUUIDEncodingStrategyTests.swift */, @@ -1143,6 +1143,7 @@ 564A2091226B89E1001F64F1 /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */, 5676FBAD22F5CEF6004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */, 564A208F226B89E1001F64F1 /* AssociationParallelRowScopesTests.swift in Sources */, + 564B3D75239BDBED007BF308 /* DatabaseSuspensionTests.swift in Sources */, 564A2047226B89E1001F64F1 /* AnyCursorTests.swift in Sources */, 564A1FFC226B89E1001F64F1 /* DatabaseValueTests.swift in Sources */, 564A200E226B89E1001F64F1 /* RowFromStatementTests.swift in Sources */, @@ -1224,7 +1225,6 @@ 564A208E226B89E1001F64F1 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */, 564A2029226B89E1001F64F1 /* FTS3TableBuilderTests.swift in Sources */, 561CFAAA2376EFAD000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */, - 5682D728239582E3004B58C4 /* DatabaseLockPreventionTests.swift in Sources */, 561CFAA82376EFAD000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */, 564A2001226B89E1001F64F1 /* RecordPrimaryKeyNoneTests.swift in Sources */, 564A2051226B89E1001F64F1 /* RecordWithColumnNameManglingTests.swift in Sources */, @@ -1348,6 +1348,7 @@ 564A20D0226B8E18001F64F1 /* FetchableRecord+QueryInterfaceRequestTests.swift in Sources */, 5676FBAE22F5CEF6004717D9 /* ValueObservationRegionRecordingTests.swift in Sources */, 564A20D1226B8E18001F64F1 /* AssociationParallelRowScopesTests.swift in Sources */, + 564B3D76239BDBED007BF308 /* DatabaseSuspensionTests.swift in Sources */, 564A20D2226B8E18001F64F1 /* AnyCursorTests.swift in Sources */, 564A20D3226B8E18001F64F1 /* DatabaseValueTests.swift in Sources */, 564A20D4226B8E18001F64F1 /* RowFromStatementTests.swift in Sources */, @@ -1429,7 +1430,6 @@ 564A211D226B8E18001F64F1 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */, 564A211E226B8E18001F64F1 /* FTS3TableBuilderTests.swift in Sources */, 561CFAAB2376EFAD000C8BAA /* AssociationHasManyOrderingTests.swift in Sources */, - 5682D729239582E3004B58C4 /* DatabaseLockPreventionTests.swift in Sources */, 561CFAA92376EFAD000C8BAA /* AssociationHasManyThroughOrderingTests.swift in Sources */, 564A211F226B8E18001F64F1 /* RecordPrimaryKeyNoneTests.swift in Sources */, 564A2120226B8E18001F64F1 /* RecordWithColumnNameManglingTests.swift in Sources */, diff --git a/Tests/GRDBTests/DatabaseConfigurationTests.swift b/Tests/GRDBTests/DatabaseConfigurationTests.swift index 3441ac76c6..38fcd0c4d3 100644 --- a/Tests/GRDBTests/DatabaseConfigurationTests.swift +++ b/Tests/GRDBTests/DatabaseConfigurationTests.swift @@ -11,6 +11,8 @@ import XCTest #endif class DatabaseConfigurationTests: GRDBTestCase { + // MARK: - prepareDatabase + func testPrepareDatabase() throws { // prepareDatabase is called when connection opens var connectionCount = 0 @@ -78,6 +80,8 @@ class DatabaseConfigurationTests: GRDBTestCase { } } + // MARK: - acceptsDoubleQuotedStringLiterals + func testAcceptsDoubleQuotedStringLiteralsDefault() throws { let configuration = Configuration() XCTAssertFalse(configuration.acceptsDoubleQuotedStringLiterals) @@ -152,4 +156,152 @@ class DatabaseConfigurationTests: GRDBTestCase { XCTAssertEqual(error.sql, "CREATE INDEX i ON player(\"foo\")") } } + + // MARK: - busyMode + + func testBusyModeImmediate() throws { + let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite") + #if GRDBCIPHER_USE_ENCRYPTION + // Work around SQLCipher bug when two connections are open to the + // same empty database: make sure the database is not empty before + // running this test + try dbQueue1.inDatabase { db in + try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)") + } + #endif + + var configuration2 = dbQueue1.configuration + configuration2.busyMode = .immediateError + let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite", configuration: configuration2) + + let s1 = DispatchSemaphore(value: 0) + let s2 = DispatchSemaphore(value: 0) + let queue = DispatchQueue.global(priority: .default) + let group = DispatchGroup() + + queue.async(group: group) { + do { + try dbQueue1.inTransaction(.exclusive) { db in + s2.signal() + queue.asyncAfter(deadline: .now() + 1) { + s1.signal() + } + _ = s1.wait(timeout: .distantFuture) + return .commit + } + } catch { + XCTFail("\(error)") + } + } + + queue.async(group: group) { + do { + _ = s2.wait(timeout: .distantFuture) + try dbQueue2.inTransaction(.exclusive) { db in return .commit } + XCTFail("Expected error") + } catch let error as DatabaseError where error.resultCode == .SQLITE_BUSY { + } catch { + XCTFail("\(error)") + } + } + + _ = group.wait(timeout: .distantFuture) + } + + func testBusyModeTimeoutTooShort() throws { + let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite") + #if GRDBCIPHER_USE_ENCRYPTION + // Work around SQLCipher bug when two connections are open to the + // same empty database: make sure the database is not empty before + // running this test + try dbQueue1.inDatabase { db in + try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)") + } + #endif + + var configuration2 = dbQueue1.configuration + configuration2.busyMode = .timeout(0.1) + let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite", configuration: configuration2) + + let s1 = DispatchSemaphore(value: 0) + let s2 = DispatchSemaphore(value: 0) + let queue = DispatchQueue.global(priority: .default) + let group = DispatchGroup() + + queue.async(group: group) { + do { + try dbQueue1.inTransaction(.exclusive) { db in + s2.signal() + queue.asyncAfter(deadline: .now() + 1) { + s1.signal() + } + _ = s1.wait(timeout: .distantFuture) + return .commit + } + } catch { + XCTFail("\(error)") + } + } + + queue.async(group: group) { + do { + _ = s2.wait(timeout: .distantFuture) + try dbQueue2.inTransaction(.exclusive) { db in return .commit } + XCTFail("Expected error") + } catch let error as DatabaseError where error.resultCode == .SQLITE_BUSY { + } catch { + XCTFail("\(error)") + } + } + + _ = group.wait(timeout: .distantFuture) + } + + // TODO: fix flaky test. It fails on Xcode 10.0, tvOS 10.0 +// func testBusyModeTimeoutTooLong() throws { +// let dbQueue1 = try makeDatabaseQueue(filename: "test.sqlite") +// #if GRDBCIPHER_USE_ENCRYPTION +// // Work around SQLCipher bug when two connections are open to the +// // same empty database: make sure the database is not empty before +// // running this test +// try dbQueue1.inDatabase { db in +// try db.execute(sql: "CREATE TABLE SQLCipherWorkAround (foo INTEGER)") +// } +// #endif +// +// var configuration2 = dbQueue1.configuration +// configuration2.busyMode = .timeout(1) +// let dbQueue2 = try makeDatabaseQueue(filename: "test.sqlite", configuration: configuration2) +// +// let s1 = DispatchSemaphore(value: 0) +// let s2 = DispatchSemaphore(value: 0) +// let queue = DispatchQueue.global(priority: .default) +// let group = DispatchGroup() +// +// queue.async(group: group) { +// do { +// try dbQueue1.inTransaction(.exclusive) { db in +// s2.signal() +// queue.asyncAfter(deadline: .now() + 0.1) { +// s1.signal() +// } +// _ = s1.wait(timeout: .distantFuture) +// return .commit +// } +// } catch { +// XCTFail("\(error)") +// } +// } +// +// queue.async(group: group) { +// do { +// _ = s2.wait(timeout: .distantFuture) +// try dbQueue2.inTransaction(.exclusive) { db in return .commit } +// } catch { +// XCTFail("\(error)") +// } +// } +// +// _ = group.wait(timeout: .distantFuture) +// } } diff --git a/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift b/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift index e02486620f..573bda10db 100644 --- a/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift +++ b/Tests/GRDBTests/DatabasePoolConcurrencyTests.swift @@ -1,5 +1,6 @@ import XCTest import Dispatch +import Foundation #if GRDBCUSTOMSQLITE import GRDBCustomSQLite #else @@ -1276,4 +1277,101 @@ class DatabasePoolConcurrencyTests: GRDBTestCase { s4.wait() } } + + // MARK: - Concurrent opening + + func testConcurrentOpening() throws { + for _ in 0..<50 { + let dbDirectoryName = "DatabasePoolConcurrencyTests-\(ProcessInfo.processInfo.globallyUniqueString)" + let directoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + .appendingPathComponent(dbDirectoryName, isDirectory: true) + let dbURL = directoryURL.appendingPathComponent("db.sqlite") + try FileManager.default.createDirectory(atPath: directoryURL.path, withIntermediateDirectories: true, attributes: nil) + defer { try! FileManager.default.removeItem(at: directoryURL) } + DispatchQueue.concurrentPerform(iterations: 10) { n in + // WTF I could never Google for the proper correct error handling + // of NSFileCoordinator. What a weird API. + let coordinator = NSFileCoordinator(filePresenter: nil) + var coordinatorError: NSError? + var poolError: Error? + coordinator.coordinate(writingItemAt: dbURL, options: .forMerging, error: &coordinatorError, byAccessor: { url in + do { + _ = try DatabasePool(path: url.path) + } catch { + poolError = error + } + }) + XCTAssert(poolError ?? coordinatorError == nil) + } + } + } + + // MARK: - NSFileCoordinator sample code tests + + // Test for sample code in Documentation/AppGroupContainers.md. + // This test passes if this method compiles + private func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool { + let coordinator = NSFileCoordinator(filePresenter: nil) + var coordinatorError: NSError? + var dbPool: DatabasePool? + var dbError: Error? + coordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError, byAccessor: { url in + do { + dbPool = try openDatabase(at: url) + } catch { + dbError = error + } + }) + if let error = dbError ?? coordinatorError { + throw error + } + return dbPool! + } + + // Test for sample code in Documentation/AppGroupContainers.md. + // This test passes if this method compiles + private func openDatabase(at databaseURL: URL) throws -> DatabasePool { + let dbPool = try DatabasePool(path: databaseURL.path) + // Perform here other database setups, such as defining + // the database schema with a DatabaseMigrator. + return dbPool + } + + // Test for sample code in Documentation/AppGroupContainers.md. + // This test passes if this method compiles + private func openSharedReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? { + let coordinator = NSFileCoordinator(filePresenter: nil) + var coordinatorError: NSError? + var dbPool: DatabasePool? + var dbError: Error? + coordinator.coordinate(readingItemAt: databaseURL, options: .withoutChanges, error: &coordinatorError, byAccessor: { url in + do { + dbPool = try openReadOnlyDatabase(at: url) + } catch { + dbError = error + } + }) + if let error = dbError ?? coordinatorError { + throw error + } + return dbPool + } + + // Test for sample code in Documentation/AppGroupContainers.md. + // This test passes if this method compiles + private func openReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? { + do { + var configuration = Configuration() + configuration.readonly = true + return try DatabasePool(path: databaseURL.path, configuration: configuration) + } catch { + if FileManager.default.fileExists(atPath: databaseURL.path) { + // Something went wrong + throw error + } else { + // Database file does not exist + return nil + } + } + } } diff --git a/Tests/GRDBTests/DatabasePoolReadOnlyTests.swift b/Tests/GRDBTests/DatabasePoolReadOnlyTests.swift index 64befb137e..7474d46ad9 100644 --- a/Tests/GRDBTests/DatabasePoolReadOnlyTests.swift +++ b/Tests/GRDBTests/DatabasePoolReadOnlyTests.swift @@ -7,6 +7,15 @@ import XCTest class DatabasePoolReadOnlyTests: GRDBTestCase { + func testOpenReadOnlyMissingDatabase() throws { + dbConfiguration.readonly = true + do { + _ = try makeDatabasePool() + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_CANTOPEN) + } + } + func testConcurrentRead() throws { let databaseFileName = "db.sqlite" diff --git a/Tests/GRDBTests/DatabaseQueueReadOnlyTests.swift b/Tests/GRDBTests/DatabaseQueueReadOnlyTests.swift index 4d72923f9b..c394b2e00f 100644 --- a/Tests/GRDBTests/DatabaseQueueReadOnlyTests.swift +++ b/Tests/GRDBTests/DatabaseQueueReadOnlyTests.swift @@ -7,6 +7,15 @@ import XCTest class DatabaseQueueReadOnlyTests : GRDBTestCase { + func testOpenReadOnlyMissingDatabase() throws { + dbConfiguration.readonly = true + do { + _ = try makeDatabaseQueue() + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_CANTOPEN) + } + } + func testReadOnlyDatabaseCanNotBeModified() throws { // Create database do { diff --git a/Tests/GRDBTests/DatabaseLockPreventionTests.swift b/Tests/GRDBTests/DatabaseSuspensionTests.swift similarity index 74% rename from Tests/GRDBTests/DatabaseLockPreventionTests.swift rename to Tests/GRDBTests/DatabaseSuspensionTests.swift index 38634696aa..36f145b5de 100644 --- a/Tests/GRDBTests/DatabaseLockPreventionTests.swift +++ b/Tests/GRDBTests/DatabaseSuspensionTests.swift @@ -1,11 +1,11 @@ import XCTest #if GRDBCUSTOMSQLITE -import GRDBCustomSQLite +@testable import GRDBCustomSQLite #else -import GRDB +@testable import GRDB #endif -class DatabaseLockPreventionTests : GRDBTestCase { +class DatabaseSuspensionTests : GRDBTestCase { private func makeDatabaseQueue(journalMode: String) throws -> DatabaseQueue { let dbQueue = try makeDatabaseQueue() @@ -16,9 +16,9 @@ class DatabaseLockPreventionTests : GRDBTestCase { // MARK: - BEGIN TRANSACTION - func testLockPreventionPreventsNewTransactionInDeleteJournalMode() throws { + func testSuspensionPreventsNewTransactionInDeleteJournalMode() throws { let dbQueue = try makeDatabaseQueue(journalMode: "delete") - dbQueue.startPreventingLock() + dbQueue.suspend() do { try dbQueue.inDatabase { db in @@ -27,7 +27,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "BEGIN TRANSACTION") } @@ -38,7 +38,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "BEGIN IMMEDIATE TRANSACTION") } @@ -49,7 +49,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "BEGIN EXCLUSIVE TRANSACTION") } @@ -60,14 +60,14 @@ class DatabaseLockPreventionTests : GRDBTestCase { XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "SAVEPOINT test") } } - func testLockPreventionDoesNotPreventNewDeferredTransactionInWALMode() throws { + func testSuspensionDoesNotPreventNewDeferredTransactionInWALMode() throws { let dbQueue = try makeDatabaseQueue(journalMode: "wal") - dbQueue.startPreventingLock() + dbQueue.suspend() try dbQueue.inDatabase { db in try db.execute(sql: "BEGIN TRANSACTION; ROLLBACK") @@ -75,9 +75,9 @@ class DatabaseLockPreventionTests : GRDBTestCase { } } - func testLockPreventionPreventsNewImmediateOrExclusiveTransactionInWALMode() throws { + func testSuspensionPreventsNewImmediateOrExclusiveTransactionInWALMode() throws { let dbQueue = try makeDatabaseQueue(journalMode: "wal") - dbQueue.startPreventingLock() + dbQueue.suspend() do { try dbQueue.inDatabase { db in @@ -86,7 +86,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "BEGIN IMMEDIATE TRANSACTION") } @@ -97,18 +97,18 @@ class DatabaseLockPreventionTests : GRDBTestCase { XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "BEGIN EXCLUSIVE TRANSACTION") } } // MARK: - COMMIT, ROLLBACK, RELEASE SAVEPOINT, ROLLBACK TRANSACTION TO SAVEPOINT - func testLockPreventionDoesNotPreventCommit() throws { + func testSuspensionDoesNotPreventCommit() throws { func test(_ dbQueue: DatabaseQueue) throws { try dbQueue.inDatabase { db in try db.execute(sql: "BEGIN TRANSACTION") - dbQueue.startPreventingLock() + dbQueue.suspend() try db.execute(sql: "COMMIT") } } @@ -116,11 +116,11 @@ class DatabaseLockPreventionTests : GRDBTestCase { try test(makeDatabaseQueue(journalMode: "wal")) } - func testLockPreventionDoesNotPreventRollback() throws { + func testSuspensionDoesNotPreventRollback() throws { func test(_ dbQueue: DatabaseQueue) throws { try dbQueue.inDatabase { db in try db.execute(sql: "BEGIN TRANSACTION") - dbQueue.startPreventingLock() + dbQueue.suspend() try db.execute(sql: "ROLLBACK") } } @@ -128,11 +128,11 @@ class DatabaseLockPreventionTests : GRDBTestCase { try test(makeDatabaseQueue(journalMode: "wal")) } - func testLockPreventionDoesNotPreventReleaseSavePoint() throws { + func testSuspensionDoesNotPreventReleaseSavePoint() throws { func test(_ dbQueue: DatabaseQueue) throws { try dbQueue.inDatabase { db in try db.execute(sql: "SAVEPOINT test") - dbQueue.startPreventingLock() + dbQueue.suspend() try db.execute(sql: "RELEASE SAVEPOINT test") } } @@ -140,11 +140,11 @@ class DatabaseLockPreventionTests : GRDBTestCase { try test(makeDatabaseQueue(journalMode: "wal")) } - func testLockPreventionDoesNotPreventRollbackSavePoint() throws { + func testSuspensionDoesNotPreventRollbackSavePoint() throws { func test(_ dbQueue: DatabaseQueue) throws { try dbQueue.inDatabase { db in try db.execute(sql: "SAVEPOINT test") - dbQueue.startPreventingLock() + dbQueue.suspend() try db.execute(sql: "ROLLBACK TRANSACTION TO SAVEPOINT test") try db.execute(sql: "RELEASE SAVEPOINT test") } @@ -155,27 +155,27 @@ class DatabaseLockPreventionTests : GRDBTestCase { // MARK: - SELECT - func testLockPreventionPreventsReadInDeleteJournalMode() throws { + func testSuspensionPreventsReadInDeleteJournalMode() throws { let dbQueue = try makeDatabaseQueue(journalMode: "delete") do { try dbQueue.inDatabase { db in - try db.execute(sql: "CREATE TABLE t(a);") - dbQueue.startPreventingLock() + try db.execute(sql: "CREATE TABLE t(a)") + dbQueue.suspend() _ = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t") } XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "SELECT COUNT(*) FROM t") } } - - func testLockPreventionDoesNotPreventReadInWALMode() throws { + + func testSuspensionDoesNotPreventReadInWALMode() throws { let dbQueue = try makeDatabaseQueue(journalMode: "wal") try dbQueue.inDatabase { db in - try db.execute(sql: "CREATE TABLE t(a);") - dbQueue.startPreventingLock() + try db.execute(sql: "CREATE TABLE t(a)") + dbQueue.suspend() try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t"), 0) @@ -188,34 +188,34 @@ class DatabaseLockPreventionTests : GRDBTestCase { // MARK: - INSERT - func testLockPreventionPreventsWriteInDeleteJournalMode() throws { + func testSuspensionPreventsWriteInDeleteJournalMode() throws { let dbQueue = try makeDatabaseQueue(journalMode: "delete") try dbQueue.inDatabase { db in - try db.execute(sql: "CREATE TABLE t(a);") - dbQueue.startPreventingLock() + try db.execute(sql: "CREATE TABLE t(a)") + dbQueue.suspend() do { try db.execute(sql: "INSERT INTO t DEFAULT VALUES") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "INSERT INTO t DEFAULT VALUES") } } } - func testLockPreventionPreventsWriteInWALMode() throws { + func testSuspensionPreventsWriteInWALMode() throws { let dbQueue = try makeDatabaseQueue(journalMode: "wal") try dbQueue.inDatabase { db in - try db.execute(sql: "CREATE TABLE t(a);") - dbQueue.startPreventingLock() + try db.execute(sql: "CREATE TABLE t(a)") + dbQueue.suspend() do { try db.execute(sql: "INSERT INTO t DEFAULT VALUES") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "INSERT INTO t DEFAULT VALUES") } @@ -227,7 +227,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "INSERT INTO t DEFAULT VALUES") } } @@ -235,19 +235,19 @@ class DatabaseLockPreventionTests : GRDBTestCase { // MARK: - Automatic ROLLBACK - func testLockPreventionRollbacksOnPreventedWrite() throws { + func testSuspensionRollbacksOnPreventedWrite() throws { func test(_ dbQueue: DatabaseQueue) throws { do { try dbQueue.write { db in - try db.execute(sql: "CREATE TABLE t(a);") + try db.execute(sql: "CREATE TABLE t(a)") XCTAssertTrue(db.isInsideTransaction) - dbQueue.startPreventingLock() + dbQueue.suspend() do { try db.execute(sql: "INSERT INTO t DEFAULT VALUES") XCTFail("Expected error") } catch let error as DatabaseError { XCTAssertEqual(error.resultCode, .SQLITE_ABORT) - XCTAssertEqual(error.message, "Aborted due to lock prevention") + XCTAssertEqual(error.message, "Database is suspended") XCTAssertEqual(error.sql, "INSERT INTO t DEFAULT VALUES") } // Aborded transaction @@ -265,7 +265,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { // MARK: - Concurrent Lock Prevention - func testLockPreventionAbortsDatabaseQueueAccess() throws { + func testSuspensionAbortsDatabaseQueueAccess() throws { func test(_ dbQueue: DatabaseQueue) throws { let semaphore1 = DispatchSemaphore(value: 0) let semaphore2 = DispatchSemaphore(value: 0) @@ -289,7 +289,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { } let block2 = { semaphore1.wait() - dbQueue.startPreventingLock() + dbQueue.suspend() semaphore2.signal() } let blocks = [block1, block2] @@ -301,10 +301,10 @@ class DatabaseLockPreventionTests : GRDBTestCase { try test(makeDatabaseQueue(journalMode: "wal")) } - func testLockPreventionDoesNotPreventFurtherReadInWALMode() throws { + func testSuspensionDoesNotPreventFurtherReadInWALMode() throws { let dbQueue = try makeDatabaseQueue(journalMode: "wal") try dbQueue.write { db in - try db.execute(sql: "CREATE TABLE t(a);") + try db.execute(sql: "CREATE TABLE t(a)") } let semaphore1 = DispatchSemaphore(value: 0) @@ -334,7 +334,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { } let block2 = { semaphore1.wait() - dbQueue.startPreventingLock() + dbQueue.suspend() semaphore2.signal() } let blocks = [block1, block2] @@ -346,7 +346,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { func testWriteTransactionAbortedDuringStatementExecution() throws { func test(_ dbQueue: DatabaseQueue) throws { try dbQueue.write { db in - try db.execute(sql: "CREATE TABLE t(a);") + try db.execute(sql: "CREATE TABLE t(a)") } let semaphore1 = DispatchSemaphore(value: 0) @@ -374,7 +374,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { } let block2 = { semaphore1.wait() - dbQueue.startPreventingLock() + dbQueue.suspend() semaphore2.signal() } let blocks = [block1, block2] @@ -389,7 +389,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { func testWriteTransactionAbortedDuringStatementExecutionPreventsFurtherDatabaseAccess() throws { func test(_ dbQueue: DatabaseQueue) throws { try dbQueue.write { db in - try db.execute(sql: "CREATE TABLE t(a);") + try db.execute(sql: "CREATE TABLE t(a)") } let semaphore1 = DispatchSemaphore(value: 0) @@ -433,7 +433,7 @@ class DatabaseLockPreventionTests : GRDBTestCase { } let block2 = { semaphore1.wait() - dbQueue.startPreventingLock() + dbQueue.suspend() semaphore2.signal() } let blocks = [block1, block2] @@ -445,15 +445,72 @@ class DatabaseLockPreventionTests : GRDBTestCase { try test(makeDatabaseQueue(journalMode: "wal")) } - // MARK: - stopPreventingLock + // MARK: - resume - func testStopPreventingLock() throws { + func testResume() throws { let dbQueue = try makeDatabaseQueue(journalMode: "delete") try dbQueue.inDatabase { db in - try db.execute(sql: "CREATE TABLE t(a);") - dbQueue.startPreventingLock() - dbQueue.stopPreventingLock() + try db.execute(sql: "CREATE TABLE t(a)") + dbQueue.suspend() + dbQueue.resume() try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 0) } } + + // MARK: - journalModeCache + + // Test for internals. Make sure the journalModeCache is not set too early, + // especially not before user can choose the journal mode + func testJournalModeCache() throws { + do { + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + XCTAssertNil(db.journalModeCache) + try db.execute(sql: "PRAGMA journal_mode=truncate") + try db.execute(sql: "CREATE TABLE t(a)") + XCTAssertNil(db.journalModeCache) + } + dbQueue.suspend() + dbQueue.inDatabase { db in + XCTAssertNil(db.journalModeCache) + try? db.execute(sql: "SELECT * FROM sqlite_master") + XCTAssertEqual(db.journalModeCache, "truncate") + } + } + do { + var configuration = Configuration() + configuration.prepareDatabase = { db in + try db.execute(sql: "PRAGMA journal_mode=truncate") + } + let dbQueue = try makeDatabaseQueue(configuration: configuration) + dbQueue.suspend() + dbQueue.inDatabase { db in + XCTAssertNil(db.journalModeCache) + try? db.execute(sql: "SELECT * FROM sqlite_master") + XCTAssertEqual(db.journalModeCache, "truncate") + } + } + do { + let dbPool = try makeDatabasePool() + try dbPool.write { db in + XCTAssertNil(db.journalModeCache) + try db.execute(sql: "CREATE TABLE t(a)") + XCTAssertNil(db.journalModeCache) + } + dbPool.suspend() + try dbPool.writeWithoutTransaction { db in + XCTAssertNil(db.journalModeCache) + try db.execute(sql: "SELECT * FROM sqlite_master") + XCTAssertEqual(db.journalModeCache, "wal") + } + try dbPool.write { db in + XCTAssertEqual(db.journalModeCache, "wal") + } + try dbPool.read { db in + XCTAssertNil(db.journalModeCache) + try db.execute(sql: "SELECT * FROM sqlite_master") + XCTAssertNil(db.journalModeCache) + } + } + } }