187 lines
5.9 KiB
Swift
187 lines
5.9 KiB
Swift
import Dispatch
|
|
|
|
/// A database connection that serializes accesses to an unchanging
|
|
/// database content, as it existed at the moment the snapshot was created.
|
|
///
|
|
/// ## Overview
|
|
///
|
|
/// A `DatabaseSnapshot` never sees any database modification during all its
|
|
/// lifetime. All database accesses performed from a snapshot always see the
|
|
/// same identical database content.
|
|
///
|
|
/// A snapshot creates one single SQLite connection. All database
|
|
/// accesses are executed in a serial **reader dispatch queue**. The SQLite
|
|
/// connection is closed when the `DatabaseSnapshot` is deallocated.
|
|
///
|
|
/// A snapshot created on a [WAL](https://sqlite.org/wal.html) database doesn't
|
|
/// prevent database modifications performed by other connections (but it won't
|
|
/// see them). Refer to [Isolation In SQLite](https://sqlite.org/isolation.html)
|
|
/// for more information.
|
|
///
|
|
/// On non-WAL databases, a snapshot prevents all database modifications as long
|
|
/// as it exists, because of the
|
|
/// [SHARED lock](https://www.sqlite.org/lockingv3.html) it holds.
|
|
///
|
|
/// ## Usage
|
|
///
|
|
/// You create instances of `DatabaseSnapshot` from a ``DatabasePool``,
|
|
/// with ``DatabasePool/makeSnapshot()``:
|
|
///
|
|
/// ```swift
|
|
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
|
|
/// let snapshot = try dbPool.makeSnapshot()
|
|
/// let playerCount = try snapshot.read { db in
|
|
/// try Player.fetchCount(db)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// When you want to control the database state seen by a snapshot, create the
|
|
/// snapshot from within a write access, outside of any transaction.
|
|
///
|
|
/// For example, compare the two snapshots below. The first one is guaranteed to
|
|
/// see an empty table of players, because is is created after all players have
|
|
/// been deleted, and from the serialized writer dispatch queue which prevents
|
|
/// any concurrent write. The second is created without this concurrency
|
|
/// protection, which means that some other threads may already have created
|
|
/// some players:
|
|
///
|
|
/// ```swift
|
|
/// let snapshot1 = try dbPool.writeWithoutTransaction { db -> DatabaseSnapshot in
|
|
/// try db.inTransaction {
|
|
/// try Player.deleteAll()
|
|
/// return .commit
|
|
/// }
|
|
///
|
|
/// return try dbPool.makeSnapshot()
|
|
/// }
|
|
///
|
|
/// // <- Other threads may have created some players here
|
|
/// let snapshot2 = try dbPool.makeSnapshot()
|
|
///
|
|
/// // Guaranteed to be zero
|
|
/// let count1 = try snapshot1.read { db in
|
|
/// try Player.fetchCount(db)
|
|
/// }
|
|
///
|
|
/// // Could be anything
|
|
/// let count2 = try snapshot2.read { db in
|
|
/// try Player.fetchCount(db)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// `DatabaseSnapshot` inherits its database access methods from the
|
|
/// ``DatabaseReader`` protocols.
|
|
///
|
|
/// `DatabaseSnapshot` serializes database accesses and can't perform concurrent
|
|
/// reads. For concurrent reads, see ``DatabaseSnapshotPool``.
|
|
public final class DatabaseSnapshot {
|
|
private let reader: SerializedDatabase
|
|
|
|
public var configuration: Configuration {
|
|
reader.configuration
|
|
}
|
|
|
|
/// The path to the database file.
|
|
public var path: String {
|
|
reader.path
|
|
}
|
|
|
|
init(
|
|
path: String,
|
|
configuration: Configuration,
|
|
defaultLabel: String = "GRDB.DatabaseSnapshot",
|
|
purpose: String? = nil)
|
|
throws
|
|
{
|
|
let configuration = Self.configure(configuration)
|
|
|
|
reader = try SerializedDatabase(
|
|
path: path,
|
|
configuration: configuration,
|
|
defaultLabel: defaultLabel,
|
|
purpose: purpose)
|
|
|
|
try reader.sync { db in
|
|
// Open transaction
|
|
try db.beginTransaction(.deferred)
|
|
|
|
// Acquire snapshot isolation
|
|
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
// Leave snapshot isolation
|
|
reader.reentrantSync { db in
|
|
try? db.commit()
|
|
}
|
|
}
|
|
|
|
private static func configure(_ configuration: Configuration) -> Configuration {
|
|
var configuration = configuration
|
|
|
|
// DatabaseSnapshot can't perform parallel reads.
|
|
configuration.maximumReaderCount = 1
|
|
|
|
// DatabaseSnapshot is read-only.
|
|
configuration.readonly = true
|
|
|
|
// DatabaseSnapshot uses deferred transactions by default.
|
|
// Other transaction kinds are forbidden by SQLite in read-only connections.
|
|
configuration.defaultTransactionKind = .deferred
|
|
|
|
// DatabaseSnapshot keeps a long-lived transaction.
|
|
configuration.allowsUnsafeTransactions = true
|
|
|
|
return configuration
|
|
}
|
|
}
|
|
|
|
extension DatabaseSnapshot: DatabaseSnapshotReader {
|
|
public func close() throws {
|
|
try reader.sync { try $0.close() }
|
|
}
|
|
|
|
// MARK: - Interrupting Database Operations
|
|
|
|
public func interrupt() {
|
|
reader.interrupt()
|
|
}
|
|
|
|
// MARK: - Reading from Database
|
|
|
|
public func read<T>(_ block: (Database) throws -> T) rethrows -> T {
|
|
try reader.sync(block)
|
|
}
|
|
|
|
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
|
reader.async { value(.success($0)) }
|
|
}
|
|
|
|
public func unsafeRead<T>(_ value: (Database) throws -> T) rethrows -> T {
|
|
try reader.sync(value)
|
|
}
|
|
|
|
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
|
reader.async { value(.success($0)) }
|
|
}
|
|
|
|
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
|
|
try reader.reentrantSync(value)
|
|
}
|
|
|
|
// MARK: - Database Observation
|
|
|
|
public func _add<Reducer: ValueReducer>(
|
|
observation: ValueObservation<Reducer>,
|
|
scheduling scheduler: some ValueObservationScheduler,
|
|
onChange: @escaping (Reducer.Value) -> Void)
|
|
-> AnyDatabaseCancellable
|
|
{
|
|
_addReadOnly(
|
|
observation: observation,
|
|
scheduling: scheduler,
|
|
onChange: onChange)
|
|
}
|
|
}
|