Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/Core/WALSnapshot.swift
T
2025-01-28 12:28:03 +08:00

95 lines
4.0 KiB
Swift

// swiftlint:disable:next line_length
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
/// An instance of WALSnapshot records the state of a WAL mode database for some
/// specific point in history.
///
/// We use `WALSnapshot` to help `ValueObservation` check for changes
/// that would happen between the initial fetch, and the start of the
/// actual observation. This class has no other purpose, and is not intended to
/// become public.
///
/// It does not work with SQLCipher, because SQLCipher does not support
/// `SQLITE_ENABLE_SNAPSHOT` correctly: we have linker errors.
/// See <https://github.com/ericsink/SQLitePCL.raw/issues/452>.
///
/// With custom SQLite builds, it only works if `SQLITE_ENABLE_SNAPSHOT`
/// is defined.
///
/// With system SQLite, it can only work when the SDK exposes the C apis and
/// their availability, which means XCode 14 (identified with Swift 5.7).
///
/// Yes, this is an awfully complex logic.
///
/// See <https://www.sqlite.org/c3ref/snapshot.html>.
final class WALSnapshot: @unchecked Sendable {
// @unchecked because sqlite3_snapshot has no threading requirements.
// <https://www.sqlite.org/c3ref/snapshot.html>
let sqliteSnapshot: UnsafeMutablePointer<sqlite3_snapshot>
init(_ db: Database) throws {
var sqliteSnapshot: UnsafeMutablePointer<sqlite3_snapshot>?
let code = withUnsafeMutablePointer(to: &sqliteSnapshot) {
return sqlite3_snapshot_get(db.sqliteConnection, "main", $0)
}
guard code == SQLITE_OK else {
// <https://www.sqlite.org/c3ref/snapshot_get.html>
//
// > The following must be true for sqlite3_snapshot_get() to succeed. [...]
// >
// > 1. The database handle must not be in autocommit mode.
// > 2. Schema S of database connection D must be a WAL
// > mode database.
// > 3. There must not be a write transaction open on schema S
// > of database connection D.
// > 4. One or more transactions must have been written to the
// > current wal file since it was created on disk (by any
// > connection). This means that a snapshot cannot be taken
// > on a wal mode database with no wal file immediately
// > after it is first opened. At least one transaction must
// > be written to it first.
// Test condition 1:
if sqlite3_get_autocommit(db.sqliteConnection) != 0 {
throw DatabaseError(resultCode: code, message: """
Can't create snapshot because database is in autocommit mode.
""")
}
// Test condition 2:
if let journalMode = try? String.fetchOne(db, sql: "PRAGMA journal_mode"),
journalMode != "wal"
{
throw DatabaseError(resultCode: code, message: """
Can't create snapshot because database is not in WAL mode.
""")
}
// Condition 3 can't happen because GRDB only calls this
// initializer from read transactions.
//
// Hence it is condition 4 that is false:
throw DatabaseError(resultCode: code, message: """
Can't create snapshot from a missing or empty wal file.
""")
}
guard let sqliteSnapshot else {
throw DatabaseError(resultCode: .SQLITE_INTERNAL) // WTF SQLite?
}
self.sqliteSnapshot = sqliteSnapshot
}
deinit {
sqlite3_snapshot_free(sqliteSnapshot)
}
/// Compares two WAL snapshots.
///
/// `a.compare(b) < 0` iff a is older than b.
///
/// See <https://www.sqlite.org/c3ref/snapshot_cmp.html>.
func compare(_ other: WALSnapshot) -> CInt {
sqlite3_snapshot_cmp(sqliteSnapshot, other.sqliteSnapshot)
}
}
#endif