Files
data-lite-core/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift
2025-10-26 18:21:59 +02:00

438 lines
18 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// A protocol that defines an SQLite database connection.
///
/// The `ConnectionProtocol` defines the essential API for managing a database connection,
/// including configuration, statement preparation, transactions, encryption, and delegation.
/// Conforming types are responsible for maintaining the connections lifecycle and settings.
///
/// ## Topics
///
/// ### Managing Connection State
///
/// - ``isAutocommit``
/// - ``isReadonly``
///
/// ### Accessing PRAGMA Values
///
/// - ``busyTimeout``
/// - ``applicationID``
/// - ``foreignKeys``
/// - ``journalMode``
/// - ``synchronous``
/// - ``userVersion``
///
/// ### Managing SQLite Lifecycle
///
/// - ``initialize()``
/// - ``shutdown()``
///
/// ### Handling Encryption
///
/// - ``apply(_:name:)``
/// - ``rekey(_:name:)``
///
/// ### Managing Delegates
///
/// - ``add(delegate:)``
/// - ``remove(delegate:)``
/// - ``add(trace:)``
/// - ``remove(trace:)``
///
/// ### Registering Custom SQL Functions
///
/// - ``add(function:)``
/// - ``remove(function:)``
///
/// ### Preparing SQL Statements
///
/// - ``prepare(sql:)``
/// - ``prepare(sql:options:)``
///
/// ### Executing SQL Commands
///
/// - ``execute(sql:)``
///
/// ### Controlling PRAGMA Settings
///
/// - ``get(pragma:)``
/// - ``set(pragma:value:)``
///
/// ### Managing Transactions
///
/// - ``beginTransaction(_:)``
/// - ``commitTransaction()``
/// - ``rollbackTransaction()``
public protocol ConnectionProtocol: AnyObject {
// MARK: - Connection State
/// The autocommit state of the connection.
///
/// Autocommit is enabled by default and remains active when no explicit transaction is open.
/// Executing `BEGIN` disables autocommit, while `COMMIT` or `ROLLBACK` re-enables it.
///
/// - Returns: `true` if autocommit mode is active; otherwise, `false`.
/// - SeeAlso: [Test For Auto-Commit Mode](https://sqlite.org/c3ref/get_autocommit.html)
var isAutocommit: Bool { get }
/// The read-only state of the connection.
///
/// Returns `true` if the main database allows only read operations, or `false` if it permits
/// both reading and writing.
///
/// - Returns: `true` if the connection is read-only; otherwise, `false`.
/// - SeeAlso: [Determine if a database is read-only](https://sqlite.org/c3ref/db_readonly.html)
var isReadonly: Bool { get }
// MARK: - PRAGMA Accessors
/// The busy timeout of the connection, in milliseconds.
///
/// Defines how long SQLite waits for a locked database to become available before returning
/// a `SQLITE_BUSY` error. A value of zero disables the timeout, causing operations to fail
/// immediately if the database is locked.
///
/// - SeeAlso: [Set A Busy Timeout](https://sqlite.org/c3ref/busy_timeout.html)
var busyTimeout: Int32 { get set }
/// The application identifier stored in the database header.
///
/// Used to distinguish database files created by different applications or file formats. This
/// value is a 32-bit integer written to the database header and can be queried or modified
/// through the `PRAGMA application_id` command.
///
/// - SeeAlso: [Application ID](https://sqlite.org/pragma.html#pragma_application_id)
var applicationID: Int32 { get set }
/// The foreign key enforcement state of the connection.
///
/// When enabled, SQLite enforces foreign key constraints on all tables. This behavior can be
/// controlled with `PRAGMA foreign_keys`.
///
/// - SeeAlso: [Foreign Keys](https://sqlite.org/pragma.html#pragma_foreign_keys)
var foreignKeys: Bool { get set }
/// The journal mode used by the database connection.
///
/// Determines how SQLite maintains the rollback journal for transactions.
///
/// - SeeAlso: [Journal Mode](https://sqlite.org/pragma.html#pragma_journal_mode)
var journalMode: JournalMode { get set }
/// The synchronization mode for database writes.
///
/// Controls how aggressively SQLite syncs data to disk for durability versus performance.
///
/// - SeeAlso: [Synchronous](https://sqlite.org/pragma.html#pragma_synchronous)
var synchronous: Synchronous { get set }
/// The user-defined schema version number.
///
/// This value is stored in the database header and can be used by applications to track schema
/// migrations or format changes.
///
/// - SeeAlso: [User Version](https://sqlite.org/pragma.html#pragma_user_version)
var userVersion: Int32 { get set }
// MARK: - SQLite Lifecycle
/// Initializes the SQLite library.
///
/// Sets up the global state required by SQLite, including operating-systemspecific
/// initialization. This function must be called before using any other SQLite API,
/// unless the library is initialized automatically.
///
/// Only the first invocation during the process lifetime, or the first after
/// ``shutdown()``, performs real initialization. All subsequent calls are no-ops.
///
/// - Note: Workstation applications normally do not need to call this function explicitly,
/// as it is invoked automatically by interfaces such as `sqlite3_open()`. It is mainly
/// intended for embedded systems and controlled initialization scenarios.
///
/// - Throws: ``SQLiteError`` if initialization fails.
/// - SeeAlso: [Initialize The SQLite Library](https://sqlite.org/c3ref/initialize.html)
static func initialize() throws(SQLiteError)
/// Shuts down the SQLite library.
///
/// Releases all global resources allocated by SQLite and undoes the effects of a
/// successful call to ``initialize()``. This function should be called exactly once
/// for each effective initialization and only after all database connections are closed.
///
/// Only the first invocation since the last call to ``initialize()`` performs
/// deinitialization. All other calls are harmless no-ops.
///
/// - Note: Workstation applications normally do not need to call this function explicitly,
/// as cleanup happens automatically at process termination. It is mainly used in
/// embedded systems where precise resource control is required.
///
/// - Important: This function is **not** threadsafe and must be called from a single thread.
/// - Throws: ``SQLiteError`` if the shutdown process fails.
/// - SeeAlso: [Initialize The SQLite Library](https://sqlite.org/c3ref/initialize.html)
static func shutdown() throws(SQLiteError)
// MARK: - Encryption
/// Applies an encryption key to a database connection.
///
/// If the database is newly created, this call initializes encryption and makes it encrypted.
/// If the database already exists, this call decrypts its contents for access using the
/// provided key. An existing unencrypted database cannot be encrypted using this method.
///
/// This function must be called immediately after the connection is opened and before invoking
/// any other operation on the same connection.
///
/// - Parameters:
/// - key: The encryption key to apply.
/// - name: The database name, or `nil` for the main database.
/// - Throws: ``SQLiteError`` if the key is invalid or the decryption process fails.
/// - SeeAlso: [Setting The Key](https://www.zetetic.net/sqlcipher/sqlcipher-api/#key)
func apply(_ key: Connection.Key, name: String?) throws(SQLiteError)
/// Changes the encryption key for an open database.
///
/// Re-encrypts the database file with a new key while preserving its existing data. The
/// connection must already be open and unlocked with a valid key applied through
/// ``apply(_:name:)``. This operation replaces the current encryption key but does not modify
/// the database contents.
///
/// This function can only be used with an encrypted database. It has no effect on unencrypted
/// databases.
///
/// - Parameters:
/// - key: The new encryption key to apply.
/// - name: The database name, or `nil` for the main database.
/// - Throws: ``SQLiteError`` if rekeying fails or encryption is not supported.
/// - SeeAlso: [Changing The Key](https://www.zetetic.net/sqlcipher/sqlcipher-api/#Changing_Key)
func rekey(_ key: Connection.Key, name: String?) throws(SQLiteError)
// MARK: - Delegation
/// Adds a delegate to receive connection-level events.
///
/// Registers an object conforming to ``ConnectionDelegate`` to receive notifications such as
/// update actions and transaction events.
///
/// - Parameter delegate: The delegate to add.
func add(delegate: ConnectionDelegate)
/// Removes a previously added delegate.
///
/// Unregisters an object that was previously added with ``add(delegate:)`` so it no longer
/// receives update and transaction events.
///
/// - Parameter delegate: The delegate to remove.
func remove(delegate: ConnectionDelegate)
/// Adds a delegate to receive SQL trace callbacks.
///
/// Registers an object conforming to ``ConnectionTraceDelegate`` to observe SQL statements as
/// they are executed by the connection.
///
/// - Parameter delegate: The trace delegate to add.
func add(trace delegate: ConnectionTraceDelegate)
/// Removes a previously added trace delegate.
///
/// Unregisters an object that was previously added with ``add(trace:)`` so it no longer
/// receives SQL trace callbacks.
///
/// - Parameter delegate: The trace delegate to remove.
func remove(trace delegate: ConnectionTraceDelegate)
// MARK: - Custom SQL Functions
/// Registers a custom SQLite function with the current connection.
///
/// The specified function type must be a subclass of ``Function/Scalar`` or
/// ``Function/Aggregate``. Once registered, the function becomes available in SQL queries
/// executed through this connection.
///
/// - Parameter function: The custom function type to register.
/// - Throws: ``SQLiteError`` if registration fails.
func add(function: Function.Type) throws(SQLiteError)
/// Unregisters a previously registered custom SQLite function.
///
/// The specified function type must match the one used during registration. After removal,
/// the function will no longer be available for use in SQL statements.
///
/// - Parameter function: The custom function type to unregister.
/// - Throws: ``SQLiteError`` if the function could not be unregistered.
func remove(function: Function.Type) throws(SQLiteError)
// MARK: - Statement Preparation
/// Prepares an SQL statement for execution.
///
/// Compiles the provided SQL query into a prepared statement associated with this connection.
/// Use the returned statement to bind parameters and execute queries safely and efficiently.
///
/// - Parameter query: The SQL query to prepare.
/// - Returns: A compiled statement ready for execution.
/// - Throws: ``SQLiteError`` if the statement could not be prepared.
///
/// - SeeAlso: [Compiling An SQL Statement](https://sqlite.org/c3ref/prepare.html)
func prepare(sql query: String) throws(SQLiteError) -> StatementProtocol
/// Prepares an SQL statement with custom compilation options.
///
/// Similar to ``prepare(sql:)`` but allows specifying additional compilation flags through
/// ``Statement/Options`` to control statement creation behavior.
///
/// - Parameters:
/// - query: The SQL query to prepare.
/// - options: Additional compilation options.
/// - Returns: A compiled statement ready for execution.
/// - Throws: ``SQLiteError`` if the statement could not be prepared.
///
/// - SeeAlso: [Compiling An SQL Statement](https://sqlite.org/c3ref/prepare.html)
func prepare(
sql query: String, options: Statement.Options
) throws(SQLiteError) -> StatementProtocol
// MARK: - SQL Execution
/// Executes one or more SQL statements in a single step.
///
/// The provided SQL string may contain one or more statements separated by semicolons.
/// Each statement is compiled and executed sequentially within the current connection.
/// This method is suitable for operations that do not produce result sets, such as
/// `CREATE TABLE`, `INSERT`, `UPDATE`, or `PRAGMA`.
///
/// Execution stops at the first error, and the corresponding ``SQLiteError`` is thrown.
///
/// - Parameter script: The SQL text containing one or more statements to execute.
/// - Throws: ``SQLiteError`` if any statement fails to execute.
///
/// - SeeAlso: [One-Step Query Execution Interface](https://sqlite.org/c3ref/exec.html)
func execute(sql script: String) throws(SQLiteError)
// MARK: - PRAGMA Control
/// Reads the current value of a database PRAGMA.
///
/// Retrieves the value of the specified PRAGMA and attempts to convert it to the provided
/// generic type `T`. This method is typically used for reading configuration or status values
/// such as `journal_mode`, `foreign_keys`, or `user_version`.
///
/// If the PRAGMA query succeeds but the value cannot be converted to the requested type,
/// the method returns `nil` instead of throwing an error.
///
/// - Parameter pragma: The PRAGMA to query.
/// - Returns: The current PRAGMA value, or `nil` if the result is empty or conversion fails.
/// - Throws: ``SQLiteError`` if the PRAGMA query itself fails.
///
/// - SeeAlso: [PRAGMA Statements](https://sqlite.org/pragma.html)
func get<T: SQLiteRepresentable>(pragma: Pragma) throws(SQLiteError) -> T?
/// Sets a database PRAGMA value.
///
/// Assigns the specified value to the given PRAGMA. This can be used to change runtime
/// configuration parameters, such as `foreign_keys`, `journal_mode`, or `synchronous`.
///
/// - Parameters:
/// - pragma: The PRAGMA to set.
/// - value: The value to assign to the PRAGMA.
/// - Throws: ``SQLiteError`` if the assignment fails.
///
/// - SeeAlso: [PRAGMA Statements](https://sqlite.org/pragma.html)
func set<T: SQLiteRepresentable>(pragma: Pragma, value: T) throws(SQLiteError)
// MARK: - Transactions
/// Begins a new transaction of the specified type.
///
/// Starts an explicit transaction using the given ``TransactionType``. If a transaction is
/// already active, this method throws an error.
///
/// - Parameter type: The transaction type to begin.
/// - Throws: ``SQLiteError`` if the transaction could not be started.
///
/// - SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html)
func beginTransaction(_ type: TransactionType) throws(SQLiteError)
/// Commits the current transaction.
///
/// Makes all changes made during the transaction permanent. If no transaction is active, this
/// method has no effect.
///
/// - Throws: ``SQLiteError`` if the commit operation fails.
///
/// - SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html)
func commitTransaction() throws(SQLiteError)
/// Rolls back the current transaction.
///
/// Reverts all changes made during the transaction. If no transaction is active, this method
/// has no effect.
///
/// - Throws: ``SQLiteError`` if the rollback operation fails.
///
/// - SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html)
func rollbackTransaction() throws(SQLiteError)
}
// MARK: - Default Implementation
public extension ConnectionProtocol {
var busyTimeout: Int32 {
get { try! get(pragma: .busyTimeout) ?? 0 }
set { try! set(pragma: .busyTimeout, value: newValue) }
}
var applicationID: Int32 {
get { try! get(pragma: .applicationID) ?? 0 }
set { try! set(pragma: .applicationID, value: newValue) }
}
var foreignKeys: Bool {
get { try! get(pragma: .foreignKeys) ?? false }
set { try! set(pragma: .foreignKeys, value: newValue) }
}
var journalMode: JournalMode {
get { try! get(pragma: .journalMode) ?? .off }
set { try! set(pragma: .journalMode, value: newValue) }
}
var synchronous: Synchronous {
get { try! get(pragma: .synchronous) ?? .off }
set { try! set(pragma: .synchronous, value: newValue) }
}
var userVersion: Int32 {
get { try! get(pragma: .userVersion) ?? 0 }
set { try! set(pragma: .userVersion, value: newValue) }
}
func prepare(sql query: String) throws(SQLiteError) -> StatementProtocol {
try prepare(sql: query, options: [])
}
func get<T: SQLiteRepresentable>(pragma: Pragma) throws(SQLiteError) -> T? {
let stmt = try prepare(sql: "PRAGMA \(pragma)")
switch try stmt.step() {
case true: return stmt.columnValue(at: 0)
case false: return nil
}
}
func set<T: SQLiteRepresentable>(pragma: Pragma, value: T) throws(SQLiteError) {
let query = "PRAGMA \(pragma) = \(value.sqliteLiteral)"
try prepare(sql: query).step()
}
func beginTransaction(_ type: TransactionType = .deferred) throws(SQLiteError) {
try prepare(sql: "BEGIN \(type) TRANSACTION", options: []).step()
}
func commitTransaction() throws(SQLiteError) {
try prepare(sql: "COMMIT TRANSACTION", options: []).step()
}
func rollbackTransaction() throws(SQLiteError) {
try prepare(sql: "ROLLBACK TRANSACTION", options: []).step()
}
}