Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/Core/DatabaseReader.swift
T

765 lines
28 KiB
Swift

#if canImport(Combine)
import Combine
#endif
import Dispatch
/// A type that reads from an SQLite database.
///
/// Do not declare new conformances to `DatabaseReader`. Only the built-in
/// conforming types are valid.
///
/// The protocol comes with isolation guarantees that describe the behavior of
/// conforming types in a multithreaded application. See <doc:Concurrency> for
/// more information.
///
/// ## Topics
///
/// ### Database Information
///
/// - ``configuration``
/// - ``path``
///
/// ### Reading from the Database
///
/// - ``read(_:)-3806d``
/// - ``read(_:)-4w6gy``
/// - ``readPublisher(receiveOn:value:)``
/// - ``asyncRead(_:)``
///
/// ### Unsafe Methods
///
/// - ``unsafeRead(_:)-5i7tf``
/// - ``unsafeRead(_:)-11mk0``
/// - ``unsafeReentrantRead(_:)``
/// - ``asyncUnsafeRead(_:)``
///
/// ### Printing Database Content
///
/// - ``dumpContent(format:to:)``
/// - ``dumpRequest(_:format:to:)``
/// - ``dumpSchema(to:)``
/// - ``dumpSQL(_:format:to:)``
/// - ``dumpTables(_:format:tableHeader:stableOrder:to:)``
/// - ``DumpFormat``
/// - ``DumpTableHeaderOptions``
///
/// ### Other Database Operations
///
/// - ``backup(to:pagesPerStep:progress:)``
/// - ``close()``
/// - ``interrupt()``
///
/// ### Supporting Types
///
/// - ``AnyDatabaseReader``
public protocol DatabaseReader: AnyObject, Sendable {
/// The database configuration.
var configuration: Configuration { get }
/// The path to the database file.
///
/// In-memory databases also have a path:
/// see [In-Memory Databases](https://www.sqlite.org/inmemorydb.html).
var path: String { get }
/// Closes the database connection.
///
/// - note: You do not have to call this method, and you should not call
/// it unless the correct execution of your program depends on precise
/// database closing. Database connections are automatically closed when
/// they are deinitialized, and this is sufficient for most applications.
///
/// If this method does not throw, then the database is properly closed, and
/// every future database access will throw a ``DatabaseError`` of
/// code `SQLITE_MISUSE`.
///
/// Otherwise, there exists concurrent database accesses or living prepared
/// statements that prevent the database from closing, and this method
/// throws a ``DatabaseError`` of code `SQLITE_BUSY`.
/// See <https://www.sqlite.org/c3ref/close.html> for more information.
///
/// After an error has been thrown, the database may still be opened, and
/// you can keep on accessing it. It may also remain in a "zombie" state,
/// in which case it will throw `SQLITE_MISUSE` for all future
/// database accesses.
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
func close() throws
// MARK: - Interrupting Database Operations
/// Causes any pending database operation to abort and return at its
/// earliest opportunity.
///
/// This method can be called from any thread.
///
/// A call to `interrupt()` that occurs when there are no running SQL
/// statements is a no-op and has no effect on SQL statements that are
/// started after `interrupt()` returns.
///
/// A database operation that is interrupted will throw a ``DatabaseError``
/// with code `SQLITE_INTERRUPT`. If the interrupted SQL operation is an
/// `INSERT`, `UPDATE`, or `DELETE` that is inside an explicit transaction,
/// then the entire transaction will be rolled back automatically. If the
/// rolled back transaction was started by a transaction-wrapping method
/// such as ``DatabaseWriter/write(_:)-76inz`` or
/// ``Database/inTransaction(_:_:)``, then all database accesses will throw
/// a ``DatabaseError`` with code `SQLITE_ABORT` until the wrapping
/// method returns.
///
/// For example:
///
/// ```swift
/// try dbQueue.write { db in
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// // not executed:
/// try Player(...).insert(db)
/// } // throws SQLITE_INTERRUPT
///
/// try dbQueue.write { db in
/// do {
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// } catch { }
/// try Player(...).insert(db) // throws SQLITE_ABORT
/// } // throws SQLITE_ABORT
///
/// try dbQueue.write { db in
/// do {
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// } catch { }
/// } // throws SQLITE_ABORT
/// ```
///
/// Beware: when an application opens a transaction without a
/// transaction-wrapping method, no `SQLITE_ABORT` error warns of
/// aborted transactions:
///
/// ```swift
/// try dbQueue.inDatabase { db in // or dbPool.writeWithoutTransaction
/// try db.beginTransaction()
/// do {
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// } catch { }
/// try Player(...).insert(db) // success
/// try db.commit() // throws SQLITE_ERROR "cannot commit - no transaction is active"
/// }
/// ```
///
/// Both `SQLITE_ABORT` and `SQLITE_INTERRUPT` errors can be checked with the
/// ``DatabaseError/isInterruptionError`` property.
func interrupt()
// MARK: - Read From Database
/// Executes read-only database operations, and returns their result after
/// they have finished executing.
///
/// For example:
///
/// ```swift
/// let count = try reader.read { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// Database operations are isolated in a transaction: they do not see
/// changes performed by eventual concurrent writes (even writes performed
/// by other processes).
///
/// The database connection is read-only: attempts to write throw a
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
///
/// The ``Database`` argument to `value` 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 value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
func read<T>(_ value: (Database) throws -> T) throws -> T
/// Schedules read-only database operations for execution, and
/// returns immediately.
///
/// For example:
///
/// ```swift
/// try reader.asyncRead { dbResult in
/// do {
/// let db = try dbResult.get()
/// let count = try Player.fetchCount(db)
/// } catch {
/// // Handle error
/// }
/// }
/// ```
///
/// Database operations are isolated in a transaction: they do not see
/// changes performed by eventual concurrent writes (even writes performed
/// by other processes).
///
/// The database connection is read-only: attempts to write throw a
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
///
/// - 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 asyncRead(_ value: @escaping (Result<Database, Error>) -> Void)
/// Executes database operations, and returns their result after they have
/// finished executing.
///
/// This method is "unsafe" because the database reader does nothing more
/// than providing a database connection. When you use this method, you
/// become responsible for the thread-safety of your application, and
/// responsible for database accesses performed by other processes. See
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
/// more information.
///
/// For example:
///
/// ```swift
/// let count = try reader.unsafeRead { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// The ``Database`` argument to `value` 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 may not be wrapped in a transaction. They
/// may see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `value`
/// closure may not return the same value.
/// - warning: Attempts to write in the database may succeed.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T
/// Schedules database operations for execution, and returns immediately.
///
/// This method is "unsafe" because the database reader does nothing more
/// than providing a database connection. When you use this method, you
/// become responsible for the thread-safety of your application, and
/// responsible for database accesses performed by other processes. See
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
/// more information.
///
/// For example:
///
/// ```swift
/// reader.asyncUnsafeRead { dbResult in
/// do {
/// let db = try dbResult.get()
/// let count = try Player.fetchCount(db)
/// } catch {
/// // handle error
/// }
/// }
/// ```
///
/// - warning: Database operations may not be wrapped in a transaction. They
/// may see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `value`
/// closure may not return the same value.
/// - warning: Attempts to write in the database may succeed.
///
/// - 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 asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void)
/// Executes database operations, and returns their result after they have
/// finished executing.
///
/// This method is "unsafe" because the database reader does nothing more
/// than providing a database connection. When you use this method, you
/// become responsible for the thread-safety of your application, and
/// responsible for database accesses performed by other processes. See
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
/// more information.
///
/// This method can be called from other database access methods. If called
/// from the dispatch queue of a current database access (read or write),
/// the `Database` argument to `value` is the same as the current
/// database access.
///
/// 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 count = try reader.unsafeReentrantRead { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - warning: Database operations may not be wrapped in a transaction. They
/// may see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `value`
/// closure may not return the same value.
/// - warning: Attempts to write in the database may succeed.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T
// MARK: - Value Observation
/// Starts a value observation.
///
/// Use the ``ValueObservation/start(in:scheduling:onError:onChange:)``
/// method instead.
///
/// - parameter observation: a ValueObservation.
/// - returns: A DatabaseCancellable that can stop the observation.
func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
}
extension DatabaseReader {
// MARK: - Backup
/// Copies the database contents into another database.
///
/// The `backup` method blocks the current thread until the destination
/// database contains the same contents as the source database.
///
/// When the source is a DatabasePool, concurrent writes can happen during
/// the backup. Those writes may, or may not, be reflected in the backup,
/// but they won't trigger any error.
///
/// Usage:
///
/// ```swift
/// let source: DatabaseQueue = ...
/// let destination: DatabaseQueue = ...
/// try source.backup(to: destination)
/// ```
///
/// When you're after progress reporting during backup, you'll want to
/// perform the backup in several steps. Each step copies the number of
/// _database pages_ you specify. See <https://www.sqlite.org/c3ref/backup_finish.html>
/// for more information:
///
/// ```swift
/// // Backup with progress reporting
/// try source.backup(to: destination, pagesPerStep: ...) { progress in
/// print("Database backup progress:", progress)
/// }
/// ```
///
/// The `progress` callback will be called at least once—when
/// `backupProgress.isCompleted == true`. If the callback throws
/// when `backupProgress.isCompleted == false`, the backup is aborted
/// and the error is rethrown. If the callback throws when
/// `backupProgress.isCompleted == true`, backup completion is
/// unaffected and the error is silently ignored.
///
/// See also ``Database/backup(to:pagesPerStep:progress:)``
///
/// - parameters:
/// - writer: The destination database.
/// - pagesPerStep: The number of database pages copied on each backup
/// step. By default, all pages are copied in one single step.
/// - progress: An optional function that is notified of the backup
/// progress.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the
/// error thrown by `progress`.
public func backup(
to writer: some DatabaseWriter,
pagesPerStep: CInt = -1,
progress: ((DatabaseBackupProgress) throws -> Void)? = nil)
throws
{
try writer.writeWithoutTransaction { destDb in
try backup(
to: destDb,
pagesPerStep: pagesPerStep,
afterBackupStep: progress)
}
}
func backup(
to destDb: Database,
pagesPerStep: CInt = -1,
afterBackupInit: (() -> Void)? = nil,
afterBackupStep: ((DatabaseBackupProgress) throws -> Void)? = nil)
throws
{
try read { dbFrom in
try dbFrom.backupInternal(
to: destDb,
pagesPerStep: pagesPerStep,
afterBackupInit: afterBackupInit,
afterBackupStep: afterBackupStep)
}
}
}
extension DatabaseReader {
// MARK: - Asynchronous Database Access
/// Executes read-only 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 count = try await reader.read { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// Database operations are isolated in a transaction: they do not see
/// changes performed by eventual concurrent writes (even writes performed
/// by other processes).
///
/// The database connection is read-only: attempts to write throw a
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func read<T>(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T {
try await withUnsafeThrowingContinuation { continuation in
asyncRead { result in
do {
try continuation.resume(returning: value(result.get()))
} 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)
///
/// This method is "unsafe" because the database reader does nothing more
/// than providing a database connection. When you use this method, you
/// become responsible for the thread-safety of your application, and
/// responsible for database accesses performed by other processes. See
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
/// more information.
///
/// For example:
///
/// ```swift
/// let count = try await reader.unsafeRead { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - warning: Database operations may not be wrapped in a transaction. They
/// may see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `value`
/// closure may not return the same value.
/// - warning: Attempts to write in the database may succeed.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func unsafeRead<T>(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T {
try await withUnsafeThrowingContinuation { continuation in
asyncUnsafeRead { result in
do {
try continuation.resume(returning: value(result.get()))
} catch {
continuation.resume(throwing: error)
}
}
}
}
}
#if canImport(Combine)
extension DatabaseReader {
// MARK: - Publishing Database Values
/// 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.Read<Int>
/// let countPublisher = reader.readPublisher { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// Database operations are isolated in a transaction: they do not see
/// changes performed by eventual concurrent writes (even writes performed
/// by other processes).
///
/// The database connection is read-only: attempts to write throw a
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
///
/// The ``Database`` argument to `value` 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 value: A closure which accesses the database.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func readPublisher<Output>(
receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main,
value: @escaping (Database) throws -> Output)
-> DatabasePublishers.Read<Output>
{
Deferred {
Future { fulfill in
self.asyncRead { dbResult in
fulfill(dbResult.flatMap { db in Result { try value(db) } })
}
}
}
.receiveValues(on: scheduler)
.eraseToReadPublisher()
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
extension DatabasePublishers {
/// A publisher that reads from the database.
///
/// `Read` publishes exactly one element, or an error.
///
/// You build such a publisher from ``DatabaseReader``.
public struct Read<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 eraseToReadPublisher() -> DatabasePublishers.Read<Output> {
.init(upstream: eraseToAnyPublisher())
}
}
#endif
extension DatabaseReader {
// MARK: - Value Observation Support
/// Adding an observation in a read-only database emits only the
/// initial value.
func _addReadOnly<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
if scheduler.immediateInitialValue() {
do {
// Perform a reentrant read, in case the observation would be
// started from a database access.
let value = try unsafeReentrantRead { db in
try db.isolated(readOnly: true) {
try observation.fetchInitialValue(db)
}
}
onChange(value)
} catch {
observation.events.didFail?(error)
}
return AnyDatabaseCancellable(cancel: { /* nothing to cancel */ })
} else {
var isCancelled = false
asyncRead { dbResult in
guard !isCancelled else { return }
let result = dbResult.flatMap { db in
Result { try observation.fetchInitialValue(db) }
}
scheduler.schedule {
guard !isCancelled else { return }
do {
try onChange(result.get())
} catch {
observation.events.didFail?(error)
}
}
}
return AnyDatabaseCancellable(cancel: { isCancelled = true })
}
}
}
/// A type-erased database reader.
///
/// An instance of `AnyDatabaseReader` forwards its operations to an underlying
/// base database reader.
public final class AnyDatabaseReader {
private let base: any DatabaseReader
/// Creates a new database reader that wraps and forwards operations
/// to `base`.
public init(_ base: some DatabaseReader) {
self.base = base
}
}
extension AnyDatabaseReader: 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)
}
}
/// A type that sees an unchanging database content.
///
/// Do not declare new conformances to `DatabaseSnapshotReader`. Only the
/// built-in conforming types are valid.
///
/// The protocol comes with the same features and guarantees as
/// ``DatabaseReader``. On top of them, a `DatabaseSnapshotReader` always sees
/// the same state of the database.
///
/// ## Topics
///
/// ### Reading from the Database
///
/// - ``reentrantRead(_:)``
public protocol DatabaseSnapshotReader: DatabaseReader { }
extension DatabaseSnapshotReader {
/// Executes database operations, and returns their result after they have
/// finished executing.
///
/// This method can be called from other database access methods. If called
/// from the dispatch queue of a current database access, the `Database`
/// argument to `value` is the same as the current database access.
///
/// For example:
///
/// ```swift
/// let count = try snapshot.reentrantRead { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
public func reentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
// Reentrant reads are safe in a snapshot
try unsafeReentrantRead(value)
}
// There is no such thing as an unsafe access to a snapshot.
public func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T {
try read(value)
}
// There is no such thing as an unsafe access to a snapshot.
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
asyncRead(value)
}
}