#if canImport(Combine) import Combine #endif import Dispatch /// A type that writes into an SQLite database. /// /// Do not declare new conformances to `DatabaseWriter`. Only the built-in /// conforming types are valid. /// /// A database writer creates one single SQLite connection dedicated to database /// updates. All updates are executed in a serial **writer dispatch queue**. /// /// Read accesses are defined by ``DatabaseReader``, the protocol all database /// writers conform to. /// /// See for more information about the behavior of conforming /// types in a multithreaded application. /// /// ## Topics /// /// ### Writing into the Database /// /// - ``write(_:)-76inz`` /// - ``write(_:)-88g7e`` /// - ``writePublisher(receiveOn:updates:)`` /// - ``writePublisher(receiveOn:updates:thenRead:)`` /// - ``writeWithoutTransaction(_:)-4qh1w`` /// - ``writeWithoutTransaction(_:)-tckw`` /// - ``asyncWrite(_:completion:)`` /// - ``asyncWriteWithoutTransaction(_:)`` /// /// ### Exclusive Access to the Database /// /// - ``barrierWriteWithoutTransaction(_:)-280j1`` /// - ``barrierWriteWithoutTransaction(_:)-7u4xw`` /// - ``asyncBarrierWriteWithoutTransaction(_:)`` /// /// ### Reading from the Latest Committed Database State /// /// - ``concurrentRead(_:)`` /// - ``spawnConcurrentRead(_:)`` /// - ``DatabaseFuture`` /// /// ### Unsafe Methods /// /// - ``unsafeReentrantWrite(_:)`` /// /// ### Observing Database Transactions /// /// - ``add(transactionObserver:extent:)`` /// - ``remove(transactionObserver:)`` /// /// ### Other Database Operations /// /// - ``erase()-w5n7`` /// - ``erase()-7jv3d`` /// - ``vacuum()-310uw`` /// - ``vacuum()-9inj0`` /// - ``vacuum(into:)-5lo41`` /// - ``vacuum(into:)-9c5mb`` /// /// ### Supporting Types /// /// - ``AnyDatabaseWriter`` public protocol DatabaseWriter: DatabaseReader { // MARK: - Writing in Database /// Executes database operations, and returns their result after they have /// finished executing. /// /// For example: /// /// ```swift /// let newPlayerCount = try writer.writeWithoutTransaction { 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. See /// for more information. /// /// - parameter updates: A closure which accesses the database. /// - throws: The error thrown by `updates`. @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func writeWithoutTransaction(_ updates: (Database) throws -> T) rethrows -> T /// Executes database operations, and returns their result after they have /// finished executing. /// /// This method waits until all currently executing database accesses /// performed by the database writer finish executing (reads and writes). /// At that point, database operations are executed. Once they finish, the /// database writer can proceed with other database accesses. /// /// For example: /// /// ```swift /// let newPlayerCount = try writer.barrierWriteWithoutTransaction { 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. See /// for more information. /// /// - parameter updates: A closure which accesses the database. /// - throws: The error thrown by `updates`. @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T /// Schedules database operations for execution, and returns immediately. /// /// Database operations are not executed until all currently executing /// database accesses performed by the database writer finish executing /// (reads and writes). At that point, database operations are executed. /// Once they finish, the database writer can proceed with other /// database accesses. /// /// For example: /// /// ```swift /// writer.asyncBarrierWriteWithoutTransaction { dbResult in /// do { /// let db = try dbResult.get() /// try Player(name: "Arthur").insert(db) /// let newPlayerCount = try Player.fetchCount(db) /// } catch { /// // Handle error /// } /// } /// ``` /// /// 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. /// /// - 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. See /// for more information. /// /// - parameter updates: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the barrier access to the database. func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) /// Schedules database operations for execution, and returns immediately. /// /// For example: /// /// ```swift /// writer.asyncWriteWithoutTransaction { db in /// do { /// try Player(name: "Arthur").insert(db) /// let newPlayerCount = try Player.fetchCount(db) /// } catch { /// // Handle error /// } /// } /// ``` /// /// 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. /// /// - 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. See /// for more information. /// /// - parameter updates: A closure which accesses the database. func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) /// Executes database operations, and returns their result after they have /// finished executing. /// /// This method can be called from other database access methods. Reentrant /// database accesses are discouraged because they muddle transaction /// boundaries. See for /// more information. /// /// For example: /// /// ```swift /// let newPlayerCount = try writer.unsafeReentrantWrite { 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. /// /// - 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`. func unsafeReentrantWrite(_ updates: (Database) throws -> T) rethrows -> T // MARK: - Reading from Database /// Schedules read-only database operations for execution, and returns a /// future value. /// /// This method must be called from the writer dispatch queue, outside of /// any transaction. You'll get a fatal error otherwise. /// /// Database operations performed by the `value` closure are isolated in a /// transaction: they do not see changes performed by eventual concurrent /// writes (even writes performed by other processes). /// /// They see the database in the state left by the last updates performed /// by the database writer. /// /// To access the fetched results, you call the ``DatabaseFuture/wait()`` /// method of the returned future, on any dispatch queue. /// /// In the example below, the number of players is fetched concurrently with /// the player insertion. Yet the future is guaranteed to return zero: /// /// ```swift /// try writer.writeWithoutTransaction { db in /// // Delete all players /// try Player.deleteAll() /// /// // Count players concurrently /// let future = writer.concurrentRead { db in /// return try Player.fetchCount() /// } /// /// // Insert a player /// try Player(...).insert(db) /// /// // Guaranteed to be zero /// let count = try future.wait() /// } /// ``` /// /// - note: Usage of this method is discouraged, because waiting on the /// returned ``DatabaseFuture`` blocks a thread. You may prefer /// ``spawnConcurrentRead(_:)`` instead. /// - parameter value: A closure which accesses the database. func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture // Exposed for RxGRDB and GRBCombine. Naming is not stabilized. /// Schedules read-only database operations for execution. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// This method must be called from the writer dispatch queue, outside of /// any transaction. You'll get a fatal error otherwise. /// /// Database operations performed by the `value` closure are isolated in a /// transaction: they do not see changes performed by eventual concurrent /// writes (even writes performed by other processes). /// /// They see the database in the state left by the last updates performed /// by the database writer. /// /// In the example below, the number of players is fetched concurrently with /// the player insertion. Yet it is guaranteed to return zero: /// /// ```swift /// try writer.writeWithoutTransaction { db in /// // Delete all players /// try Player.deleteAll() /// /// // Count players concurrently /// writer.spawnConcurrentRead { db in /// do { /// let db = try dbResult.get() /// // Guaranteed to be zero /// let count = try Player.fetchCount(db) /// } catch { /// // Handle error /// } /// } /// /// // Insert a player /// try Player(...).insert(db) /// } /// ``` /// /// - important: The database operations are executed immediately, /// or asynchronously, depending on the actual class /// of `DatabaseWriter`. /// /// - parameter value: A closure which accesses the database. Its argument /// is a `Result` that provides the database connection, or the failure /// that would prevent establishing the read access to the database. func spawnConcurrentRead(_ value: @escaping (Result) -> Void) } extension DatabaseWriter { /// Executes database operations, and returns their result after they have /// finished executing. /// /// For example: /// /// ```swift /// let newPlayerCount = try writer.write { db in /// try Player(name: "Arthur").insert(db) /// return try Player.fetchCount(db) /// } /// ``` /// /// Database operations are wrapped in a transaction. If they throw an /// error, the transaction is rollbacked and the error is rethrown. /// /// Concurrent database accesses can not see partial database updates (even /// when performed by other processes). /// /// 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. /// /// - parameter updates: A closure which accesses the database. /// - throws: The error thrown by `updates`, or any ``DatabaseError`` that /// would happen while establishing the database access or committing /// the transaction. @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func write(_ updates: (Database) throws -> T) throws -> T { try writeWithoutTransaction { db in var result: T? try db.inTransaction { result = try updates(db) return .commit } return result! } } /// Schedules database operations for execution, and returns immediately. /// /// For example: /// /// ```swift /// writer.asyncWrite { db -> Int in /// try Player(name: "Arthur").insert(db) /// return try Player.fetchCount(db) /// } completion: { db, result in /// switch result { /// case let .success(newPlayerCount): /// // Handle success /// case let .failure(error): /// // Handle error /// } /// ``` /// /// Database operations run by the `updates` closure are wrapped in /// a transaction. If they throw an error, the transaction is rollbacked. /// /// The `completion` closure has two arguments: a database connection, and /// the result of the transaction. This result is a failure if the /// transaction could not be committed or if `updates` has thrown an error. /// /// Concurrent database accesses can not see partial database updates /// performed by `updates` (even when performed by other processes). /// /// Database operations run in the writer dispatch queue, serialized /// with all database updates performed by this `DatabaseWriter`. /// /// The ``Database`` argument to `updates` and `completion` is valid only /// during the execution of those closures. Do not store or return the /// database connection for later use. /// /// - parameter updates: A closure which accesses the database. /// - parameter completion: A closure called with the transaction result. public func asyncWrite( _ updates: @escaping (Database) throws -> T, completion: @escaping (Database, Result) -> Void) { asyncWriteWithoutTransaction { db in do { var result: T? try db.inTransaction { result = try updates(db) return .commit } completion(db, .success(result!)) } catch { completion(db, .failure(error)) } } } // MARK: - Transaction Observers /// Adds a transaction observer to the writer connection, so that it /// gets notified of database changes and transactions. /// /// This method waits until all currently executing database accesses /// performed by the writer dispatch queue finish executing. /// At that point, database observation begins. /// /// It has no effect on read-only database connections. /// /// For example: /// /// ```swift /// let myObserver = MyObserver() /// try dbQueue.add(transactionObserver: myObserver) /// ``` /// /// - parameter transactionObserver: A transaction observer. /// - parameter extent: The duration of the observation. The default is /// the observer lifetime (observation lasts until observer /// is deallocated). public func add( transactionObserver: some TransactionObserver, extent: Database.TransactionObservationExtent = .observerLifetime) { writeWithoutTransaction { $0.add(transactionObserver: transactionObserver, extent: extent) } } /// Removes a transaction observer from the writer connection. /// /// This method waits until all currently executing database accesses /// performed by the writer dispatch queue finish executing. /// At that point, database observation stops. /// /// For example: /// /// ```swift /// let myObserver = MyObserver() /// try dbQueue.remove(transactionObserver: myObserver) /// ``` public func remove(transactionObserver: some TransactionObserver) { writeWithoutTransaction { $0.remove(transactionObserver: transactionObserver) } } // MARK: - Erasing the content of the database /// Erase the database: delete all content, drop all tables, etc. @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func erase() throws { try barrierWriteWithoutTransaction { try $0.erase() } } // MARK: - Claiming Disk Space /// Rebuilds the database file, repacking it into a minimal amount of /// disk space. /// /// Related SQLite documentation: @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func vacuum() throws { try writeWithoutTransaction { try $0.execute(sql: "VACUUM") } } // VACUUM INTO was introduced in SQLite 3.27.0: // https://www.sqlite.org/releaselog/3_27_0.html // // Old versions of SQLCipher won't have it, but I don't know how to perform // availability checks that depend on the version of the SQLCipher CocoaPod // chosen by the application. So let's just have the method fail at runtime. // // This method is declared on DatabaseWriter instead of DatabaseReader, // so that it is not available on DatabaseSnaphot. VACUUM INTO is not // available inside the transaction that is kept open by DatabaseSnaphot. #if GRDBCUSTOMSQLITE || GRDBCIPHER /// Creates a new database file at the specified path with a minimum /// amount of disk space. /// /// Databases encrypted with SQLCipher are copied with the same password /// and configuration as the original database. /// /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func vacuum(into filePath: String) throws { try writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) } } #else /// Creates a new database file at the specified path with a minimum /// amount of disk space. /// /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails @available(iOS 14, macOS 10.16, tvOS 14, watchOS 7, *) public func vacuum(into filePath: String) throws { try writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) } } #endif // MARK: - Database Observation /// Starts an observation that fetches fresh database values synchronously, /// from the writer database connection, right after the database /// was modified. func _addWriteOnly( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, onChange: @escaping (Reducer.Value) -> Void) -> AnyDatabaseCancellable { assert(!configuration.readonly, "Use _addReadOnly(observation:) instead") let observer = ValueWriteOnlyObserver( writer: self, scheduler: scheduler, readOnly: !observation.requiresWriteAccess, trackingMode: observation.trackingMode, reducer: observation.makeReducer(), events: observation.events, onChange: onChange) return observer.start() } } extension DatabaseWriter { // MARK: - Asynchronous Database Access /// Executes database operations, and returns their result after they have /// finished executing. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// For example: /// /// ```swift /// let newPlayerCount = try await writer.write { db in /// try Player(name: "Arthur").insert(db) /// return try Player.fetchCount(db) /// } /// ``` /// /// Database operations are wrapped in a transaction. If they throw an /// error, the transaction is rollbacked and the error is rethrown. /// /// Concurrent database accesses can not see partial database updates (even /// when performed by other processes). /// /// 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. /// /// - parameter updates: A closure which accesses the database. /// - throws: The error thrown by `updates`, or any ``DatabaseError`` that /// would happen while establishing the database access or committing /// the transaction. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func write(_ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in asyncWrite(updates, completion: { _, result in continuation.resume(with: result) }) } } /// Executes database operations, and returns their result after they have /// finished executing. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// For example: /// /// ```swift /// let newPlayerCount = try await writer.writeWithoutTransaction { 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. /// /// - 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`. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func writeWithoutTransaction(_ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in asyncWriteWithoutTransaction { db in do { try continuation.resume(returning: updates(db)) } catch { continuation.resume(throwing: error) } } } } /// Executes database operations, and returns their result after they have /// finished executing. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// Database operations are not executed until all currently executing /// database accesses performed by the database writer finish executing /// (both reads and writes). At that point, database operations are /// executed. Once they finish, the database writer can proceed with other /// database accesses. /// /// For example: /// /// ```swift /// let newPlayerCount = try await writer.barrierWriteWithoutTransaction { 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`. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func barrierWriteWithoutTransaction( _ updates: @Sendable @escaping (Database) throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in asyncBarrierWriteWithoutTransaction { dbResult in continuation.resume(with: dbResult.flatMap { db in Result { try updates(db) } }) } } } /// Erase the database: delete all content, drop all tables, etc. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func erase() async throws { try await writeWithoutTransaction { try $0.erase() } } /// Rebuilds the database file, repacking it into a minimal amount of /// disk space. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// Related SQLite documentation: @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func vacuum() async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM") } } #if GRDBCUSTOMSQLITE || GRDBCIPHER /// Creates a new database file at the specified path with a minimum /// amount of disk space. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// Databases encrypted with SQLCipher are copied with the same password /// and configuration as the original database. /// /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) } } #else /// Creates a new database file at the specified path with a minimum /// amount of disk space. /// /// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features) /// /// Related SQLite documentation: /// /// - Parameter filePath: file path for new database @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func vacuum(into filePath: String) async throws { try await writeWithoutTransaction { try $0.execute(sql: "VACUUM INTO ?", arguments: [filePath]) } } #endif } #if canImport(Combine) extension DatabaseWriter { // MARK: - Publishing Database Updates /// Returns a publisher that publishes one value and completes. /// /// The database is not accessed until subscription. Value and completion /// are published on `scheduler` (the main dispatch queue by default). /// /// For example: /// /// ```swift /// // DatabasePublishers.Write /// let newPlayerCountPublisher = writer.writePublisher { db in /// try Player(name: "Arthur").insert(db) /// return try Player.fetchCount(db) /// } /// ``` /// /// Database operations are wrapped in a transaction. If they throw an /// error, the transaction is rollbacked and the error completes /// the publisher. /// /// Concurrent database accesses can not see partial database updates (even /// when performed by other processes). /// /// Database operations are asynchronously dispatched 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. /// /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which accesses the database. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func writePublisher( receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main, updates: @escaping (Database) throws -> Output) -> DatabasePublishers.Write { OnDemandFuture { fulfill in self.asyncWrite(updates, completion: { _, result in fulfill(result) }) } // We don't want users to process emitted values on a // database dispatch queue. .receiveValues(on: scheduler) .eraseToWritePublisher() } /// Returns a publisher that publishes one value and completes. /// /// The database is not accessed until subscription. Value and completion /// are published on `scheduler` (the main dispatch queue by default). /// /// For example: /// /// ```swift /// // DatabasePublishers.Write /// let newPlayerCountPublisher = writer.writePublisher { db in /// try Player(name: "Arthur").insert(db) /// } thenRead: { db, _ in /// try Player.fetchCount(db) /// } /// ``` /// /// The returned publisher publishes exactly the same value as /// ``writePublisher(receiveOn:updates:)``: /// /// ```swift /// // DatabasePublishers.Write /// let newPlayerCountPublisher = writer.writePublisher { db in /// try Player(name: "Arthur").insert(db) /// return try Player.fetchCount(db) /// } /// ``` /// /// The difference is that the last fetches are performed in the `thenRead` /// closure. This closure accepts two arguments: a read-only database /// connection, and the result of the `updates` function. This allows you to /// pass information from a function to the other (it is ignored in the /// sample code above). /// /// When you use a ``DatabasePool``, this method applies a scheduling /// optimization: the `thenRead` closure sees the database in the state left /// by the `updates` closure, but it does not block any concurrent writes. /// This can reduce database write contention. /// /// When you use a ``DatabaseQueue``, the results are guaranteed to be /// identical, but no scheduling optimization is applied. /// /// The ``Database`` argument to `updates` and `value` is valid only during /// the execution of those closures. Do not store or return the database /// connection for later use. /// /// - parameter scheduler: A Combine Scheduler. /// - parameter updates: A closure which writes in the database. /// - parameter value: A closure which reads from the database. @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) public func writePublisher( receiveOn scheduler: S = DispatchQueue.main, updates: @escaping (Database) throws -> T, thenRead value: @escaping (Database, T) throws -> Output) -> DatabasePublishers.Write where S: Scheduler { OnDemandFuture { fulfill in self.asyncWriteWithoutTransaction { db in var updatesValue: T? do { try db.inTransaction { updatesValue = try updates(db) return .commit } } catch { fulfill(.failure(error)) return } self.spawnConcurrentRead { dbResult in fulfill(dbResult.flatMap { db in Result { try value(db, updatesValue!) } }) } } } // We don't want users to process emitted values on a // database dispatch queue. .receiveValues(on: scheduler) .eraseToWritePublisher() } } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension DatabasePublishers { /// A publisher that writes into the database. /// /// `Write` publishes exactly one element, or an error. /// /// You build such a publisher from ``DatabaseWriter``. public struct Write: Publisher { public typealias Output = Output public typealias Failure = Error fileprivate let upstream: AnyPublisher public func receive(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { upstream.receive(subscriber: subscriber) } } } @available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) extension Publisher where Failure == Error { fileprivate func eraseToWritePublisher() -> DatabasePublishers.Write { .init(upstream: self.eraseToAnyPublisher()) } } #endif /// A future database value. /// /// You get instances of `DatabaseFuture` from the `DatabaseWriter` /// ``DatabaseWriter/concurrentRead(_:)`` method. For example: /// /// ```swift /// let futureCount: Future = try writer.writeWithoutTransaction { db in /// try Player(...).insert() /// /// // Count players concurrently /// return writer.concurrentRead { db in /// return try Player.fetchCount() /// } /// } /// /// let count: Int = try futureCount.wait() /// ``` public class DatabaseFuture { private var consumed = false private let _wait: () throws -> Value init(_ wait: @escaping () throws -> Value) { _wait = wait } init(_ result: Result) { _wait = result.get } /// Blocks the current thread until the value is available, and returns it. /// /// It is a programmer error to call this method several times. /// /// - throws: Any error that prevented the value from becoming available. public func wait() throws -> Value { // Not thread-safe and quick and dirty. // Goal is that users learn not to call this method twice. GRDBPrecondition(consumed == false, "DatabaseFuture.wait() must be called only once") consumed = true return try _wait() } } /// A type-erased database writer. /// /// An instance of `AnyDatabaseWriter` forwards its operations to an underlying /// base database writer. public final class AnyDatabaseWriter { private let base: any DatabaseWriter /// Creates a new database reader that wraps and forwards operations /// to `base`. public init(_ base: some DatabaseWriter) { self.base = base } } extension AnyDatabaseWriter: DatabaseReader { public var configuration: Configuration { base.configuration } public var path: String { base.path } public func close() throws { try base.close() } public func interrupt() { base.interrupt() } @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func read(_ value: (Database) throws -> T) throws -> T { try base.read(value) } public func asyncRead(_ value: @escaping (Result) -> Void) { base.asyncRead(value) } @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func unsafeRead(_ value: (Database) throws -> T) throws -> T { try base.unsafeRead(value) } public func asyncUnsafeRead(_ value: @escaping (Result) -> Void) { base.asyncUnsafeRead(value) } public func unsafeReentrantRead(_ value: (Database) throws -> T) throws -> T { try base.unsafeReentrantRead(value) } public func _add( observation: ValueObservation, scheduling scheduler: some ValueObservationScheduler, onChange: @escaping (Reducer.Value) -> Void) -> AnyDatabaseCancellable { base._add( observation: observation, scheduling: scheduler, onChange: onChange) } } extension AnyDatabaseWriter: DatabaseWriter { @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func writeWithoutTransaction(_ updates: (Database) throws -> T) rethrows -> T { try base.writeWithoutTransaction(updates) } @_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails public func barrierWriteWithoutTransaction(_ updates: (Database) throws -> T) throws -> T { try base.barrierWriteWithoutTransaction(updates) } public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result) -> Void) { base.asyncBarrierWriteWithoutTransaction(updates) } public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) { base.asyncWriteWithoutTransaction(updates) } public func unsafeReentrantWrite(_ updates: (Database) throws -> T) rethrows -> T { try base.unsafeReentrantWrite(updates) } public func concurrentRead(_ value: @escaping (Database) throws -> T) -> DatabaseFuture { base.concurrentRead(value) } public func spawnConcurrentRead(_ value: @escaping (Result) -> Void) { base.spawnConcurrentRead(value) } }