95 lines
3.5 KiB
Swift
95 lines
3.5 KiB
Swift
// swiftlint:disable:next line_length
|
|
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
|
/// A long-live read-only WAL transaction.
|
|
///
|
|
/// `WALSnapshotTransaction` **takes ownership** of its reader
|
|
/// `SerializedDatabase` (TODO: make it a move-only type eventually).
|
|
class WALSnapshotTransaction {
|
|
private let reader: SerializedDatabase
|
|
private let release: (_ isInsideTransaction: Bool) -> Void
|
|
|
|
/// The state of the database at the beginning of the transaction.
|
|
let walSnapshot: WALSnapshot
|
|
|
|
/// Creates a long-live WAL transaction on a read-only connection.
|
|
///
|
|
/// The `release` closure is always called. It is called when the
|
|
/// `WALSnapshotTransaction` is deallocated, or if the initializer
|
|
/// throws.
|
|
///
|
|
/// In normal operations, the argument to `release` is always false,
|
|
/// meaning that the connection is no longer in a transaction. If true,
|
|
/// the connection has been left inside a transaction, due to
|
|
/// some error.
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// ```swift
|
|
/// let transaction = WALSnapshotTransaction(
|
|
/// reader: reader,
|
|
/// release: { isInsideTransaction in
|
|
/// ...
|
|
/// })
|
|
/// ```
|
|
///
|
|
/// - parameter reader: A read-only database connection.
|
|
/// - parameter release: A closure to call when the read-only connection
|
|
/// is no longer used.
|
|
init(
|
|
onReader reader: SerializedDatabase,
|
|
release: @escaping (_ isInsideTransaction: Bool) -> Void)
|
|
throws
|
|
{
|
|
assert(reader.configuration.readonly)
|
|
|
|
do {
|
|
// Open a long-lived transaction, and enter snapshot isolation
|
|
self.walSnapshot = try reader.sync(allowingLongLivedTransaction: true) { db in
|
|
try db.beginTransaction(.deferred)
|
|
// This also acquires snapshot isolation because checking
|
|
// database schema performs a read access.
|
|
try db.clearSchemaCacheIfNeeded()
|
|
return try WALSnapshot(db)
|
|
}
|
|
self.reader = reader
|
|
self.release = release
|
|
} catch {
|
|
// self is not initialized, so deinit will not run.
|
|
Self.commitAndRelease(reader: reader, release: release)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
Self.commitAndRelease(reader: reader, release: release)
|
|
}
|
|
|
|
/// Executes database operations in the snapshot transaction, and
|
|
/// returns their result after they have finished executing.
|
|
func read<T>(_ value: (Database) throws -> T) rethrows -> T {
|
|
// We should check the validity of the snapshot, as DatabaseSnapshotPool does.
|
|
try reader.sync(value)
|
|
}
|
|
|
|
/// Schedules database operations for execution, and
|
|
/// returns immediately.
|
|
func asyncRead(_ value: @escaping (Database) -> Void) {
|
|
// We should check the validity of the snapshot, as DatabaseSnapshotPool does.
|
|
reader.async(value)
|
|
}
|
|
|
|
private static func commitAndRelease(
|
|
reader: SerializedDatabase,
|
|
release: (_ isInsideTransaction: Bool) -> Void)
|
|
{
|
|
// WALSnapshotTransaction may be deinitialized in the dispatch
|
|
// queue of its reader: allow reentrancy.
|
|
let isInsideTransaction = reader.reentrantSync(allowingLongLivedTransaction: false) { db in
|
|
try? db.commit()
|
|
return db.isInsideTransaction
|
|
}
|
|
release(isInsideTransaction)
|
|
}
|
|
}
|
|
#endif
|