import GRDB import os.log /// `AppDatabase` lets the application access the database. /// /// You create an `AppDatabase` with a connection to an SQLite database /// (see ). /// /// Create those connections with a configuration returned from /// `AppDatabase/makeConfiguration(_:)`. /// /// For example: /// /// ```swift /// // Create an in-memory AppDatabase /// let config = AppDatabase.makeConfiguration() /// let dbQueue = try DatabaseQueue(configuration: config) /// let appDatabase = try AppDatabase(dbQueue) /// ``` struct AppDatabase { /// Creates an `AppDatabase`, and makes sure the database schema /// is ready. /// /// - important: Create the `DatabaseWriter` with a configuration /// returned by ``makeConfiguration(_:)``. init(_ dbWriter: any DatabaseWriter) throws { self.dbWriter = dbWriter try migrator.migrate(dbWriter) } /// Provides access to the database. /// /// Application can use a `DatabasePool`, and tests can use a fast /// in-memory `DatabaseQueue`. /// /// See private let dbWriter: any DatabaseWriter } // MARK: - Database Configuration extension AppDatabase { private static let sqlLogger = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "SQL") /// Returns a database configuration suited for `PlayerRepository`. /// /// SQL statements are logged if the `SQL_TRACE` environment variable /// is set. /// /// - parameter base: A base configuration. public static func makeConfiguration(_ base: Configuration = Configuration()) -> Configuration { var config = base // An opportunity to add required custom SQL functions or // collations, if needed: // config.prepareDatabase { db in // db.add(function: ...) // } // Log SQL statements if the `SQL_TRACE` environment variable is set. // See if ProcessInfo.processInfo.environment["SQL_TRACE"] != nil { config.prepareDatabase { db in db.trace { // It's ok to log statements publicly. Sensitive // information (statement arguments) are not logged // unless config.publicStatementArguments is set // (see below). os_log("%{public}@", log: sqlLogger, type: .debug, String(describing: $0)) } } } #if DEBUG // Protect sensitive information by enabling verbose debugging in // DEBUG builds only. // See config.publicStatementArguments = true #endif return config } } // MARK: - Database Migrations extension AppDatabase { /// The DatabaseMigrator that defines the database schema. /// /// See private var migrator: DatabaseMigrator { var migrator = DatabaseMigrator() #if DEBUG // Speed up development by nuking the database when migrations change // See migrator.eraseDatabaseOnSchemaChange = true #endif migrator.registerMigration("createPlayer") { db in // Create a table // See try db.create(table: "player") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text).notNull() t.column("score", .integer).notNull() } } // Migrations for future application versions will be inserted here: // migrator.registerMigration(...) { db in // ... // } return migrator } } // MARK: - Database Access: Writes // The write methods execute invariant-preserving database transactions. extension AppDatabase { /// Saves (inserts or updates) a player. When the method returns, the /// player is present in the database, and its id is not nil. func savePlayer(_ player: inout Player) throws { try dbWriter.write { db in try player.save(db) } } /// Delete the specified players func deletePlayers(ids: [Int64]) throws { try dbWriter.write { db in _ = try Player.deleteAll(db, ids: ids) } } /// Delete all players func deleteAllPlayers() throws { try dbWriter.write { db in _ = try Player.deleteAll(db) } } /// Refresh all players (by performing some random changes, for demo purpose). func refreshPlayers() throws { try dbWriter.write { db in if try Player.all().isEmpty(db) { // When database is empty, insert new random players try createRandomPlayers(db) } else { // Insert a player if Bool.random() { _ = try Player.makeRandom().inserted(db) // insert but ignore inserted id } // Delete a random player if Bool.random() { try Player.order(sql: "RANDOM()").limit(1).deleteAll(db) } // Update some players for var player in try Player.fetchAll(db) where Bool.random() { try player.updateChanges(db) { $0.score = Player.randomScore() } } } } } /// Create random players if the database is empty. func createRandomPlayersIfEmpty() throws { try dbWriter.write { db in if try Player.all().isEmpty(db) { try createRandomPlayers(db) } } } /// Support for `createRandomPlayersIfEmpty()` and `refreshPlayers()`. private func createRandomPlayers(_ db: Database) throws { for _ in 0..<8 { _ = try Player.makeRandom().inserted(db) // insert but ignore inserted id } } } // MARK: - Database Access: Reads // This demo app does not provide any specific reading method, and instead // gives an unrestricted read-only access to the rest of the application. // In your app, you are free to choose another path, and define focused // reading methods. extension AppDatabase { /// Provides a read-only access to the database var reader: DatabaseReader { dbWriter } }