diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index d2aa1c8d..039fa024 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -113,7 +113,6 @@ extension DependencyValues { func appDatabase() throws -> any DatabaseWriter { @Dependency(\.context) var context - let database: any DatabaseWriter var configuration = Configuration() configuration.foreignKeysEnabled = true configuration.prepareDatabase { db in @@ -128,21 +127,13 @@ func appDatabase() throws -> any DatabaseWriter { } #endif } - if context == .preview { - database = try DatabaseQueue(configuration: configuration) - } else { - let path = - context == .live - ? URL.documentsDirectory.appending(component: "db.sqlite").path() - : URL.temporaryDirectory.appending(component: "\(UUID().uuidString)-db.sqlite").path() - logger.debug( - """ - App database: - open "\(path)" - """ - ) - database = try DatabasePool(path: path, configuration: configuration) - } + let database = try SQLiteData.defaultDatabase(configuration: configuration) + logger.debug( + """ + App database: + open "\(database.path)" + """ + ) var migrator = DatabaseMigrator() #if DEBUG migrator.eraseDatabaseOnSchemaChange = true diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 83196a56..8deead50 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -77,7 +77,6 @@ extension Int { func appDatabase() throws -> any DatabaseWriter { @Dependency(\.context) var context - let database: any DatabaseWriter var configuration = Configuration() configuration.foreignKeysEnabled = true configuration.prepareDatabase { db in @@ -91,16 +90,13 @@ func appDatabase() throws -> any DatabaseWriter { } #endif } - if context == .preview { - database = try DatabaseQueue(configuration: configuration) - } else { - let path = - context == .live - ? URL.documentsDirectory.appending(component: "db.sqlite").path() - : URL.temporaryDirectory.appending(component: "\(UUID().uuidString)-db.sqlite").path() - logger.info("open \(path)") - database = try DatabasePool(path: path, configuration: configuration) - } + let database = try SQLiteData.defaultDatabase(configuration: configuration) + logger.debug( + """ + App database: + open "\(database.path)" + """ + ) var migrator = DatabaseMigrator() #if DEBUG migrator.eraseDatabaseOnSchemaChange = true diff --git a/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md index ccf7b0cb..f59a286f 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md @@ -55,7 +55,7 @@ specified a cascading action (such as delete). We further recommend that you enable query tracing to log queries that are executed in your application. This can be handy for tracking down long-running queries, or when more queries execute than you expect. We also recommend only doing this in debug builds to avoid leaking sensitive -information when the app is running on a user's device, and we further recommned using OSLog +information when the app is running on a user's device, and we further recommend using OSLog when running your app in the simulator/device and using `Swift.print` in previews: ```diff @@ -101,7 +101,8 @@ matter. ### Step 3: Create database connection Once a `Configuration` value is set up we can construct the actual database connection. The simplest -way to do this is to construct the database connection for a path on the file system like so: +way to do this is to construct the database connection using the +``defaultDatabase(path:configuration:)`` function: ```diff -func appDatabase() -> any DatabaseWriter { @@ -120,55 +121,14 @@ way to do this is to construct the database connection for a path on the file sy } } #endif -+ let path = URL.documentsDirectory.appending(component: "db.sqlite").path() -+ logger.info("open \(path)") -+ let database = try DatabasePool(path: path, configuration: configuration) ++ let database = try defaultDatabase(configuration: configuration) ++ logger.info("open '\(database.path)'") + return database } ``` -However, this can be improved. First, this code will crash if it is executed in Xcode previews -because SQLite is unable to form a connection to a database on disk in a preview context. And -second, in tests we should write this databadse to the temporary directoy on disk with a unique -name so that each test gets a fresh database and so that multiple tests can run in parallel. - -To fix this we can use `@Dependency(\.context)` to determine if we are in a "live" application -context or if we're in a preview or test. - -```diff - func appDatabase() -> any DatabaseWriter { - @Dependency(\.context) var context - var configuration = Configuration() - configuration.foreignKeysEnabled = true - #if DEBUG - configuration.prepareDatabase { db in - db.trace(options: .profile) { - if context == .preview { - print("\($0.expandedDescription)") - } else { - logger.debug("\($0.expandedDescription)") - } - } - } - #endif -- let path = URL.documentsDirectory.appending(component: "db.sqlite").path() -- logger.info("open \(path)") -- let database = try DatabasePool(path: path, configuration: configuration) -+ let database: any DatabaseWriter -+ switch context { -+ case .live: -+ let path = URL.documentsDirectory.appending(component: "db.sqlite").path() -+ logger.info("open \(path)") -+ database = try DatabasePool(path: path, configuration: configuration) -+ case .test: -+ let path = URL.temporaryDirectory.appending(component: "\(UUID().uuidString)-db.sqlite").path() -+ database = try DatabasePool(path: path, configuration: configuration) -+ case .preview: -+ database = try DatabaseQueue(configuration: configuration) -+ } - return database - } -``` +This function provisions a context-dependent database for you, _e.g._ in previews and tests it +will provision unique, temporary databases that won't conflict with your live app's database. ### Step 4: Migrate database @@ -193,18 +153,8 @@ database connection: } } #endif - let database: any DatabaseWriter - switch context { - case .live: - let path = URL.documentsDirectory.appending(component: "db.sqlite").path() - logger.info("open \(path)") - database = try DatabasePool(path: path, configuration: configuration) - case .test: - let path = URL.temporaryDirectory.appending(component: "\(UUID().uuidString)-db.sqlite").path() - database = try DatabasePool(path: path, configuration: configuration) - case .preview: - database = try DatabaseQueue(configuration: configuration) - } + let database = try defaultDatabase(configuration: configuration) + logger.info("open '\(database.path)'") + var migrator = DatabaseMigrator() + #if DEBUG + migrator.eraseDatabaseOnSchemaChange = true @@ -278,18 +228,8 @@ func appDatabase() throws -> any DatabaseWriter { } } #endif - let database: any DatabaseWriter - switch context { - case .live: - let path = URL.documentsDirectory.appending(component: "db.sqlite").path() - logger.info("open \(path)") - database = try DatabasePool(path: path, configuration: configuration) - case .test: - let path = URL.temporaryDirectory.appending(component: "\(UUID().uuidString)-db.sqlite").path() - database = try DatabasePool(path: path, configuration: configuration) - case .preview: - database = try DatabaseQueue(configuration: configuration) - } + let database = try defaultDatabase(configuration: configuration) + logger.info("open '\(database.path)'") var migrator = DatabaseMigrator() #if DEBUG migrator.eraseDatabaseOnSchemaChange = true diff --git a/Sources/SQLiteData/Documentation.docc/SQLiteData.md b/Sources/SQLiteData/Documentation.docc/SQLiteData.md index 53fe4919..850fa13c 100644 --- a/Sources/SQLiteData/Documentation.docc/SQLiteData.md +++ b/Sources/SQLiteData/Documentation.docc/SQLiteData.md @@ -282,6 +282,7 @@ with SQLite to take full advantage of GRDB and SQLiteData. ### Database configuration and access +- ``defaultDatabase(path:configuration:)`` - ``GRDB/Database`` - ``Dependencies/DependencyValues/defaultDatabase`` diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift b/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift index 72808f75..9781798d 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift @@ -1,6 +1,50 @@ import Dependencies +import Foundation import GRDB +/// Prepares a context-sensitive database writer. +/// +/// * In a live app context, a database is provisioned in the app container (unless explicitly +/// overridden with the `path` parameter). +/// * In an Xcode preview context, an in-memory database is provisioned. +/// * In a test context, a database pool is provisioned at a temporary file. +/// +/// - Parameters: +/// - path: A path to the database. If `nil`, a path to a file in the application support +/// directory will be used. +/// - configuration: A database configuration. +/// - Returns: A context-sensitive database writer. +public func defaultDatabase( + path: String? = nil, + configuration: Configuration = Configuration() +) throws -> any DatabaseWriter { + let database: any DatabaseWriter + @Dependency(\.context) var context + switch context { + case .live: + var defaultPath: String { + get throws { + let applicationSupportDirectory = try FileManager.default.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + return applicationSupportDirectory.appendingPathComponent("SQLiteData.db").absoluteString + } + } + database = try DatabasePool(path: path ?? defaultPath, configuration: configuration) + case .preview: + database = try DatabaseQueue(configuration: configuration) + case .test: + database = try DatabasePool( + path: "\(NSTemporaryDirectory())\(UUID().uuidString).db", + configuration: configuration + ) + } + return database +} + extension DependencyValues { /// The default database used by `fetchAll`, `fetchOne`, and `fetch`. ///