From d0401740af5381e57ec1e998c819d7440314dc38 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Sep 2025 16:16:10 -0700 Subject: [PATCH 1/5] Add `database` for context-sensitive provisioning This small helper will take live/test/preview into account to simplify how folks bootstrap their databases. --- Examples/Reminders/Schema.swift | 22 ++--- Examples/SyncUps/Schema.swift | 17 ++-- .../Articles/PreparingDatabase.md | 80 +++---------------- .../Documentation.docc/SQLiteData.md | 1 + .../DefaultDatabase.swift | 44 ++++++++++ 5 files changed, 69 insertions(+), 95 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index d2aa1c8d..e82bedf7 100644 --- a/Examples/Reminders/Schema.swift +++ b/Examples/Reminders/Schema.swift @@ -128,21 +128,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) - } + 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..3d828dd6 100644 --- a/Examples/SyncUps/Schema.swift +++ b/Examples/SyncUps/Schema.swift @@ -91,16 +91,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) - } + 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..d30d56b4 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md @@ -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(path: path, 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(path: path, 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(path: path, 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..d5ae529a 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 as 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`. /// From 3ac5cf97de24892615027cc279910c45494ffcd1 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Sep 2025 16:23:42 -0700 Subject: [PATCH 2/5] wip --- .../SQLiteData/Documentation.docc/Articles/PreparingDatabase.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md index d30d56b4..c9c681ed 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 From a5a68a50b8e30f2f0d53c6d83a36736cde1c93f7 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Sep 2025 16:25:19 -0700 Subject: [PATCH 3/5] wip --- .../Documentation.docc/Articles/PreparingDatabase.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md b/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md index c9c681ed..f59a286f 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/PreparingDatabase.md @@ -121,7 +121,7 @@ way to do this is to construct the database connection using the } } #endif -+ let database = try defaultDatabase(path: path, configuration: configuration) ++ let database = try defaultDatabase(configuration: configuration) + logger.info("open '\(database.path)'") + return database } @@ -153,7 +153,7 @@ database connection: } } #endif - let database = try defaultDatabase(path: path, configuration: configuration) + let database = try defaultDatabase(configuration: configuration) logger.info("open '\(database.path)'") + var migrator = DatabaseMigrator() + #if DEBUG @@ -228,7 +228,7 @@ func appDatabase() throws -> any DatabaseWriter { } } #endif - let database = try defaultDatabase(path: path, configuration: configuration) + let database = try defaultDatabase(configuration: configuration) logger.info("open '\(database.path)'") var migrator = DatabaseMigrator() #if DEBUG From a55ef246a1f61fbbede92ae57729b106a2244c17 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Sep 2025 16:26:05 -0700 Subject: [PATCH 4/5] Update DefaultDatabase.swift --- Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift b/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift index d5ae529a..9781798d 100644 --- a/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift +++ b/Sources/SQLiteData/StructuredQueries+GRDB/DefaultDatabase.swift @@ -7,7 +7,7 @@ import GRDB /// * 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 as a temporary file. +/// * 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 From 24fa0272e6b7e0ce9711fe3f3cefbde2935c0733 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Sep 2025 17:04:29 -0700 Subject: [PATCH 5/5] wip --- Examples/Reminders/Schema.swift | 3 +-- Examples/SyncUps/Schema.swift | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Examples/Reminders/Schema.swift b/Examples/Reminders/Schema.swift index e82bedf7..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,7 +127,7 @@ func appDatabase() throws -> any DatabaseWriter { } #endif } - database = try SQLiteData.defaultDatabase(configuration: configuration) + let database = try SQLiteData.defaultDatabase(configuration: configuration) logger.debug( """ App database: diff --git a/Examples/SyncUps/Schema.swift b/Examples/SyncUps/Schema.swift index 3d828dd6..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,7 +90,7 @@ func appDatabase() throws -> any DatabaseWriter { } #endif } - database = try SQLiteData.defaultDatabase(configuration: configuration) + let database = try SQLiteData.defaultDatabase(configuration: configuration) logger.debug( """ App database: