add
This commit is contained in:
@@ -1,382 +0,0 @@
|
||||
// swiftlint:disable:next line_length
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
/// A database connection that allows concurrent accesses to an unchanging
|
||||
/// database content, as it existed at the moment the snapshot was created.
|
||||
///
|
||||
/// ## Overview
|
||||
///
|
||||
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
///
|
||||
/// A `DatabaseSnapshotPool` never sees any database modification during all its
|
||||
/// lifetime. All database accesses performed from a snapshot always see the
|
||||
/// same identical database content.
|
||||
///
|
||||
/// It creates a pool of up to ``Configuration/maximumReaderCount`` read-only
|
||||
/// SQLite connections. All read accesses are executed in **reader dispatch
|
||||
/// queues** (one per read-only SQLite connection). SQLite connections are
|
||||
/// closed when the `DatabasePool` is deallocated.
|
||||
///
|
||||
/// An SQLite database in the [WAL mode](https://www.sqlite.org/wal.html) is
|
||||
/// required for creating a `DatabaseSnapshotPool`.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// You create a `DatabaseSnapshotPool` from a
|
||||
/// [WAL mode](https://www.sqlite.org/wal.html) database, such as databases
|
||||
/// created from a ``DatabasePool``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
|
||||
/// let snapshot = try dbPool.makeSnapshotPool()
|
||||
/// ```
|
||||
///
|
||||
/// When you want to control the database state seen by a snapshot, create the
|
||||
/// snapshot from a database connection, outside of a write transaction. You can
|
||||
/// for example take snapshots from a ``ValueObservation``:
|
||||
///
|
||||
/// ```swift
|
||||
/// // An observation of the 'player' table
|
||||
/// // that notifies fresh database snapshots:
|
||||
/// let observation = ValueObservation.tracking { db in
|
||||
/// // Don't fetch players now, and return a snapshot instead.
|
||||
/// // Register an access to the player table so that the
|
||||
/// // observation tracks changes to this table.
|
||||
/// try db.registerAccess(to: Player.all())
|
||||
/// return try DatabaseSnapshotPool(db)
|
||||
/// }
|
||||
///
|
||||
/// // Start observing the 'player' table
|
||||
/// let cancellable = try observation.start(in: dbPool) { error in
|
||||
/// // Handle error
|
||||
/// } onChange: { (snapshot: DatabaseSnapshotPool) in
|
||||
/// // Handle a fresh snapshot
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// `DatabaseSnapshotPool` inherits its database access methods from the
|
||||
/// ``DatabaseReader`` protocols.
|
||||
///
|
||||
/// Related SQLite documentation:
|
||||
///
|
||||
/// - <https://www.sqlite.org/c3ref/snapshot_get.html>
|
||||
/// - <https://www.sqlite.org/c3ref/snapshot_open.html>
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating a DatabaseSnapshotPool
|
||||
///
|
||||
/// See also ``DatabasePool/makeSnapshotPool()``.
|
||||
///
|
||||
/// - ``init(_:configuration:)``
|
||||
/// - ``init(path:configuration:)``
|
||||
public final class DatabaseSnapshotPool {
|
||||
public let configuration: Configuration
|
||||
|
||||
/// The path to the database file.
|
||||
public let path: String
|
||||
|
||||
/// The pool of reader connections.
|
||||
/// It is constant, until close() sets it to nil.
|
||||
private var readerPool: Pool<SerializedDatabase>?
|
||||
|
||||
/// The WAL snapshot
|
||||
private let walSnapshot: WALSnapshot
|
||||
|
||||
/// A connection that prevents checkpoints and keeps the WAL snapshot valid.
|
||||
/// It is never used.
|
||||
private let snapshotHolder: DatabaseQueue
|
||||
|
||||
/// Creates a snapshot of the database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
|
||||
/// let snapshot = try dbPool.writeWithoutTransaction { db -> DatabaseSnapshotPool in
|
||||
/// try db.inTransaction {
|
||||
/// try Player.deleteAll()
|
||||
/// return .commit
|
||||
/// }
|
||||
///
|
||||
/// // Create the snapshot after all players have been deleted.
|
||||
/// return DatabaseSnapshotPool(db)
|
||||
/// }
|
||||
///
|
||||
/// // Later... Maybe some players have been created.
|
||||
/// // The snapshot is guaranteed to see an empty table of players, though:
|
||||
/// let count = try snapshot.read { db in
|
||||
/// try Player.fetchCount(db)
|
||||
/// }
|
||||
/// assert(count == 0)
|
||||
/// ```
|
||||
///
|
||||
/// A ``DatabaseError`` of code `SQLITE_ERROR` is thrown if the SQLite
|
||||
/// database is not in the [WAL mode](https://www.sqlite.org/wal.html),
|
||||
/// or if this method is called from a write transaction, or if the
|
||||
/// wal file is missing or truncated (size zero).
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/snapshot_get.html>
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter configuration: A configuration. If nil, the configuration of
|
||||
/// `db` is used.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public init(_ db: Database, configuration: Configuration? = nil) throws {
|
||||
var configuration = Self.configure(configuration ?? db.configuration)
|
||||
|
||||
// Acquire and hold WAL snapshot
|
||||
let walSnapshot = try db.isolated(readOnly: true) {
|
||||
try WALSnapshot(db)
|
||||
}
|
||||
var holderConfig = Configuration()
|
||||
holderConfig.allowsUnsafeTransactions = true
|
||||
snapshotHolder = try DatabaseQueue(path: db.path, configuration: holderConfig)
|
||||
try snapshotHolder.inDatabase { db in
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
|
||||
let code = sqlite3_snapshot_open(db.sqliteConnection, "main", walSnapshot.sqliteSnapshot)
|
||||
guard code == SQLITE_OK else {
|
||||
throw DatabaseError(resultCode: code)
|
||||
}
|
||||
}
|
||||
|
||||
configuration.prepareDatabase { db in
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
|
||||
let code = sqlite3_snapshot_open(db.sqliteConnection, "main", walSnapshot.sqliteSnapshot)
|
||||
guard code == SQLITE_OK else {
|
||||
throw DatabaseError(resultCode: code)
|
||||
}
|
||||
}
|
||||
|
||||
self.configuration = configuration
|
||||
self.path = db.path
|
||||
self.walSnapshot = walSnapshot
|
||||
|
||||
var readerCount = 0
|
||||
readerPool = Pool(
|
||||
maximumCount: configuration.maximumReaderCount,
|
||||
qos: configuration.readQoS,
|
||||
makeElement: {
|
||||
readerCount += 1 // protected by Pool (TODO: document this protection behavior)
|
||||
return try SerializedDatabase(
|
||||
path: db.path,
|
||||
configuration: configuration,
|
||||
defaultLabel: "GRDB.DatabaseSnapshotPool",
|
||||
purpose: "snapshot.\(readerCount)")
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a snapshot of the database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let snapshot = try DatabaseSnapshotPool(path: "/path/to/database.sqlite")
|
||||
/// ```
|
||||
///
|
||||
/// A ``DatabaseError`` of code `SQLITE_ERROR` is thrown if the SQLite
|
||||
/// database is not in the [WAL mode](https://www.sqlite.org/wal.html),
|
||||
/// or if the wal file is missing or truncated (size zero).
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/snapshot_get.html>
|
||||
///
|
||||
/// - parameters:
|
||||
/// - path: The path to the database file.
|
||||
/// - configuration: A configuration.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public init(path: String, configuration: Configuration = Configuration()) throws {
|
||||
var configuration = Self.configure(configuration)
|
||||
|
||||
// Acquire and hold WAL snapshot
|
||||
var holderConfig = Configuration()
|
||||
holderConfig.allowsUnsafeTransactions = true
|
||||
snapshotHolder = try DatabaseQueue(path: path, configuration: holderConfig)
|
||||
let walSnapshot = try snapshotHolder.inDatabase { db in
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
|
||||
return try WALSnapshot(db)
|
||||
}
|
||||
|
||||
configuration.prepareDatabase { db in
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
|
||||
let code = sqlite3_snapshot_open(db.sqliteConnection, "main", walSnapshot.sqliteSnapshot)
|
||||
guard code == SQLITE_OK else {
|
||||
throw DatabaseError(resultCode: code)
|
||||
}
|
||||
}
|
||||
|
||||
self.configuration = configuration
|
||||
self.path = path
|
||||
self.walSnapshot = walSnapshot
|
||||
|
||||
var readerCount = 0
|
||||
readerPool = Pool(
|
||||
maximumCount: configuration.maximumReaderCount,
|
||||
qos: configuration.readQoS,
|
||||
makeElement: {
|
||||
readerCount += 1 // protected by Pool (TODO: document this protection behavior)
|
||||
return try SerializedDatabase(
|
||||
path: path,
|
||||
configuration: configuration,
|
||||
defaultLabel: "GRDB.DatabaseSnapshotPool",
|
||||
purpose: "snapshot.\(readerCount)")
|
||||
})
|
||||
}
|
||||
|
||||
private static func configure(_ configuration: Configuration) -> Configuration {
|
||||
var configuration = configuration
|
||||
|
||||
// DatabaseSnapshotPool needs a non-empty pool of connections.
|
||||
GRDBPrecondition(configuration.maximumReaderCount > 0, "configuration.maximumReaderCount must be at least 1")
|
||||
|
||||
// DatabaseSnapshotPool is read-only.
|
||||
configuration.readonly = true
|
||||
|
||||
// DatabaseSnapshotPool uses deferred transactions by default.
|
||||
// Other transaction kinds are forbidden by SQLite in read-only connections.
|
||||
configuration.defaultTransactionKind = .deferred
|
||||
|
||||
// DatabaseSnapshotPool keeps a long-lived transaction.
|
||||
configuration.allowsUnsafeTransactions = true
|
||||
|
||||
// DatabaseSnapshotPool requires the WAL mode.
|
||||
// See <https://www.sqlite.org/wal.html#sometimes_queries_return_sqlite_busy_in_wal_mode>
|
||||
if configuration.readonlyBusyMode == nil {
|
||||
configuration.readonlyBusyMode = .timeout(10)
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseSnapshotPool: @unchecked Sendable { }
|
||||
|
||||
extension DatabaseSnapshotPool: DatabaseSnapshotReader {
|
||||
public func close() throws {
|
||||
try readerPool?.barrier {
|
||||
defer { readerPool = nil }
|
||||
|
||||
try readerPool?.forEach { reader in
|
||||
try reader.sync { try $0.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func interrupt() {
|
||||
readerPool?.forEach { $0.interrupt() }
|
||||
}
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func read<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
|
||||
let (reader, releaseReader) = try readerPool.get()
|
||||
var completion: PoolCompletion!
|
||||
defer {
|
||||
releaseReader(completion)
|
||||
}
|
||||
return try reader.sync { db in
|
||||
do {
|
||||
let value = try value(db)
|
||||
completion = poolCompletion(db)
|
||||
return value
|
||||
} catch {
|
||||
completion = poolCompletion(db)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
value(.failure(DatabaseError.connectionIsClosed()))
|
||||
return
|
||||
}
|
||||
|
||||
readerPool.asyncGet { result in
|
||||
do {
|
||||
let (reader, releaseReader) = try result.get()
|
||||
// Second async jump because that's how `Pool.async` has to be used.
|
||||
reader.async { db in
|
||||
value(.success(db))
|
||||
releaseReader(self.poolCompletion(db))
|
||||
}
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
if let reader = currentReader {
|
||||
return try reader.reentrantSync { db in
|
||||
let result = try value(db)
|
||||
if snapshotIsLost(db) {
|
||||
throw DatabaseError(resultCode: .SQLITE_ABORT, message: "Snapshot is lost.")
|
||||
}
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
// There is no unsafe access to a snapshot.
|
||||
return try read(value)
|
||||
}
|
||||
}
|
||||
|
||||
public func _add<Reducer>(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
onChange: @escaping (Reducer.Value) -> Void)
|
||||
-> AnyDatabaseCancellable where Reducer: ValueReducer
|
||||
{
|
||||
_addReadOnly(observation: observation, scheduling: scheduler, onChange: onChange)
|
||||
}
|
||||
|
||||
/// Returns a reader that can be used from the current dispatch queue,
|
||||
/// if any.
|
||||
private var currentReader: SerializedDatabase? {
|
||||
guard let readerPool else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var readers: [SerializedDatabase] = []
|
||||
readerPool.forEach { reader in
|
||||
// We can't check for reader.onValidQueue here because
|
||||
// Pool.forEach() runs its closure argument in some arbitrary
|
||||
// dispatch queue. We thus extract the reader so that we can query
|
||||
// it below.
|
||||
readers.append(reader)
|
||||
}
|
||||
|
||||
// Now the readers array contains some readers. The pool readers may
|
||||
// already be different, because some other thread may have started
|
||||
// a new read, for example.
|
||||
//
|
||||
// This doesn't matter: the reader we are looking for is already on
|
||||
// its own dispatch queue. If it exists, is still in use, thus still
|
||||
// in the pool, and thus still relevant for our check:
|
||||
return readers.first { $0.onValidQueue }
|
||||
}
|
||||
|
||||
private func poolCompletion(_ db: Database) -> PoolCompletion {
|
||||
snapshotIsLost(db) ? .discard : .reuse
|
||||
}
|
||||
|
||||
private func snapshotIsLost(_ db: Database) -> Bool {
|
||||
do {
|
||||
let currentSnapshot = try WALSnapshot(db)
|
||||
if currentSnapshot.compare(walSnapshot) == 0 {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user