Files
UUVPN/iOS-SwiftUI-Code/GRDB.swift-6.29.3/GRDB/Core/Database.swift
T
2025-01-22 14:09:10 +08:00

2231 lines
83 KiB
Swift

import Foundation
/// A raw SQLite connection, suitable for the SQLite C API.
public typealias SQLiteConnection = OpaquePointer
/// A raw SQLite function argument.
typealias SQLiteValue = OpaquePointer
let SQLITE_TRANSIENT = unsafeBitCast(OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self)
/// An SQLite connection.
///
/// You don't create `Database` instances directly. Instead, you connect to a
/// database with one of the <doc:DatabaseConnections>, and you use a database
/// access method. For example:
///
/// ```swift
/// let dbQueue = try DatabaseQueue()
///
/// try dbQueue.write { (db: Database) in
/// try Player(name: "Arthur").insert(db)
/// }
/// ```
///
/// `Database` methods that modify, query, or validate the database schema are
/// listed in <doc:DatabaseSchema>.
///
/// ## Topics
///
/// ### Database Information
///
/// - ``changesCount``
/// - ``configuration``
/// - ``debugDescription``
/// - ``description``
/// - ``lastErrorCode``
/// - ``lastErrorMessage``
/// - ``lastInsertedRowID``
/// - ``maximumStatementArgumentCount``
/// - ``sqliteConnection``
/// - ``totalChangesCount``
/// - ``SQLiteConnection``
///
/// ### Database Statements
///
/// - ``allStatements(literal:)``
/// - ``allStatements(sql:arguments:)``
/// - ``cachedStatement(literal:)``
/// - ``cachedStatement(sql:)``
/// - ``execute(literal:)``
/// - ``execute(sql:arguments:)``
/// - ``makeStatement(literal:)``
/// - ``makeStatement(sql:)``
/// - ``SQLStatementCursor``
///
/// ### Database Transactions
///
/// - ``beginTransaction(_:)``
/// - ``commit()``
/// - ``inSavepoint(_:)``
/// - ``inTransaction(_:_:)``
/// - ``isInsideTransaction``
/// - ``readOnly(_:)``
/// - ``rollback()``
/// - ``transactionDate``
/// - ``TransactionCompletion``
/// - ``TransactionKind``
///
/// ### Printing Database Content
///
/// - ``dumpContent(format:to:)``
/// - ``dumpRequest(_:format:to:)``
/// - ``dumpSchema(to:)``
/// - ``dumpSQL(_:format:to:)``
/// - ``dumpTables(_:format:tableHeader:stableOrder:to:)``
/// - ``DumpFormat``
/// - ``DumpTableHeaderOptions``
///
/// ### Database Observation
///
/// - ``add(transactionObserver:extent:)``
/// - ``remove(transactionObserver:)``
/// - ``afterNextTransaction(onCommit:onRollback:)``
/// - ``notifyChanges(in:)``
/// - ``registerAccess(to:)``
///
/// ### Collations
///
/// - ``add(collation:)``
/// - ``reindex(collation:)-171fj``
/// - ``reindex(collation:)-2hxil``
/// - ``remove(collation:)``
/// - ``CollationName``
/// - ``DatabaseCollation``
///
/// ### SQL Functions
///
/// - ``add(function:)``
/// - ``remove(function:)``
/// - ``DatabaseFunction``
///
/// ### Notifications
///
/// - ``resumeNotification``
/// - ``suspendNotification``
///
/// ### Other Database Operations
///
/// - ``add(tokenizer:)``
/// - ``backup(to:pagesPerStep:progress:)``
/// - ``checkpoint(_:on:)``
/// - ``clearSchemaCache()``
/// - ``logError``
/// - ``releaseMemory()``
/// - ``trace(options:_:)``
/// - ``CheckpointMode``
/// - ``DatabaseBackupProgress``
/// - ``StorageClass``
/// - ``TraceEvent``
/// - ``TracingOptions``
public final class Database: CustomStringConvertible, CustomDebugStringConvertible {
// The Database class is not thread-safe. An instance should always be
// used through a SerializedDatabase.
// MARK: - SQLite C API
/// The raw SQLite connection, suitable for the SQLite C API.
///
/// The result is nil after the database has been successfully closed with
/// ``DatabaseReader/close()``.
public private(set) var sqliteConnection: SQLiteConnection?
// MARK: - Configuration
/// The error logging function.
///
/// Related SQLite documentation: <https://www.sqlite.org/errlog.html>
public static var logError: LogErrorFunction? = nil {
didSet {
if logError != nil {
_registerErrorLogCallback { (_, code, message) in
guard let logError = Database.logError else { return }
guard let message = message.map(String.init) else { return }
let resultCode = ResultCode(rawValue: code)
logError(resultCode, message)
}
} else {
_registerErrorLogCallback(nil)
}
}
}
/// The database configuration.
public let configuration: Configuration
/// A description of this database connection.
///
/// The returned string is based on the ``Configuration/label``
/// of ``configuration``.
public let description: String
public var debugDescription: String { "<Database: \(description)>" }
// MARK: - Database Information
/// The rowID of the most recently inserted row.
///
/// If no row has ever been inserted using this database connection,
/// the last inserted rowID is zero.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/last_insert_rowid.html>
public var lastInsertedRowID: Int64 {
SchedulingWatchdog.preconditionValidQueue(self)
return sqlite3_last_insert_rowid(sqliteConnection)
}
/// The number of rows modified, inserted or deleted by the most recent
/// successful INSERT, UPDATE or DELETE statement.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/changes.html>
public var changesCount: Int {
SchedulingWatchdog.preconditionValidQueue(self)
return Int(sqlite3_changes(sqliteConnection))
}
/// The total number of rows modified, inserted or deleted by all successful
/// INSERT, UPDATE or DELETE statements since the database connection was
/// opened.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/total_changes.html>
public var totalChangesCount: Int {
SchedulingWatchdog.preconditionValidQueue(self)
return Int(sqlite3_total_changes(sqliteConnection))
}
/// A Boolean value indicating whether the database connection is currently
/// inside a transaction.
///
/// A database is inside a transaction if and only if it is not in the
/// autocommit mode. See <https://sqlite.org/c3ref/get_autocommit.html>.
public var isInsideTransaction: Bool {
// https://sqlite.org/c3ref/get_autocommit.html
//
// > The sqlite3_get_autocommit() interface returns non-zero or zero if
// > the given database connection is or is not in autocommit mode,
// > respectively.
//
// > Autocommit mode is on by default. Autocommit mode is disabled by a
// > BEGIN statement. Autocommit mode is re-enabled by a COMMIT
// > or ROLLBACK.
//
// > If another thread changes the autocommit status of the database
// > connection while this routine is running, then the return value
// > is undefined.
SchedulingWatchdog.preconditionValidQueue(self)
if sqliteConnection == nil { return false } // Support for SerializedDatabase.deinit
return sqlite3_get_autocommit(sqliteConnection) == 0
}
/// The last error code.
public var lastErrorCode: ResultCode { ResultCode(rawValue: sqlite3_errcode(sqliteConnection)) }
/// The last error message.
public var lastErrorMessage: String? { String(cString: sqlite3_errmsg(sqliteConnection)) }
// MARK: - Internal properties
let path: String
/// Support for schema changes performed with ``DatabasePool``: each read
/// access needs to clear the schema cache if the schema has been modified
/// by the writer connection since the previous read. This property is reset
/// to `PRAGMA schema_version` at the beginning of each DatabasePool read.
///
/// DatabasePool writer connection and DatabaseQueue do not perform such
/// automatic schema management: they won't clear their schema cache if an
/// external connection modifies the schema.
///
/// See `clearSchemaCacheIfNeeded()`.
var lastSchemaVersion: Int32?
/// The cache for the available database schemas (main, temp, attached databases).
var schemaCache = SchemaCache()
/// The cache for statements managed by GRDB. It is distinct from
/// `publicStatementCache` so that we do not mess with statement arguments
/// set by the user.
lazy var internalStatementCache = StatementCache(database: self)
/// The cache for statements managed by the user.
lazy var publicStatementCache = StatementCache(database: self)
/// The database authorizer provides information about compiled
/// database statements, and prevents the truncate optimization when
/// row deletions are observed by transaction observers.
lazy var authorizer = StatementAuthorizer(self)
/// The observation broker supports database observation and
/// transaction observers.
///
/// It is nil in read-only connections, because we do not report read-only
/// transactions to transaction observers.
private(set) var observationBroker: DatabaseObservationBroker?
/// The list of compile options used when building SQLite
static func sqliteCompileOptions() throws -> Set<String> {
try DatabaseQueue().inDatabase {
try Set(String.fetchCursor($0, sql: "PRAGMA COMPILE_OPTIONS"))
}
}
/// Whether the database region selected by statement execution is
/// recorded into `selectedRegion` by `track(_:)`.
///
/// To start recording the selected region, use `recordingSelection(_:_:)`.
private(set) var isRecordingSelectedRegion = false
/// The database region selected by statement execution, when
/// `isRecordingSelectedRegion` is true.
var selectedRegion = DatabaseRegion()
/// Support for `checkForAbortedTransaction()`
var isInsideTransactionBlock = false
/// Support for `checkForSuspensionViolation(from:)`
@LockedBox var isSuspended = false
/// Support for `checkForSuspensionViolation(from:)`
/// This cache is never cleared: we assume journal mode never changes.
var journalModeCache: String?
// MARK: - Transaction Date
enum AutocommitState {
case off
case on
}
/// The state of the auto-commit mode, as left by the last
/// executed statement.
///
/// The goal of this property is to detect changes in the auto-commit mode.
/// When you need to know if the database is currently in the auto-commit
/// mode, always prefer ``isInsideTransaction``.
var autocommitState = AutocommitState.on
/// The date of the current transaction, wrapped in a result that is an
/// error if there was an error grabbing this date when the transaction has
/// started.
///
/// Invariant: `transactionDateResult` is nil iff connection is not
/// inside a transaction.
var transactionDateResult: Result<Date, Error>?
/// The date of the current transaction.
///
/// The returned date is constant at any point during a transaction. It is
/// set when the database leaves the
/// [autocommit mode](https://www.sqlite.org/c3ref/get_autocommit.html) with
/// a `BEGIN` statement.
///
/// When the database is not currently in a transaction, a new date is
/// returned on each call.
///
/// See <doc:RecordTimestamps> for an example of usage.
///
/// The transaction date, by default, is the start date of the current
/// transaction. You can override this default behavior by configuring
/// ``Configuration/transactionClock``.
public var transactionDate: Date {
get throws {
SchedulingWatchdog.preconditionValidQueue(self)
// Check invariant: `transactionDateResult` is nil iff connection
// is not inside a transaction.
assert(isInsideTransaction || transactionDateResult == nil)
if let transactionDateResult {
return try transactionDateResult.get()
} else {
return try configuration.transactionClock.now(self)
}
}
}
// MARK: - Private properties
/// Support for ``Configuration/busyMode``.
private var busyCallback: BusyCallback?
/// Support for ``trace(options:_:)``.
private var trace: ((TraceEvent) -> Void)?
/// The registered custom SQL functions.
private var functions = Set<DatabaseFunction>()
/// The registered custom SQL collations.
private var collations = Set<DatabaseCollation>()
/// Support for `beginReadOnly()` and `endReadOnly()`.
private var readOnlyDepth = 0
// MARK: - Initializer
init(
path: String,
description: String,
configuration: Configuration) throws
{
self.sqliteConnection = try Database.openConnection(path: path, flags: configuration.SQLiteOpenFlags)
self.description = description
self.configuration = configuration
self.path = path
// We do not report read-only transactions to transaction observers, so
// don't bother installing the observation broker for read-only connections.
if !configuration.readonly {
observationBroker = DatabaseObservationBroker(self)
}
}
deinit {
assert(sqliteConnection == nil)
}
// MARK: - Database Opening
private static func openConnection(path: String, flags: CInt) throws -> SQLiteConnection {
// See <https://www.sqlite.org/c3ref/open.html>
var sqliteConnection: SQLiteConnection? = nil
let code = sqlite3_open_v2(path, &sqliteConnection, flags, nil)
guard code == SQLITE_OK else {
// https://www.sqlite.org/c3ref/open.html
// > Whether or not an error occurs when it is opened, resources
// > associated with the database connection handle should be
// > released by passing it to sqlite3_close() when it is no
// > longer required.
//
// https://www.sqlite.org/c3ref/close.html
// > Calling sqlite3_close() or sqlite3_close_v2() with a NULL
// > pointer argument is a harmless no-op.
_ = sqlite3_close(sqliteConnection) // ignore result code
throw DatabaseError(resultCode: code)
}
guard let sqliteConnection else {
throw DatabaseError(resultCode: .SQLITE_INTERNAL) // WTF SQLite?
}
return sqliteConnection
}
// MARK: - Database Setup
/// This method must be called after database initialization
func setUp() throws {
setupBusyMode()
setupDoubleQuotedStringLiterals()
try setupForeignKeys()
setupDefaultFunctions()
setupDefaultCollations()
setupAuthorizer()
observationBroker?.installCommitAndRollbackHooks()
try activateExtendedCodes()
#if SQLITE_HAS_CODEC
try validateSQLCipher()
#endif
// Last step before we can start accessing the database.
try configuration.setUp(self)
try validateFormat()
configuration.SQLiteConnectionDidOpen?()
}
/// Performs ``Configuration/JournalModeConfiguration/wal``.
func setUpWALMode() throws {
let journalMode = try String.fetchOne(self, sql: "PRAGMA journal_mode = WAL")
guard journalMode == "wal" else {
throw DatabaseError(message: "could not activate WAL Mode at path: \(path)")
}
// https://www.sqlite.org/pragma.html#pragma_synchronous
// > Many applications choose NORMAL when in WAL mode
try execute(sql: "PRAGMA synchronous = NORMAL")
// Make sure a non-empty wal file exists.
//
// The presence of the wal file avoids an SQLITE_CANTOPEN (14)
// error when the user opens a pool and reads from it.
// See <https://github.com/groue/GRDB.swift/issues/102>.
//
// The non-empty wal file avoids an SQLITE_ERROR (1) error
// when the user opens a pool and creates a wal snapshot
// (which happens when starting a ValueObservation).
// See <https://github.com/groue/GRDB.swift/issues/1383>.
let walPath = path + "-wal"
if try FileManager.default.fileExists(atPath: walPath) == false
|| (URL(fileURLWithPath: walPath).resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0) == 0
{
try inSavepoint {
try execute(sql: """
CREATE TABLE grdb_issue_102 (id INTEGER PRIMARY KEY);
DROP TABLE grdb_issue_102;
""")
return .commit
}
}
}
private func setupDoubleQuotedStringLiterals() {
if configuration.acceptsDoubleQuotedStringLiterals {
_enableDoubleQuotedStringLiterals(sqliteConnection)
} else {
_disableDoubleQuotedStringLiterals(sqliteConnection)
}
}
private func setupForeignKeys() throws {
// Foreign keys are disabled by default with SQLite3
if configuration.foreignKeysEnabled {
try execute(sql: "PRAGMA foreign_keys = ON")
}
}
private func setupBusyMode() {
let busyMode = configuration.readonly
? configuration.readonlyBusyMode ?? configuration.busyMode
: configuration.busyMode
switch busyMode {
case .immediateError:
break
case .timeout(let duration):
let milliseconds = CInt(duration * 1000)
sqlite3_busy_timeout(sqliteConnection, milliseconds)
case .callback(let callback):
busyCallback = callback
let dbPointer = Unmanaged.passUnretained(self).toOpaque()
sqlite3_busy_handler(
sqliteConnection,
{ (dbPointer, numberOfTries) in
let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue()
let callback = db.busyCallback!
return callback(Int(numberOfTries)) ? 1 : 0
},
dbPointer)
}
}
private func setupDefaultFunctions() {
add(function: .capitalize)
add(function: .lowercase)
add(function: .uppercase)
add(function: .localizedCapitalize)
add(function: .localizedLowercase)
add(function: .localizedUppercase)
}
private func setupDefaultCollations() {
add(collation: .unicodeCompare)
add(collation: .caseInsensitiveCompare)
add(collation: .localizedCaseInsensitiveCompare)
add(collation: .localizedCompare)
add(collation: .localizedStandardCompare)
}
private func setupAuthorizer() {
// SQLite authorizer is set only once per database connection.
//
// This is because authorizer changes have SQLite invalidate statements,
// with undesired side effects. See:
//
// - DatabaseCursorTests.testIssue583()
// - http://sqlite.1065341.n5.nabble.com/Issue-report-sqlite3-set-authorizer-triggers-error-4-516-SQLITE-ABORT-ROLLBACK-during-statement-itern-td107972.html
authorizer.register()
}
private func activateExtendedCodes() throws {
if (configuration.SQLiteOpenFlags & 0x02000000 /* SQLITE_OPEN_EXRESCODE */) != 0 {
// Nothing to do
return
}
let code = sqlite3_extended_result_codes(sqliteConnection, 1)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}
#if SQLITE_HAS_CODEC
private func validateSQLCipher() throws {
// https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
//
// > In order to avoid situations where SQLite might be used
// > improperly at runtime, we strongly recommend that
// > applications institute a runtime test to ensure that the
// > application is actually using SQLCipher on the active
// > connection.
if try String.fetchOne(self, sql: "PRAGMA cipher_version") == nil {
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: """
GRDB is not linked against SQLCipher. \
Check https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
""")
}
}
#endif
private func validateFormat() throws {
// Users are surprised when they open a picture as a database and
// see no error (https://github.com/groue/GRDB.swift/issues/54).
//
// So let's fail early if file is not a database, or encrypted with
// another passphrase.
try makeStatement(sql: "SELECT * FROM sqlite_master LIMIT 1").makeCursor().next()
}
// MARK: - Database Closing
/// Closes a connection with `sqlite3_close`. This method is intended for
/// the public `close()` function. It may fail.
func close() throws {
SchedulingWatchdog.preconditionValidQueue(self)
guard let sqliteConnection else {
// Already closed
return
}
configuration.SQLiteConnectionWillClose?(sqliteConnection)
// Finalize all cached statements since they would prevent
// immediate connection closing.
internalStatementCache.clear()
publicStatementCache.clear()
// https://www.sqlite.org/c3ref/close.html
// > If the database connection is associated with unfinalized prepared
// > statements or unfinished sqlite3_backup objects then
// > sqlite3_close() will leave the database connection open and
// > return SQLITE_BUSY.
let code = sqlite3_close(sqliteConnection)
guard code == SQLITE_OK else {
// So there remain some unfinalized prepared statement somewhere.
if let log = Self.logError {
if code == SQLITE_BUSY {
// Let the user know about unfinalized statements that did
// prevent the connection from closing properly.
var stmt: SQLiteStatement? = sqlite3_next_stmt(sqliteConnection, nil)
while stmt != nil {
log(ResultCode(rawValue: code), "unfinalized statement: \(String(cString: sqlite3_sql(stmt)))")
stmt = sqlite3_next_stmt(sqliteConnection, stmt)
}
}
}
throw DatabaseError(resultCode: code, message: lastErrorMessage)
}
self.sqliteConnection = nil
configuration.SQLiteConnectionDidClose?()
}
/// Closes a connection with `sqlite3_close_v2`. This method is intended for
/// deallocated connections.
func close_v2() {
SchedulingWatchdog.preconditionValidQueue(self)
guard let sqliteConnection else {
// Already closed
return
}
configuration.SQLiteConnectionWillClose?(sqliteConnection)
// Finalize all cached statements since they would prevent
// immediate connection closing.
internalStatementCache.clear()
publicStatementCache.clear()
// https://www.sqlite.org/c3ref/close.html
// > If sqlite3_close_v2() is called with unfinalized prepared
// > statements and/or unfinished sqlite3_backups, then the database
// > connection becomes an unusable "zombie" which will automatically
// > be deallocated when the last prepared statement is finalized or the
// > last sqlite3_backup is finished.
// >
// > The sqlite3_close_v2() interface is intended for use with host
// > languages that are garbage collected, and where the order in which
// > destructors are called is arbitrary.
let code = sqlite3_close_v2(sqliteConnection)
if code != SQLITE_OK, let log = Self.logError {
// A rare situation where GRDB doesn't fatalError on
// unprocessed errors.
let message = String(cString: sqlite3_errmsg(sqliteConnection))
log(ResultCode(rawValue: code), "could not close database: \(message)")
}
self.sqliteConnection = nil
configuration.SQLiteConnectionDidClose?()
}
// MARK: - Limits
/// The maximum number of arguments accepted by an SQLite statement.
///
/// For example, requests such as the one below must make sure the `ids`
/// array does not contain more than `maximumStatementArgumentCount`
/// elements:
///
/// ```swift
/// // DELETE FROM player WHERE id IN (?, ?, ...)
/// let ids: [Int] = ...
/// try dbQueue.write { db in
/// try Player.deleteAll(db, keys: ids)
/// }
/// ```
///
/// Related SQLite documentation: see `SQLITE_LIMIT_VARIABLE_NUMBER` in
/// <https://www.sqlite.org/limits.html>.
public var maximumStatementArgumentCount: Int {
Int(sqlite3_limit(sqliteConnection, SQLITE_LIMIT_VARIABLE_NUMBER, -1))
}
// MARK: - Functions
/// Adds or redefines a custom SQL function.
///
/// When you want to add a function to all connections created by a
/// ``DatabasePool``, add the function in
/// ``Configuration/prepareDatabase(_:)``:
///
/// ```swift
/// var config = Configuration()
/// config.prepareDatabase { db in
/// // Add the function to both writer and readers connections.
/// db.add(function: ...)
/// }
/// let dbPool = try DatabasePool(path: ..., configuration: config)
/// ```
public func add(function: DatabaseFunction) {
functions.update(with: function)
function.install(in: self)
}
/// Removes a custom SQL function.
public func remove(function: DatabaseFunction) {
functions.remove(function)
function.uninstall(in: self)
}
// MARK: - Collations
/// Adds or redefines a collation.
///
/// When you want to add a collation to all connections created by a
/// ``DatabasePool``, add the collation in
/// ``Configuration/prepareDatabase(_:)``:
///
/// ```swift
/// var config = Configuration()
/// config.prepareDatabase { db in
/// // Add the collation to both writer and readers connections.
/// db.add(collation: ...)
/// }
/// let dbPool = try DatabasePool(path: ..., configuration: config)
/// ```
public func add(collation: DatabaseCollation) {
collations.update(with: collation)
let collationPointer = Unmanaged.passUnretained(collation).toOpaque()
let code = sqlite3_create_collation_v2(
sqliteConnection,
collation.name,
SQLITE_UTF8,
collationPointer,
{ (collationPointer, length1, buffer1, length2, buffer2) in
let collation = Unmanaged<DatabaseCollation>.fromOpaque(collationPointer!).takeUnretainedValue()
return CInt(collation.function(length1, buffer1, length2, buffer2).rawValue)
}, nil)
guard code == SQLITE_OK else {
// Assume a GRDB bug: there is no point throwing any error.
fatalError(DatabaseError(resultCode: code, message: lastErrorMessage))
}
}
/// Removes a collation.
public func remove(collation: DatabaseCollation) {
collations.remove(collation)
sqlite3_create_collation_v2(
sqliteConnection,
collation.name,
SQLITE_UTF8,
nil, nil, nil)
}
// MARK: - Read-Only Access
/// MUST be balanced with `endReadOnly()`.
func beginReadOnly() throws {
if configuration.readonly { return }
if readOnlyDepth == 0 {
try internalCachedStatement(sql: "PRAGMA query_only = 1").execute()
}
readOnlyDepth += 1
}
/// MUST balance `beginReadOnly()`.
func endReadOnly() throws {
if configuration.readonly { return }
readOnlyDepth -= 1
assert(readOnlyDepth >= 0, "unbalanced endReadOnly()")
if readOnlyDepth == 0 {
try internalCachedStatement(sql: "PRAGMA query_only = 0").execute()
}
}
/// Executes read-only database operations, and returns their result
/// after they have finished executing.
///
/// Attempts to write throw a ``DatabaseError`` with
/// resultCode `SQLITE_READONLY`.
///
/// For example:
///
/// ```swift
/// try dbQueue.write do { db in
/// // Write OK
/// try Player(...).insert(db)
///
/// try db.readOnly {
/// // Read OK
/// let players = try Player.fetchAll(db)
///
/// // Throws SQLITE_READONLY
/// try Player(...).insert(db)
/// }
/// }
/// ```
///
/// This method is reentrant.
///
/// - parameter value: A closure that reads from the database.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the
/// error thrown by `value`.
public func readOnly<T>(_ value: () throws -> T) throws -> T {
try beginReadOnly()
return try throwingFirstError(
execute: value,
finally: endReadOnly)
}
/// Returns whether database connection is read-only (due to
/// `SQLITE_OPEN_READONLY` or `PRAGMA query_only=1`).
var isReadOnly: Bool {
readOnlyDepth > 0 || configuration.readonly
}
// MARK: - Database Observation
/// Reports the database region to ``ValueObservation``.
///
/// Calling this method does not fetch any database values. It just
/// helps optimizing `ValueObservation`. See
/// ``ValueObservation/trackingConstantRegion(_:)`` for more
/// information, and some examples of usage.
///
/// For example:
///
/// ```swift
/// let observation = ValueObservation.tracking { db in
/// // All changes to the 'player' and 'team' tables
/// // will trigger the observation.
/// try db.registerAccess(to: Player.all())
/// try db.registerAccess(to: Team.all())
/// }
/// ```
///
/// This method has no effect on a `ValueObservation` created with
/// ``ValueObservation/tracking(regions:fetch:)``. In the example below,
/// only the `player` table is tracked:
///
/// ```swift
/// // Observes the 'player' table only
/// let observation = ValueObservation.tracking(region: Player.all()) { db in
/// // Ignored
/// try db.registerAccess(to: Team.all())
/// }
/// ```
public func registerAccess(to region: @autoclosure () -> some DatabaseRegionConvertible) throws {
if isRecordingSelectedRegion {
try selectedRegion.formUnion(region().databaseRegion(self))
}
}
/// Notifies that some changes were performed in the provided
/// database region.
///
/// This method makes it possible to notify undetected changes, such as
/// changes performed by another process, changes performed by
/// direct calls to SQLite C functions, or changes to the
/// database schema.
/// See <doc:GRDB/TransactionObserver#Dealing-with-Undetected-Changes>
/// for a detailed list of undetected database modifications.
///
/// It triggers active transaction observers (``TransactionObserver``).
/// In particular, ``ValueObservation`` that observe the input `region`
/// will fetch and notify a fresh value.
///
/// For example:
///
/// ```swift
/// try dbQueue.write { db in
/// // Notify observers that some changes were performed in the database
/// try db.notifyChanges(in: .fullDatabase)
///
/// // Notify observers that some changes were performed in the player table
/// try db.notifyChanges(in: Player.all())
///
/// // Equivalent alternative
/// try db.notifyChanges(in: Table("player"))
/// }
/// ```
///
/// This method has no effect when called from a read-only
/// database access.
///
/// > Caveat: Individual rowids in the input region are ignored.
/// > Notifying a change to a specific rowid is the same as notifying a
/// > change in the whole table:
/// >
/// > ```swift
/// > try dbQueue.write { db in
/// > // Equivalent
/// > try db.notifyChanges(in: Player.all())
/// > try db.notifyChanges(in: Player.filter(id: 1))
/// > }
/// > ```
public func notifyChanges(in region: some DatabaseRegionConvertible) throws {
// Don't do anything when read-only, because read-only transactions
// are not notified. We don't want to notify transactions observers
// of changes, and have them wait for a commit notification that
// will never come.
if !isReadOnly, let observationBroker {
let eventKinds = try region
.databaseRegion(self)
// Use canonical table names for case insensitivity of the input.
.canonicalTables(self)
.impactfulEventKinds(self)
try observationBroker.notifyChanges(withEventsOfKind: eventKinds)
}
}
/// Extends the `region` argument with the database region selected by all
/// statements executed by the closure, and all regions explicitly tracked
/// with the ``registerAccess(to:)`` method.
///
/// For example:
///
/// ```swift
/// var region = DatabaseRegion()
/// try db.recordingSelection(&region) {
/// let players = try Player.fetchAll(db)
/// let team = try Team.fetchOne(db, id: 42)
/// try db.registerAccess(to: Table("awards"))
/// }
/// print(region) // awards,player(*),team(*)[42]
/// ```
///
/// This method is used by ``ValueObservation``:
///
/// ```swift
/// let playersObservation = ValueObservation.tracking { db in
/// // Here all fetches are recorded, so that we know what is the
/// // database region that must be observed.
/// try Player.fetchAll(db)
/// }
/// ```
func recordingSelection<T>(_ region: inout DatabaseRegion, _ block: () throws -> T) rethrows -> T {
if region.isFullDatabase {
return try block()
}
let oldFlag = self.isRecordingSelectedRegion
let oldRegion = self.selectedRegion
isRecordingSelectedRegion = true
selectedRegion = DatabaseRegion()
defer {
region.formUnion(selectedRegion)
isRecordingSelectedRegion = oldFlag
if isRecordingSelectedRegion {
selectedRegion = oldRegion.union(selectedRegion)
} else {
selectedRegion = oldRegion
}
}
return try block()
}
// MARK: - Trace
/// Registers a tracing function.
///
/// For example:
///
/// ```swift
/// // Trace all SQL statements executed by the database
/// var config = Configuration()
/// config.prepareDatabase { db in
/// db.trace(options: .statement) { event in
/// print("SQL: \(event)")
/// }
/// }
/// let dbQueue = try DatabaseQueue(path: ..., configuration: config)
/// ```
///
/// Pass an empty options set in order to stop database tracing:
///
/// ```swift
/// // Stop tracing
/// db.trace(options: [])
/// ```
///
/// If you want to see statement arguments in the traced events, you will
/// need to set the ``Configuration/publicStatementArguments`` flag in the
/// database ``configuration``.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/trace_v2.html>
///
/// - parameter options: The set of desired event kinds. Defaults to
/// `.statement`, which notifies all executed database statements.
/// - parameter trace: the tracing function.
public func trace(options: TracingOptions = .statement, _ trace: ((TraceEvent) -> Void)? = nil) {
SchedulingWatchdog.preconditionValidQueue(self)
self.trace = trace
if options.isEmpty || trace == nil {
sqlite3_trace_v2(sqliteConnection, 0, nil, nil)
return
}
let dbPointer = Unmanaged.passUnretained(self).toOpaque()
sqlite3_trace_v2(sqliteConnection, CUnsignedInt(bitPattern: options.rawValue), { (mask, dbPointer, p, x) in
let db = Unmanaged<Database>.fromOpaque(dbPointer!).takeUnretainedValue()
db.trace_v2(CInt(bitPattern: mask), p, x, sqlite3_expanded_sql)
return SQLITE_OK
}, dbPointer)
}
// Precondition: configuration.trace != nil
private func trace_v2(
_ mask: CInt,
_ p: UnsafeMutableRawPointer?,
_ x: UnsafeMutableRawPointer?,
_ sqlite3_expanded_sql: @escaping @convention(c) (OpaquePointer?) -> UnsafeMutablePointer<CChar>?)
{
guard let trace else { return }
switch mask {
case SQLITE_TRACE_STMT:
if let sqliteStatement = p, let unexpandedSQL = x {
let statement = TraceEvent.Statement(
sqliteStatement: OpaquePointer(sqliteStatement),
unexpandedSQL: UnsafePointer(unexpandedSQL.assumingMemoryBound(to: CChar.self)),
sqlite3_expanded_sql: sqlite3_expanded_sql,
publicStatementArguments: configuration.publicStatementArguments)
trace(TraceEvent.statement(statement))
}
case SQLITE_TRACE_PROFILE:
if let sqliteStatement = p, let durationP = x?.assumingMemoryBound(to: Int64.self) {
let statement = TraceEvent.Statement(
sqliteStatement: OpaquePointer(sqliteStatement),
unexpandedSQL: nil,
sqlite3_expanded_sql: sqlite3_expanded_sql,
publicStatementArguments: configuration.publicStatementArguments)
let duration = TimeInterval(durationP.pointee) / 1.0e9
trace(TraceEvent.profile(statement: statement, duration: duration))
}
default:
break
}
}
// MARK: - WAL Checkpoints
/// Runs a WAL checkpoint.
///
/// Related SQLite documentation:
/// - <https://www.sqlite.org/wal.html>
/// - <https://www.sqlite.org/c3ref/wal_checkpoint_v2.html>
///
/// - parameter kind: The checkpoint mode (default passive)
/// - parameter dbName: The database name (default "main")
/// - returns: A tuple where `walFrameCount` is the total number of frames
/// in the log file and `checkpointedFrameCount` is the total number of
/// checkpointed frames in the log file
@discardableResult
public func checkpoint(_ kind: Database.CheckpointMode = .passive, on dbName: String? = "main") throws
-> (walFrameCount: Int, checkpointedFrameCount: Int)
{
SchedulingWatchdog.preconditionValidQueue(self)
var walFrameCount: CInt = -1
var checkpointedFrameCount: CInt = -1
let code = sqlite3_wal_checkpoint_v2(sqliteConnection, dbName, kind.rawValue,
&walFrameCount, &checkpointedFrameCount)
switch code {
case SQLITE_OK:
return (walFrameCount: Int(walFrameCount), checkpointedFrameCount: Int(checkpointedFrameCount))
case SQLITE_MISUSE:
throw DatabaseError(resultCode: code)
default:
throw DatabaseError(resultCode: code, message: lastErrorMessage)
}
}
// MARK: - Interrupt
// See <https://www.sqlite.org/c3ref/interrupt.html>
func interrupt() {
sqlite3_interrupt(sqliteConnection)
}
// MARK: - Database Suspension
/// When this notification is posted, databases which were opened with the
/// ``Configuration/observesSuspensionNotifications`` configuration flag
/// are suspended.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// A suspended database makes everything to avoid acquiring a lock on the
/// database. All database operations may throw a ``DatabaseError`` of code
/// `SQLITE_INTERRUPT` or `SQLITE_ABORT`, except reads in WAL mode.
///
/// See <doc:DatabaseSharing#How-to-limit-the-0xDEAD10CC-exception> for
/// more information.
public static let suspendNotification = Notification.Name("GRDB.Database.Suspend")
/// When this notification is posted, databases which were opened with the
/// ``Configuration/observesSuspensionNotifications`` configuration flag
/// are resumed.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
public static let resumeNotification = Notification.Name("GRDB.Database.Resume")
/// Suspends the database. A suspended database prevents database locks in
/// order to avoid the [`0xdead10cc`
/// exception](https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report).
///
/// This method can be called from any thread.
///
/// During suspension, any lock is released as soon as possible, and
/// lock acquisition is prevented. All database accesses may throw a
/// DatabaseError of code `SQLITE_INTERRUPT`, or `SQLITE_ABORT`, except
/// reads in WAL mode.
///
/// Suspension ends with `resume()`.
func suspend() {
$isSuspended.update { isSuspended in
if isSuspended {
return
}
// Prevent future lock acquisition
isSuspended = true
// Interrupt the database because this may trigger an
// SQLITE_INTERRUPT error which may itself abort a transaction and
// release a lock. See <https://www.sqlite.org/c3ref/interrupt.html>
interrupt()
// Now what about the eventual remaining lock? We'll issue a
// rollback on next database access which requires a lock, in
// checkForSuspensionViolation(from:).
}
}
/// Resumes the database. A resumed database stops preventing database locks
/// in order to avoid the [`0xdead10cc`
/// exception](https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report).
///
/// This method can be called from any thread.
///
/// See suspend().
func resume() {
isSuspended = false
}
/// Support for `checkForSuspensionViolation(from:)`
private func journalMode() throws -> String {
if let journalMode = journalModeCache {
return journalMode
}
// Don't return String.fetchOne(self, sql: "PRAGMA journal_mode"), so
// that we don't create an infinite loop in checkForSuspensionViolation(from:)
var statement: SQLiteStatement? = nil
let sql = "PRAGMA journal_mode"
sqlite3_prepare_v2(sqliteConnection, sql, -1, &statement, nil)
defer { sqlite3_finalize(statement) }
sqlite3_step(statement)
guard let cString = sqlite3_column_text(statement, 0) else {
throw DatabaseError(resultCode: lastErrorCode, message: lastErrorMessage, sql: sql)
}
let journalMode = String(cString: cString)
journalModeCache = journalMode
return journalMode
}
/// If the database is suspended, and executing the statement would lock the
/// database in a way that may trigger the [`0xdead10cc` exception](https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report),
/// this method rollbacks the current transaction and throws `SQLITE_ABORT`.
///
/// See `suspend()` and ``Configuration/observesSuspensionNotifications``.
func checkForSuspensionViolation(from statement: Statement) throws {
try $isSuspended.read { isSuspended in
guard isSuspended else {
return
}
if try journalMode() == "wal" && statement.isReadonly {
// In WAL mode, accept read-only statements:
// - SELECT ...
// - BEGIN DEFERRED TRANSACTION
//
// Those are not read-only:
// - INSERT ...
// - BEGIN IMMEDIATE TRANSACTION
return
}
if statement.releasesDatabaseLock {
// Accept statements that release locks:
// - COMMIT
// - ROLLBACK
// - ROLLBACK TRANSACTION TO SAVEPOINT
// - RELEASE SAVEPOINT
return
}
// Attempt at releasing an eventual lock with ROLLBACk,
// as explained in Database.suspend().
//
// Use sqlite3_exec instead of `try? rollback()` in order to avoid
// an infinite loop in checkForSuspensionViolation(from:)
_ = sqlite3_exec(sqliteConnection, "ROLLBACK", nil, nil, nil)
throw DatabaseError(
resultCode: .SQLITE_ABORT,
message: "Database is suspended",
sql: statement.sql,
arguments: statement.arguments,
publicStatementArguments: configuration.publicStatementArguments)
}
}
// MARK: - Transactions & Savepoint
/// Throws `SQLITE_ABORT` if called from a transaction-wrapping method and
/// transaction has been aborted (for example, by `sqlite3_interrupt`, or a
/// `ON CONFLICT ROLLBACK` clause.
///
/// For example:
///
/// try db.inTransaction {
/// do {
/// // Throws an error because the transaction was rollbacked.
/// try ...
/// } catch {
/// // Catch the error and continue.
/// ...
/// }
///
/// // <- Here we're inside an aborted transaction.
/// try checkForAbortedTransaction(...) // throws SQLITE_ABORT
/// ...
/// }
func checkForAbortedTransaction(
sql: @autoclosure () -> String? = nil,
arguments: @autoclosure () -> StatementArguments? = nil)
throws
{
if isInsideTransactionBlock && sqlite3_get_autocommit(sqliteConnection) != 0 {
throw DatabaseError(
resultCode: .SQLITE_ABORT,
message: "Transaction was aborted",
sql: sql(),
arguments: arguments(),
publicStatementArguments: configuration.publicStatementArguments)
}
}
/// Wraps database operations inside a database transaction.
///
/// For example:
///
/// ```swift
/// try dbQueue.writeWithoutTransaction do { db in
/// try db.inTransaction {
/// try db.execute(sql: "INSERT ...")
/// return .commit
/// }
/// }
/// ```
///
/// If `operations` throws an error, the transaction is rollbacked and the
/// error is rethrown. If it returns ``TransactionCompletion/rollback``, the
/// transaction is also rollbacked, but no error is thrown.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_transaction.html>
///
/// - warning: This method is not reentrant: you can not nest transactions.
/// Use ``inSavepoint(_:)`` instead.
///
/// - parameters:
/// - kind: The transaction type (default nil).
///
/// If nil, and the database connection is read-only, the transaction
/// kind is ``TransactionKind/deferred``.
///
/// If nil, and the database connection is not read-only, the
/// transaction kind is the ``Configuration/defaultTransactionKind``
/// of the ``configuration``.
/// - operations: A function that executes SQL statements and returns
/// either ``TransactionCompletion/commit`` or ``TransactionCompletion/rollback``.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the
/// error thrown by `operations`.
public func inTransaction(_ kind: TransactionKind? = nil, _ operations: () throws -> TransactionCompletion) throws {
// Begin transaction
try beginTransaction(kind)
// Support for `checkForAbortedTransaction()`.
let wasInsideTransactionBlock = isInsideTransactionBlock
isInsideTransactionBlock = true
defer {
isInsideTransactionBlock = wasInsideTransactionBlock
}
// Now that transaction has begun, we'll rollback in case of error.
// But we'll throw the first caught error, so that user knows
// what happened.
var firstError: Error? = nil
let needsRollback: Bool
do {
let completion = try operations()
switch completion {
case .commit:
// In case of aborted transaction, throw SQLITE_ABORT instead
// of the generic SQLITE_ERROR "cannot commit - no transaction is active"
try checkForAbortedTransaction()
// Leave transaction block now, so that transaction observers
// can execute statements without getting errors from
// checkForAbortedTransaction().
isInsideTransactionBlock = wasInsideTransactionBlock
try commit()
needsRollback = false
case .rollback:
needsRollback = true
}
} catch {
firstError = error
needsRollback = true
}
if needsRollback {
do {
try rollback()
} catch {
if firstError == nil {
firstError = error
}
}
}
if let firstError {
throw firstError
}
}
/// Runs the block with an isolation level equal or greater than
/// snapshot isolation.
///
/// - parameter readOnly: If true, writes are forbidden.
func isolated<T>(readOnly: Bool = false, _ block: () throws -> T) throws -> T {
if sqlite3_get_autocommit(sqliteConnection) == 0 {
// Spare savepoints
if readOnly {
return try self.readOnly(block)
} else {
return try block()
}
} else {
var result: T?
if readOnly {
// Enter read-only mode before starting a transaction, so that the
// transaction commit does not trigger database observation.
// See <https://github.com/groue/GRDB.swift/pull/1213>.
try self.readOnly {
try inSavepoint {
result = try block()
return .commit
}
}
} else {
try inSavepoint {
result = try block()
return .commit
}
}
return result!
}
}
/// Wraps database operations inside a savepoint.
///
/// For example:
///
/// ```swift
/// try dbQueue.write do {
/// try db.inSavepoint {
/// try db.execute(sql: "INSERT ...")
/// return .commit
/// }
/// }
/// ```
///
/// If `operations` throws an error, the savepoint is rollbacked and the
/// error is rethrown. If it returns ``TransactionCompletion/rollback``, the
/// savepoint is also rollbacked, but no error is thrown.
///
/// This method is reentrant: you can nest savepoints.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_savepoint.html>
///
/// - parameter operations: A function that executes SQL statements and
/// returns either ``TransactionCompletion/commit`` or
/// ``TransactionCompletion/rollback``.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the
/// error thrown by `operations`.
public func inSavepoint(_ operations: () throws -> TransactionCompletion) throws {
if !isInsideTransaction {
// By default, top level SQLite savepoints open a
// deferred transaction.
//
// But GRDB database configuration mandates a default transaction
// kind that we have to honor.
//
// Besides, starting some (?) SQLCipher/SQLite version, SQLite has a
// bug. Returning 1 from `sqlite3_commit_hook` does not leave the
// database in the autocommit mode, as expected after a rollback.
// This bug only happens, as far as we know, when a transaction is
// started with a savepoint:
//
// SAVEPOINT test;
// CREATE TABLE t(a);
// -- Rollbacked with sqlite3_commit_hook:
// RELEASE SAVEPOINT test;
// -- Not in the autocommit mode here!
//
// For those two reasons, we open a transaction instead of a
// top-level savepoint.
try inTransaction { try operations() }
return
}
// Begin savepoint
//
// We use a single name for savepoints because there is no need
// using unique savepoint names. User could still mess with them
// with raw SQL queries, but let's assume that it is unlikely that
// the user uses "grdb" as a savepoint name.
try execute(sql: "SAVEPOINT grdb")
// Support for `checkForAbortedTransaction()`.
let wasInsideTransactionBlock = isInsideTransactionBlock
isInsideTransactionBlock = true
defer {
isInsideTransactionBlock = wasInsideTransactionBlock
}
// Now that savepoint has begun, we'll rollback in case of error.
// But we'll throw the first caught error, so that user knows
// what happened.
var firstError: Error? = nil
let needsRollback: Bool
do {
let completion = try operations()
switch completion {
case .commit:
// In case of aborted transaction, throw SQLITE_ABORT instead
// of the generic SQLITE_ERROR "cannot commit - no transaction is active"
try checkForAbortedTransaction()
// Leave transaction block now, so that transaction observers
// can execute statements without getting errors from
// checkForAbortedTransaction().
isInsideTransactionBlock = wasInsideTransactionBlock
try execute(sql: "RELEASE SAVEPOINT grdb")
assert(sqlite3_get_autocommit(sqliteConnection) == 0)
needsRollback = false
case .rollback:
needsRollback = true
}
} catch {
firstError = error
needsRollback = true
}
if needsRollback {
do {
// Rollback, and release the savepoint.
// Rollback alone is not enough to clear the savepoint from
// the SQLite savepoint stack.
try execute(sql: "ROLLBACK TRANSACTION TO SAVEPOINT grdb")
try execute(sql: "RELEASE SAVEPOINT grdb")
} catch {
if firstError == nil {
firstError = error
}
}
}
if let firstError {
throw firstError
}
}
/// Begins a database transaction.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_transaction.html>
///
/// - parameters:
/// - kind: The transaction type (default nil).
///
/// If nil, and the database connection is read-only, the transaction
/// kind is ``TransactionKind/deferred``.
///
/// If nil, and the database connection is not read-only, the
/// transaction kind is the ``Configuration/defaultTransactionKind``
/// of the ``configuration``.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func beginTransaction(_ kind: TransactionKind? = nil) throws {
// SQLite throws an error for non-deferred transactions when read-only.
let kind = kind ?? (isReadOnly ? .deferred : configuration.defaultTransactionKind)
try execute(sql: "BEGIN \(kind.rawValue) TRANSACTION")
assert(sqlite3_get_autocommit(sqliteConnection) == 0)
}
/// Rollbacks a database transaction.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_transaction.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func rollback() throws {
// The SQLite documentation contains two related but distinct techniques
// to handle rollbacks and errors:
//
// https://www.sqlite.org/lang_transaction.html#immediate
//
// > Response To Errors Within A Transaction
// >
// > If certain kinds of errors occur within a transaction, the
// > transaction may or may not be rolled back automatically.
// > The errors that can cause an automatic rollback include:
// >
// > - SQLITE_FULL: database or disk full
// > - SQLITE_IOERR: disk I/O error
// > - SQLITE_BUSY: database in use by another process
// > - SQLITE_NOMEM: out or memory
// >
// > [...] It is recommended that applications respond to the
// > errors listed above by explicitly issuing a ROLLBACK
// > command. If the transaction has already been rolled back
// > automatically by the error response, then the ROLLBACK
// > command will fail with an error, but no harm is caused
// > by this.
//
// https://sqlite.org/c3ref/get_autocommit.html
//
// > The sqlite3_get_autocommit() interface returns non-zero or zero if
// > the given database connection is or is not in autocommit mode,
// > respectively.
// >
// > [...] If certain kinds of errors occur on a statement within a
// > multi-statement transaction (errors including SQLITE_FULL,
// > SQLITE_IOERR, SQLITE_NOMEM, SQLITE_BUSY, and SQLITE_INTERRUPT) then
// > the transaction might be rolled back automatically. The only way to
// > find out whether SQLite automatically rolled back the transaction
// > after an error is to use this function.
//
// The second technique is more robust, because we don't have to guess
// which rollback errors should be ignored, and which rollback errors
// should be exposed to the library user.
if isInsideTransaction {
try execute(sql: "ROLLBACK TRANSACTION")
}
assert(sqlite3_get_autocommit(sqliteConnection) != 0)
}
/// Commits a database transaction.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_transaction.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func commit() throws {
try execute(sql: "COMMIT TRANSACTION")
assert(sqlite3_get_autocommit(sqliteConnection) != 0)
}
// MARK: - Memory Management
/// Frees as much memory as possible.
public func releaseMemory() {
SchedulingWatchdog.preconditionValidQueue(self)
if let sqliteConnection {
sqlite3_db_release_memory(sqliteConnection)
}
schemaCache.clear()
internalStatementCache.clear()
publicStatementCache.clear()
}
// MARK: - Erasing
func erase() throws {
#if SQLITE_HAS_CODEC
// SQLCipher does not support the backup API:
// https://discuss.zetetic.net/t/using-the-sqlite-online-backup-api/2631
// So we'll drop all database objects one after the other.
// Prevent foreign keys from messing with drop table statements
let foreignKeysEnabled = try Bool.fetchOne(self, sql: "PRAGMA foreign_keys")!
if foreignKeysEnabled {
try execute(sql: "PRAGMA foreign_keys = OFF")
}
try throwingFirstError(
execute: {
// Remove all database objects, one after the other
try inTransaction {
let sql = "SELECT type, name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'"
while let row = try Row.fetchOne(self, sql: sql) {
let type: String = row["type"]
let name: String = row["name"]
try execute(sql: "DROP \(type) \(name.quotedDatabaseIdentifier)")
}
return .commit
}
},
finally: {
// Restore foreign keys if needed
if foreignKeysEnabled {
try execute(sql: "PRAGMA foreign_keys = ON")
}
})
#else
try DatabaseQueue().backup(to: self)
#endif
}
// MARK: - Backup
/// Copies the database contents into another database.
///
/// The `backup` method blocks the current thread until the destination
/// database contains the same contents as the source database.
///
/// Usage:
///
/// ```swift
/// let source: DatabaseQueue = ...
/// let destination: DatabaseQueue = ...
/// try source.write { sourceDb in
/// try destination.barrierWriteWithoutTransaction { destDb in
/// try sourceDb.backup(to: destDb)
/// }
/// }
/// ```
///
/// When you're after progress reporting during backup, you'll want to
/// perform the backup in several steps. Each step copies the number of
/// _database pages_ you specify. See <https://www.sqlite.org/c3ref/backup_finish.html>
/// for more information:
///
/// ```swift
/// // Backup with progress reporting
/// try sourceDb.backup(to: destDb, pagesPerStep: ...) { progress in
/// print("Database backup progress:", progress)
/// }
/// ```
///
/// The `progress` callback will be called at least once—when
/// `backupProgress.isCompleted == true`. If the callback throws
/// when `backupProgress.isCompleted == false`, the backup is aborted
/// and the error is rethrown. If the callback throws when
/// `backupProgress.isCompleted == true`, backup completion is
/// unaffected and the error is silently ignored.
///
/// See also ``DatabaseReader/backup(to:pagesPerStep:progress:)``.
///
/// - parameters:
/// - destDb: The destination database.
/// - pagesPerStep: The number of database pages copied on each backup
/// step. By default, all pages are copied in one single step.
/// - progress: An optional function that is notified of the backup
/// progress.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the
/// error thrown by `progress`.
public func backup(
to destDb: Database,
pagesPerStep: CInt = -1,
progress: ((DatabaseBackupProgress) throws -> Void)? = nil)
throws
{
try backupInternal(
to: destDb,
pagesPerStep: pagesPerStep,
afterBackupStep: progress)
}
func backupInternal(
to destDb: Database,
pagesPerStep: CInt = -1,
afterBackupInit: (() -> Void)? = nil,
afterBackupStep: ((DatabaseBackupProgress) throws -> Void)? = nil)
throws
{
guard let backup = sqlite3_backup_init(destDb.sqliteConnection, "main", sqliteConnection, "main") else {
throw DatabaseError(resultCode: destDb.lastErrorCode, message: destDb.lastErrorMessage)
}
guard Int(bitPattern: backup) != Int(SQLITE_ERROR) else {
throw DatabaseError()
}
afterBackupInit?()
do {
backupLoop: while true {
let rc = sqlite3_backup_step(backup, pagesPerStep)
let totalPageCount = Int(sqlite3_backup_pagecount(backup))
let remainingPageCount = Int(sqlite3_backup_remaining(backup))
let progress = DatabaseBackupProgress(
remainingPageCount: remainingPageCount,
totalPageCount: totalPageCount,
isCompleted: rc == SQLITE_DONE)
switch rc {
case SQLITE_DONE:
try? afterBackupStep?(progress)
break backupLoop
case SQLITE_OK:
try afterBackupStep?(progress)
case let code:
throw DatabaseError(resultCode: code, message: destDb.lastErrorMessage)
}
}
} catch {
sqlite3_backup_finish(backup)
throw error
}
switch sqlite3_backup_finish(backup) {
case SQLITE_OK:
break
case let code:
throw DatabaseError(resultCode: code, message: destDb.lastErrorMessage)
}
// The schema of the destination database has changed:
destDb.clearSchemaCache()
}
}
// Explicit non-conformance to Sendable: `Database` must be used from a
// serialized database access dispatch queue (see `SerializedDatabase`).
@available(*, unavailable)
extension Database: Sendable { }
#if SQLITE_HAS_CODEC
extension Database {
// MARK: - Encryption
/// Sets the passphrase used to crypt and decrypt an SQLCipher database.
///
/// Call this method from `Configuration.prepareDatabase`,
/// as in the example below:
///
/// var config = Configuration()
/// config.prepareDatabase { db in
/// try db.usePassphrase("secret")
/// }
public func usePassphrase(_ passphrase: String) throws {
guard var data = passphrase.data(using: .utf8) else {
throw DatabaseError(message: "invalid passphrase")
}
defer {
data.resetBytes(in: 0..<data.count)
}
try usePassphrase(data)
}
/// Sets the passphrase used to crypt and decrypt an SQLCipher database.
///
/// Call this method from `Configuration.prepareDatabase`,
/// as in the example below:
///
/// var config = Configuration()
/// config.prepareDatabase { db in
/// try db.usePassphrase(passphraseData)
/// }
public func usePassphrase(_ passphrase: Data) throws {
let code = passphrase.withUnsafeBytes {
sqlite3_key(sqliteConnection, $0.baseAddress, CInt($0.count))
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
}
}
/// Changes the passphrase used by an SQLCipher encrypted database.
public func changePassphrase(_ passphrase: String) throws {
guard var data = passphrase.data(using: .utf8) else {
throw DatabaseError(message: "invalid passphrase")
}
defer {
data.resetBytes(in: 0..<data.count)
}
try changePassphrase(data)
}
/// Changes the passphrase used by an SQLCipher encrypted database.
public func changePassphrase(_ passphrase: Data) throws {
// FIXME: sqlite3_rekey is discouraged.
//
// https://github.com/ccgus/fmdb/issues/547#issuecomment-259219320
//
// > We (Zetetic) have been discouraging the use of sqlite3_rekey in
// > favor of attaching a new database with the desired encryption
// > options and using sqlcipher_export() to migrate the contents and
// > schema of the original db into the new one:
// > https://discuss.zetetic.net/t/how-to-encrypt-a-plaintext-sqlite-database-to-use-sqlcipher-and-avoid-file-is-encrypted-or-is-not-a-database-errors/
let code = passphrase.withUnsafeBytes {
sqlite3_rekey(sqliteConnection, $0.baseAddress, CInt($0.count))
}
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code, message: lastErrorMessage)
}
}
}
#endif
extension Database {
// MARK: - Database-Related Types
/// See BusyMode and <https://www.sqlite.org/c3ref/busy_handler.html>
public typealias BusyCallback = (_ numberOfTries: Int) -> Bool
/// When there are several connections to a database, a connection may try
/// to access the database while it is locked by another connection.
///
/// The BusyMode enum describes the behavior of GRDB when such a situation
/// occurs:
///
/// - .immediateError: The SQLITE_BUSY error is immediately returned to the
/// connection that tries to access the locked database.
///
/// - .timeout: The SQLITE_BUSY error will be returned only if the database
/// remains locked for more than the specified duration.
///
/// - .callback: Perform your custom lock handling.
///
/// To set the busy mode of a database, use Configuration:
///
/// // Wait 1 second before failing with SQLITE_BUSY
/// let configuration = Configuration(busyMode: .timeout(1))
/// let dbQueue = try DatabaseQueue(path: ..., configuration: configuration)
///
/// Relevant SQLite documentation:
///
/// - <https://www.sqlite.org/c3ref/busy_timeout.html>
/// - <https://www.sqlite.org/c3ref/busy_handler.html>
/// - <https://www.sqlite.org/lang_transaction.html>
/// - <https://www.sqlite.org/wal.html>
public enum BusyMode {
/// The `SQLITE_BUSY` error is immediately returned to the connection
/// that tries to access the locked database.
case immediateError
/// The `SQLITE_BUSY` error will be returned only if the database
/// remains locked for more than the specified duration (in seconds).
case timeout(TimeInterval)
/// A custom callback that is called when a database is locked.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/busy_handler.html>
case callback(BusyCallback)
}
/// The available checkpoint modes.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/wal_checkpoint_v2.html>
public enum CheckpointMode: CInt, Sendable {
/// The `SQLITE_CHECKPOINT_PASSIVE` mode.
case passive = 0
/// The `SQLITE_CHECKPOINT_FULL` mode.
case full = 1
/// The `SQLITE_CHECKPOINT_RESTART` mode.
case restart = 2
/// The `SQLITE_CHECKPOINT_TRUNCATE` mode.
case truncate = 3
}
/// The name of a string comparison function used by SQLite.
///
/// Related SQLite documentation:
/// - <https://www.sqlite.org/datatype3.html#collating_sequences>
/// - <https://www.sqlite.org/datatype3.html#collation>
public struct CollationName: RawRepresentable, Hashable, Sendable {
public let rawValue: String
/// Creates a collation name.
public init(rawValue: String) {
self.rawValue = rawValue
}
/// The `BINARY` built-in SQL collation.
public static let binary = CollationName(rawValue: "BINARY")
/// The `NOCASE` built-in SQL collation.
public static let nocase = CollationName(rawValue: "NOCASE")
/// The `RTRIM` built-in SQL collation.
public static let rtrim = CollationName(rawValue: "RTRIM")
}
/// An SQL column type.
///
/// You use column types when you modify the database schema. For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // id INTEGER PRIMARY KEY,
/// // name TEXT,
/// // creationDate DATETIME,
/// // )
/// try db.create(table: "player") { t in
/// t.primaryKey("id", .integer)
/// t.column("name", .text)
/// t.column("creationDate", .datetime)
/// }
/// ```
///
/// For more information, see
/// [Datatypes In SQLite](https://www.sqlite.org/datatype3.html).
public struct ColumnType: RawRepresentable, Hashable, Sendable {
/// The SQL for the column type (`"TEXT"`, `"BLOB"`, etc.)
public let rawValue: String
/// Creates an SQL column type.
public init(rawValue: String) {
self.rawValue = rawValue
}
/// The `TEXT` column type.
public static let text = ColumnType(rawValue: "TEXT")
/// The `TEXT` column type, suitable for JSON columns.
///
/// SQLite JSON functions and operators are
/// [documented](https://www.sqlite.org/json1.html#interface_overview)
/// to throw errors if any of their arguments are binary blobs.
/// That's the reason why it is recommended to store JSON as text.
public static let jsonText = ColumnType(rawValue: "TEXT")
/// The `INTEGER` column type.
public static let integer = ColumnType(rawValue: "INTEGER")
/// The `DOUBLE` column type.
public static let double = ColumnType(rawValue: "DOUBLE")
/// The `REAL` column type.
public static let real = ColumnType(rawValue: "REAL")
/// The `NUMERIC` column type.
public static let numeric = ColumnType(rawValue: "NUMERIC")
/// The `BOOLEAN` column type.
public static let boolean = ColumnType(rawValue: "BOOLEAN")
/// The `BLOB` column type.
public static let blob = ColumnType(rawValue: "BLOB")
/// The `DATE` column type.
public static let date = ColumnType(rawValue: "DATE")
/// The `DATETIME` column type.
public static let datetime = ColumnType(rawValue: "DATETIME")
/// The `ANY` column type.
public static let any = ColumnType(rawValue: "ANY")
}
/// An SQLite conflict resolution.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_conflict.html>
public enum ConflictResolution: String, Sendable {
/// The `ROLLBACK` conflict resolution.
case rollback = "ROLLBACK"
/// The `ABORT` conflict resolution.
case abort = "ABORT"
/// The `FAIL` conflict resolution.
case fail = "FAIL"
/// The `IGNORE` conflict resolution.
case ignore = "IGNORE"
/// The `REPLACE` conflict resolution.
case replace = "REPLACE"
}
/// A foreign key action.
///
/// Related SQLite documentation: <https://www.sqlite.org/foreignkeys.html>
public enum ForeignKeyAction: String, Sendable {
/// The `CASCADE` foreign key action.
case cascade = "CASCADE"
/// The `RESTRICT` foreign key action.
case restrict = "RESTRICT"
/// The `SET NULL` foreign key action.
case setNull = "SET NULL"
/// The `SET DEFAULT` foreign key action.
case setDefault = "SET DEFAULT"
}
/// An error log function that takes an error code and message.
public typealias LogErrorFunction = (_ resultCode: ResultCode, _ message: String) -> Void
/// An SQLite storage class.
///
/// For more information, see
/// [Datatypes In SQLite](https://www.sqlite.org/datatype3.html).
public struct StorageClass: RawRepresentable, Hashable, Sendable {
/// The SQL for the storage class (`"INTEGER"`, `"REAL"`, etc.)
public let rawValue: String
/// Creates an SQL storage class.
public init(rawValue: String) {
self.rawValue = rawValue
}
/// The `INTEGER` storage class.
public static let integer = StorageClass(rawValue: "INTEGER")
/// The `REAL` storage class.
public static let real = StorageClass(rawValue: "REAL")
/// The `TEXT` storage class.
public static let text = StorageClass(rawValue: "TEXT")
/// The `BLOB` storage class.
public static let blob = StorageClass(rawValue: "BLOB")
}
/// An option for the SQLite tracing feature.
///
/// You use `TracingOptions` with the `Database`
/// ``Database/trace(options:_:)`` method.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/c_trace.html>
public struct TracingOptions: OptionSet, Sendable {
/// The raw trace event code.
public let rawValue: CInt
/// Creates a `TracingOptions` from a raw trace event code.
public init(rawValue: CInt) {
self.rawValue = rawValue
}
/// The option that reports executed statements.
///
/// Trace event code: `SQLITE_TRACE_STMT`.
public static let statement = TracingOptions(rawValue: SQLITE_TRACE_STMT)
/// The option that reports executed statements and the estimated
/// duration that the statement took to run.
///
/// Trace event code: `SQLITE_TRACE_PROFILE`.
public static let profile = TracingOptions(rawValue: SQLITE_TRACE_PROFILE)
}
/// A trace event.
///
/// You get instances of `TraceEvent` from the `Database`
/// ``Database/trace(options:_:)`` method.
public enum TraceEvent: CustomStringConvertible {
/// Information about an executed statement.
public struct Statement: CustomStringConvertible {
var sqliteStatement: SQLiteStatement
var unexpandedSQL: UnsafePointer<CChar>?
var sqlite3_expanded_sql: @convention(c) (OpaquePointer?) -> UnsafeMutablePointer<CChar>?
var publicStatementArguments: Bool // See Configuration.publicStatementArguments
/// The executed SQL, where bound parameters are not expanded.
///
/// For example:
///
/// ```sql
/// SELECT * FROM player WHERE email = ?
/// ```
public var sql: String {
if let unexpandedSQL {
let sql = String(cString: unexpandedSQL)
return sql.hasPrefix("--") ? sql : sql.trimmedSQLStatement
} else {
return String(cString: sqlite3_sql(sqliteStatement)).trimmedSQLStatement
}
}
/// The executed SQL, where bound parameters are expanded.
///
/// For example:
///
/// ```sql
/// SELECT * FROM player WHERE email = 'arthur@example.com'
/// ```
///
/// - warning: It is your responsibility to prevent sensitive
/// information from leaking in unexpected locations, so use this
/// property with care.
public var expandedSQL: String {
if let unexpandedSQL {
let sql = String(cString: unexpandedSQL)
if sql.hasPrefix("--") { return sql }
}
guard let cString = sqlite3_expanded_sql(sqliteStatement) else {
return ""
}
defer { sqlite3_free(cString) }
return String(cString: cString).trimmedSQLStatement
}
public var description: String {
if publicStatementArguments {
return expandedSQL
} else {
return sql
}
}
}
/// An event reported by the
/// ``Database/TracingOptions/statement`` option.
case statement(Statement)
/// An event reported by the
/// ``Database/TracingOptions/profile`` option.
case profile(statement: Statement, duration: TimeInterval)
/// A description of the trace event.
///
/// For example:
///
/// SELECT * FROM player WHERE email = ?
/// 0.1s SELECT * FROM player WHERE email = ?
///
/// The format of the event description may change between GRDB releases,
/// without notice: don't have your application rely on any specific format.
public var description: String {
switch self {
case let .statement(statement):
return statement.description
case let .profile(statement: statement, duration: duration):
let durationString = String(format: "%.3f", duration)
return "\(durationString)s \(statement)"
}
}
/// A description of the trace event, where bound parameters
/// are expanded.
///
/// For example:
///
/// SELECT * FROM player WHERE email = 'arthur@example.com'
/// 0.1s SELECT * FROM player WHERE email = 'arthur@example.com'
///
/// The format of the event description may change between GRDB releases,
/// without notice: don't have your application rely on any specific format.
///
/// - warning: It is your responsibility to prevent sensitive
/// information from leaking in unexpected locations, so use this
/// property with care.
public var expandedDescription: String {
switch self {
case let .statement(statement):
return statement.expandedSQL
case let .profile(statement: statement, duration: duration):
let durationString = String(format: "%.3f", duration)
return "\(durationString)s \(statement.expandedSQL)"
}
}
}
/// A transaction commit, or rollback.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_transaction.html>.
@frozen
public enum TransactionCompletion: Sendable {
case commit
case rollback
}
/// A transaction kind.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_transaction.html>.
public enum TransactionKind: String, Sendable {
/// The `DEFERRED` transaction kind.
case deferred = "DEFERRED"
/// The `IMMEDIATE` transaction kind.
case immediate = "IMMEDIATE"
/// The `EXCLUSIVE` transaction kind.
case exclusive = "EXCLUSIVE"
}
/// An SQLite threading mode. See <https://www.sqlite.org/threadsafe.html>.
///
/// - Note: Only the multi-thread mode (`SQLITE_OPEN_NOMUTEX`) is currently
/// supported, since all <doc:DatabaseConnections> access SQLite connections
/// through a `SerializedDatabase`.
enum ThreadingMode {
case `default`
case multiThread
case serialized
var SQLiteOpenFlags: CInt {
switch self {
case .`default`:
return 0
case .multiThread:
return SQLITE_OPEN_NOMUTEX
case .serialized:
return SQLITE_OPEN_FULLMUTEX
}
}
}
}
// Explicit non-conformance to Sendable: a trace event contains transient
// information.
@available(*, unavailable)
extension Database.TraceEvent: Sendable { }
// Explicit non-conformance to Sendable: a trace event contains transient
// information.
@available(*, unavailable)
extension Database.TraceEvent.Statement: Sendable { }