1067 lines
42 KiB
Swift
1067 lines
42 KiB
Swift
#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 <doc:Concurrency> 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
|
|
/// <doc:Concurrency#Rule-2:-Mind-your-transactions> 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<T>(_ 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
|
|
/// <doc:Concurrency#Rule-2:-Mind-your-transactions> 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<T>(_ 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
|
|
/// <doc:Concurrency#Rule-2:-Mind-your-transactions> 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<Database, Error>) -> 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
|
|
/// <doc:Concurrency#Rule-2:-Mind-your-transactions> 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 <doc:Concurrency#Rule-2:-Mind-your-transactions> 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<T>(_ 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<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T>
|
|
|
|
// 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<Database, Error>) -> 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<T>(_ 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<T>(
|
|
_ updates: @escaping (Database) throws -> T,
|
|
completion: @escaping (Database, Result<T, Error>) -> 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: <https://www.sqlite.org/lang_vacuum.html>
|
|
@_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: <https://www.sqlite.org/lang_vacuum.html#vacuuminto>
|
|
///
|
|
/// - 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: <https://www.sqlite.org/lang_vacuum.html#vacuuminto>
|
|
///
|
|
/// - 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<Reducer: ValueReducer>(
|
|
observation: ValueObservation<Reducer>,
|
|
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<T>(_ 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<T>(_ 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<T>(
|
|
_ 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: <https://www.sqlite.org/lang_vacuum.html>
|
|
@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: <https://www.sqlite.org/lang_vacuum.html#vacuuminto>
|
|
///
|
|
/// - 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: <https://www.sqlite.org/lang_vacuum.html#vacuuminto>
|
|
///
|
|
/// - 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<Int>
|
|
/// 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<Output>(
|
|
receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main,
|
|
updates: @escaping (Database) throws -> Output)
|
|
-> DatabasePublishers.Write<Output>
|
|
{
|
|
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<Int>
|
|
/// 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<Int>
|
|
/// 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<S, T, Output>(
|
|
receiveOn scheduler: S = DispatchQueue.main,
|
|
updates: @escaping (Database) throws -> T,
|
|
thenRead value: @escaping (Database, T) throws -> Output)
|
|
-> DatabasePublishers.Write<Output>
|
|
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<Output>: Publisher {
|
|
public typealias Output = Output
|
|
public typealias Failure = Error
|
|
|
|
fileprivate let upstream: AnyPublisher<Output, Error>
|
|
|
|
public func receive<S>(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<Output> {
|
|
.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<Int> = 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<Value> {
|
|
private var consumed = false
|
|
private let _wait: () throws -> Value
|
|
|
|
init(_ wait: @escaping () throws -> Value) {
|
|
_wait = wait
|
|
}
|
|
|
|
init(_ result: Result<Value, Error>) {
|
|
_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<T>(_ value: (Database) throws -> T) throws -> T {
|
|
try base.read(value)
|
|
}
|
|
|
|
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
|
base.asyncRead(value)
|
|
}
|
|
|
|
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
|
public func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T {
|
|
try base.unsafeRead(value)
|
|
}
|
|
|
|
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
|
base.asyncUnsafeRead(value)
|
|
}
|
|
|
|
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
|
|
try base.unsafeReentrantRead(value)
|
|
}
|
|
|
|
public func _add<Reducer: ValueReducer>(
|
|
observation: ValueObservation<Reducer>,
|
|
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<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
|
try base.writeWithoutTransaction(updates)
|
|
}
|
|
|
|
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
|
public func barrierWriteWithoutTransaction<T>(_ updates: (Database) throws -> T) throws -> T {
|
|
try base.barrierWriteWithoutTransaction(updates)
|
|
}
|
|
|
|
public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
|
|
base.asyncBarrierWriteWithoutTransaction(updates)
|
|
}
|
|
|
|
public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
|
|
base.asyncWriteWithoutTransaction(updates)
|
|
}
|
|
|
|
public func unsafeReentrantWrite<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
|
try base.unsafeReentrantWrite(updates)
|
|
}
|
|
|
|
public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
|
|
base.concurrentRead(value)
|
|
}
|
|
|
|
public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
|
base.spawnConcurrentRead(value)
|
|
}
|
|
}
|