import Foundation #if os(iOS) import UIKit #endif public final class DatabaseQueue { private let writer: SerializedDatabase /// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers. private var suspensionObservers: [NSObjectProtocol] = [] // MARK: - Configuration public var configuration: Configuration { writer.configuration } public var path: String { writer.path } // MARK: - Initializers /// Opens or creates an SQLite database. /// /// For example: /// /// ```swift /// let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite") /// ``` /// /// The SQLite connection is closed when the database queue /// gets deallocated. /// /// - parameters: /// - path: The path to the database file. /// - configuration: A configuration. /// - throws: A ``DatabaseError`` whenever an SQLite error occurs. public init(path: String, configuration: Configuration = Configuration()) throws { // DatabaseQueue can't perform parallel reads var configuration = configuration configuration.maximumReaderCount = 1 writer = try SerializedDatabase( path: path, configuration: configuration, defaultLabel: "GRDB.DatabaseQueue") // Set up journal mode unless readonly if !configuration.readonly { switch configuration.journalMode { case .default: break case .wal: try writer.sync { try $0.setUpWALMode() } } } setupSuspension() // Be a nice iOS citizen, and don't consume too much memory // See https://github.com/groue/GRDB.swift/#memory-management #if os(iOS) if configuration.automaticMemoryManagement { setupMemoryManagement() } #endif } /// Opens an in-memory SQLite database. /// /// To create an independent in-memory database, don't pass any name. The /// database memory is released when the database queue is deallocated: /// /// ```swift /// // An independent in-memory database /// let dbQueue = try DatabaseQueue() /// ``` /// /// When you need to open several connections to the same in-memory /// database, give it a name: /// /// ```swift /// // A shared in-memory database /// let dbQueue = try DatabaseQueue(named: "myDatabase") /// ``` /// /// In this case, the database is automatically deleted and memory is /// reclaimed when the last connection to the database of the given /// name closes. /// /// Related SQLite documentation: /// /// - parameter name: When nil, an independent in-memory database opens. /// Otherwise, the shared in-memory database of the given name opens. /// - parameter configuration: A configuration. public init(named name: String? = nil, configuration: Configuration = Configuration()) throws { let path: String if let name { path = "file:\(name)?mode=memory&cache=shared" } else { path = ":memory:" } writer = try SerializedDatabase( path: path, configuration: configuration, defaultLabel: "GRDB.DatabaseQueue") } deinit { // Remove block-based Notification observers. suspensionObservers.forEach(NotificationCenter.default.removeObserver(_:)) // Undo job done in setupMemoryManagement() // // https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error // Explicit unregistration is required before macOS 10.11. NotificationCenter.default.removeObserver(self) } } // @unchecked because of suspensionObservers extension DatabaseQueue: @unchecked Sendable { } extension DatabaseQueue { // MARK: - Memory management /// Free as much memory as possible. /// /// This method blocks the current thread until all database accesses are completed. public func releaseMemory() { writer.sync { $0.releaseMemory() } } #if os(iOS) /// Listens to UIApplicationDidEnterBackgroundNotification and /// UIApplicationDidReceiveMemoryWarningNotification in order to release /// as much memory as possible. private func setupMemoryManagement() { let center = NotificationCenter.default center.addObserver( self, selector: #selector(DatabaseQueue.applicationDidReceiveMemoryWarning(_:)), name: UIApplication.didReceiveMemoryWarningNotification, object: nil) center.addObserver( self, selector: #selector(DatabaseQueue.applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil) } @objc private func applicationDidEnterBackground(_ notification: NSNotification) { guard let application = notification.object as? UIApplication else { return } let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil) if task == .invalid { // Release memory synchronously releaseMemory() } else { // Release memory asynchronously writer.async { db in db.releaseMemory() application.endBackgroundTask(task) } } } @objc private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) { writer.async { db in db.releaseMemory() } } #endif } extension DatabaseQueue: DatabaseReader { public func close() throws { try writer.sync { try $0.close() } } // MARK: - Interrupting Database Operations public func interrupt() { writer.interrupt() } // MARK: - Database Suspension func suspend() { writer.suspend() } func resume() { writer.resume() } private func setupSuspension() { if configuration.observesSuspensionNotifications { let center = NotificationCenter.default suspensionObservers.append(center.addObserver( forName: Database.suspendNotification, object: nil, queue: nil, using: { [weak self] _ in self?.suspend() } )) suspensionObservers.append(center.addObserver( forName: Database.resumeNotification, object: nil, queue: nil, using: { [weak self] _ in self?.resume() } )) } } // MARK: - Reading from Database @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func read(_ value: (Database) throws -> T) throws -> T { try writer.sync { db in try db.isolated(readOnly: true) { try value(db) } } } public func asyncRead(_ value: @escaping (Result) -> Void) { writer.async { db in defer { // Ignore error because we can not notify it. try? db.commit() try? db.endReadOnly() } do { // Enter read-only mode before starting a transaction, so that the // transaction commit does not trigger database observation. // See . try db.beginReadOnly() try db.beginTransaction(.deferred) value(.success(db)) } catch { value(.failure(error)) } } } public func unsafeRead(_ value: (Database) throws -> T) rethrows -> T { try writer.sync(value) } public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { writer.async { value(.success($0)) } } public func unsafeReentrantRead(_ value: (Database) throws -> T) rethrows -> T { try writer.reentrantSync(value) } public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { // DatabaseQueue can't perform parallel reads. // Perform a blocking read instead. return DatabaseFuture(Result { // Check that we're on the writer queue, as documented try writer.execute { db in try db.isolated(readOnly: true) { try value(db) } } }) } public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { // Check that we're on the writer queue... writer.execute { db in // ... and that no transaction is opened. GRDBPrecondition(!db.isInsideTransaction, "must not be called from inside a transaction.") defer { // Ignore error because we can not notify it. try? db.commit() try? db.endReadOnly() } do { // Enter read-only mode before starting a transaction, so that the // transaction commit does not trigger database observation. // See . try db.beginReadOnly() try db.beginTransaction(.deferred) value(.success(db)) } catch { value(.failure(error)) } } } // MARK: - Database Observation public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, onChange: @escaping (Reducer.Value) -> Void) -> AnyDatabaseCancellable { if configuration.readonly { // The easy case: the database does not change return _addReadOnly( observation: observation, scheduling: scheduler, onChange: onChange) } else { // Observe from the writer database connection. return _addWriteOnly( observation: observation, scheduling: scheduler, onChange: onChange) } } } extension DatabaseQueue: DatabaseWriter { // MARK: - Writing in Database /// Wraps database operations inside a database transaction. /// /// The `updates` function runs in the writer dispatch queue, serialized /// with all database updates. /// /// If `updates` throws an error, the transaction is rollbacked and the /// error is rethrown. If it returns /// ``Database/TransactionCompletion/rollback``, the transaction is also /// rollbacked, but no error is thrown. /// /// For example: /// /// ```swift /// try dbQueue.inTransaction { db in /// try Player(name: "Arthur").insert(db) /// try Player(name: "Barbara").insert(db) /// return .commit /// } /// ``` /// /// - parameters: /// - kind: The transaction type (default nil). If nil, the transaction /// type is the ``Configuration/defaultTransactionKind`` of the /// the ``configuration``. /// - updates: A function that updates the database. /// - throws: The error thrown by `updates`, or by the wrapping transaction. public func inTransaction( _ kind: Database.TransactionKind? = nil, _ updates: (Database) throws -> Database.TransactionCompletion) throws { try writer.sync { db in try db.inTransaction(kind) { try updates(db) } } } @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func writeWithoutTransaction(_ updates: (Database) throws -> T) rethrows -> T { try writer.sync(updates) } @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { try writer.sync(updates) } public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { writer.async { updates(.success($0)) } } /// Executes database operations, and returns their result after they have /// finished executing. /// /// This method is identical to /// ``DatabaseWriter/writeWithoutTransaction(_:)-4qh1w`` /// /// For example: /// /// ```swift /// let newPlayerCount = try dbQueue.inDatabase { db in /// try Player(name: "Arthur").insert(db) /// return try Player.fetchCount(db) /// } /// ``` /// /// Database operations run in the writer dispatch queue, serialized /// with all database updates performed by this `DatabaseWriter`. /// /// The ``Database`` argument to `updates` is valid only during the /// execution of the closure. Do not store or return the database connection /// for later use. /// /// It is a programmer error to call this method from another database /// access method. Doing so raises a "Database methods are not reentrant" /// fatal error at runtime. /// /// - warning: Database operations are not wrapped in a transaction. They /// can see changes performed by concurrent writes or writes performed by /// other processes: two identical requests performed by the `updates` /// closure may not return the same value. Concurrent database accesses /// can see partial updates performed by the `updates` closure. /// /// - parameter updates: A closure which accesses the database. /// - throws: The error thrown by `updates`. public func inDatabase(_ updates: (Database) throws -> T) rethrows -> T { try writer.sync(updates) } public func unsafeReentrantWrite(_ updates: (Database) throws -> T) rethrows -> T { try writer.reentrantSync(updates) } public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { writer.async(updates) } } // MARK: - Temp Copy extension DatabaseQueue { /// Returns a connection to an in-memory copy of the database at `path`. /// /// Changes performed on the returned connection do not impact the /// original database at `path`. /// /// The database memory is released when the returned connection /// is deallocated. /// /// For example: /// /// ```swift /// let path = "/path/to/database.sqlite" /// let dbQueue = try DatabaseQueue.inMemoryCopy(fromPath: path) /// ``` public static func inMemoryCopy( fromPath path: String, configuration: Configuration = Configuration()) throws -> DatabaseQueue { var sourceConfig = configuration sourceConfig.readonly = true let source = try DatabaseQueue(path: path, configuration: sourceConfig) var copyConfig = configuration copyConfig.readonly = false let result = try DatabaseQueue(configuration: copyConfig) try source.backup(to: result) if configuration.readonly { // Result was not opened read-only so that we could perform the // copy. And SQLITE_OPEN_READONLY has no effect on in-memory // databases anyway. // // So let's simulate read-only with PRAGMA query_only. try result.inDatabase { db in try db.beginReadOnly() } } return result } /// Returns a connection to a private, temporary, on-disk copy of the /// database at `path`. /// /// Changes performed on the returned connection do not impact the /// original database at `path`. /// /// The on-disk copy will be automatically deleted from disk as soon as /// the returned connection is closed or deallocated. /// /// For example: /// /// ```swift /// let path = "/path/to/database.sqlite" /// let dbQueue = try DatabaseQueue.temporaryCopy(fromPath: path) /// ``` public static func temporaryCopy( fromPath path: String, configuration: Configuration = Configuration()) throws -> DatabaseQueue { var sourceConfig = configuration sourceConfig.readonly = true let source = try DatabaseQueue(path: path, configuration: sourceConfig) // // > If the filename is an empty string, then a private, temporary // > on-disk database will be created. This private database will be // > automatically deleted as soon as the database connection // > is closed. var copyConfig = configuration copyConfig.readonly = false let result = try DatabaseQueue(path: "", configuration: copyConfig) try source.backup(to: result) if configuration.readonly { // Result was not opened read-only so that we could perform the // copy. So let's simulate read-only with PRAGMA query_only. try result.inDatabase { db in try db.beginReadOnly() } } return result } }