// 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: /// /// - /// - /// /// ## 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? /// 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: /// /// - 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: /// /// - 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 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(_ 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) -> 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(_ 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( observation: ValueObservation, 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