Refactor entire codebase and rewrite documentation

This commit is contained in:
2025-10-10 18:06:34 +03:00
parent b4e9755c15
commit 8e471f2b9f
74 changed files with 3405 additions and 4149 deletions

View File

@@ -1,65 +0,0 @@
import Foundation
import DataLiteC
extension Connection {
/// Represents an error encountered when interacting with the underlying database engine.
///
/// This type encapsulates SQLite-specific error codes and messages returned
/// from a `Connection` instance. It is used throughout the system to report
/// failures related to database operations.
///
/// ## Topics
///
/// ### Instance Properties
///
/// - ``code``
/// - ``message``
/// - ``description``
///
/// ### Initializers
///
/// - ``init(code:message:)``
public struct Error: Swift.Error, Equatable, CustomStringConvertible {
// MARK: - Properties
/// The database engine error code.
///
/// This code indicates the specific error returned by SQLite during an operation.
/// For a full list of possible error codes, see:
/// [SQLite Result and Error Codes](https://www.sqlite.org/rescode.html).
public let code: Int32
/// A human-readable error message describing the failure.
public let message: String
/// A textual representation of the error.
///
/// Combines the error code and message into a single descriptive string.
public var description: String {
"Connection.Error code: \(code) message: \(message)"
}
// MARK: - Initialization
/// Creates an error with the given code and message.
///
/// - Parameters:
/// - code: The SQLite error code.
/// - message: A description of the error.
public init(code: Int32, message: String) {
self.code = code
self.message = message
}
/// Creates an error by extracting details from a SQLite connection.
///
/// - Parameter connection: A pointer to the SQLite connection.
///
/// This initializer reads the extended error code and error message
/// from the provided SQLite connection pointer.
init(_ connection: OpaquePointer) {
self.code = sqlite3_extended_errcode(connection)
self.message = String(cString: sqlite3_errmsg(connection))
}
}
}

View File

@@ -1,41 +1,39 @@
import Foundation
extension Connection {
/// An encryption key for accessing an encrypted SQLite database.
/// An encryption key for opening an encrypted SQLite database.
///
/// Used after the connection is opened to unlock the contents of the database.
/// Two formats are supported: a passphrase with subsequent derivation, and
/// a raw 256-bit key (32 bytes) without transformation.
/// The key is applied after the connection is established to unlock the database contents.
/// Two formats are supported:
/// - a passphrase, which undergoes key derivation;
/// - a raw 256-bit key (32 bytes) passed without transformation.
public enum Key {
/// A passphrase used to derive an encryption key.
/// A human-readable passphrase used for key derivation.
///
/// Intended for human-readable strings such as passwords or PIN codes.
/// The string is passed directly without escaping or quoting.
/// The passphrase is supplied as-is and processed by the underlying key derivation
/// mechanism configured in the database engine.
case passphrase(String)
/// A raw 256-bit encryption key (32 bytes).
///
/// No key derivation is performed. The key is passed as-is and must be
/// securely generated and stored.
/// The key is passed directly to the database without derivation. It must be securely
/// generated and stored.
case rawKey(Data)
/// The string value to be passed to the database engine.
/// The string value passed to the database engine.
///
/// For `.passphrase`, this is the raw string exactly as provided.
/// For `.rawKey`, this is a hexadecimal literal in the format `X'...'`.
/// For `.passphrase`, returns the passphrase exactly as provided.
/// For `.rawKey`, returns a hexadecimal literal in the format `X'...'`.
public var keyValue: String {
switch self {
case .passphrase(let string):
return string
string
case .rawKey(let data):
return data.sqliteLiteral
data.sqliteLiteral
}
}
/// The length of the key value in bytes.
///
/// Returns the number of bytes in the UTF-8 encoding of `keyValue`,
/// not the length of the original key or string.
/// The number of bytes in the string representation of the key.
public var length: Int32 {
Int32(keyValue.utf8.count)
}

View File

@@ -1,140 +1,43 @@
import Foundation
extension Connection {
/// The `Location` enum represents different locations for a SQLite database.
/// A location specifying where the SQLite database is stored or created.
///
/// This enum allows you to specify how and where a SQLite database will be stored or accessed.
/// You can choose from three options:
///
/// - **File**: A database located at a specified file path or URI. This option is suitable
/// for persistent storage and can reference any valid file location in the filesystem or
/// a URI.
///
/// - **In-Memory**: An in-memory database that exists only in RAM. This option is useful
/// for temporary data processing, testing, or scenarios where persistence is not required.
///
/// - **Temporary**: A temporary database on disk that is created for the duration of the
/// connection and is automatically deleted when the connection is closed or when the
/// process ends.
///
/// ### Usage
///
/// You can create instances of the `Location` enum to specify the desired database location:
///
/// ```swift
/// let fileLocation = Connection.Location.file(path: "/path/to/database.db")
/// let inMemoryLocation = Connection.Location.inMemory
/// let temporaryLocation = Connection.Location.temporary
/// ```
/// Three locations are supported:
/// - ``file(path:)``: A database at a specific file path or URI (persistent).
/// - ``inMemory``: An in-memory database that exists only in RAM.
/// - ``temporary``: A temporary on-disk database deleted when the connection closes.
public enum Location {
/// A database located at a given file path or URI.
/// A database stored at a given file path or URI.
///
/// This case allows you to specify the exact location of a SQLite database using a file
/// path or a URI. The provided path should point to a valid SQLite database file. If the
/// database file does not exist, the behavior will depend on the connection options
/// specified when opening the database.
/// Use this for persistent databases located on disk or referenced via SQLite URI.
/// The file is created if it does not exist (subject to open options).
///
/// - Parameter path: The path or URI to the database file. This can be an absolute or
/// relative path, or a URI scheme supported by SQLite.
///
/// ### Example
///
/// You can create a `Location.file` case as follows:
///
/// ```swift
/// let databaseLocation = Connection.Location.file(path: "/path/to/database.db")
/// ```
///
/// - Important: Ensure that the specified path is correct and that your application has
/// the necessary permissions to access the file.
///
/// For more details, refer to [Uniform Resource Identifiers](https://www.sqlite.org/uri.html).
/// - Parameter path: Absolute/relative file path or URI.
/// - SeeAlso: [Uniform Resource Identifiers](https://sqlite.org/uri.html)
case file(path: String)
/// An in-memory database.
/// A transient in-memory database.
///
/// In-memory databases are temporary and exist only in RAM. They are not persisted to disk,
/// which makes them suitable for scenarios where you need fast access to data without the
/// overhead of disk I/O.
/// The database exists only in RAM and is discarded once the connection closes.
/// Suitable for testing, caching, or temporary data processing.
///
/// When you create an in-memory database, it is stored entirely in memory, meaning that
/// all data will be lost when the connection is closed or the application exits.
///
/// ### Usage
///
/// You can specify an in-memory database as follows:
///
/// ```swift
/// let databaseLocation = Connection.Location.inMemory
/// ```
///
/// - Important: In-memory databases should only be used for scenarios where persistence is
/// not required, such as temporary data processing or testing.
///
/// - Note: In-memory databases can provide significantly faster performance compared to
/// disk-based databases due to the absence of disk I/O operations.
///
/// For more details, refer to [In-Memory Databases](https://www.sqlite.org/inmemorydb.html).
/// - SeeAlso: [In-Memory Databases](https://sqlite.org/inmemorydb.html)
case inMemory
/// A temporary database on disk.
/// A temporary on-disk database.
///
/// Temporary databases are created on disk but are not intended for persistent storage. They
/// are automatically deleted when the connection is closed or when the process ends. This
/// allows you to use a database for temporary operations without worrying about the overhead
/// of file management.
/// Created on disk and removed automatically when the connection closes or the
/// process terminates. Useful for ephemeral data that should not persist.
///
/// Temporary databases can be useful for scenarios such as:
/// - Testing database operations without affecting permanent data.
/// - Storing transient data that only needs to be accessible during a session.
///
/// ### Usage
///
/// You can specify a temporary database as follows:
///
/// ```swift
/// let databaseLocation = Connection.Location.temporary
/// ```
///
/// - Important: Since temporary databases are deleted when the connection is closed, make
/// sure to use this option only for non-persistent data requirements.
///
/// For more details, refer to [Temporary Databases](https://www.sqlite.org/inmemorydb.html).
/// - SeeAlso: [Temporary Databases](https://sqlite.org/inmemorydb.html)
case temporary
/// Returns the path to the database.
///
/// This computed property provides the appropriate path representation for the selected
/// `Location` case. Depending on the case, it returns:
/// - The specified file path for `.file`.
/// - The string `":memory:"` for in-memory databases, indicating that the database exists
/// only in RAM.
/// - An empty string for temporary databases, as these are created on disk but do not
/// require a specific file path.
///
/// ### Usage
///
/// You can access the `path` property as follows:
///
/// ```swift
/// let location = Connection.Location.file(path: "/path/to/database.db")
/// let databasePath = location.path // "/path/to/database.db"
///
/// let inMemoryLocation = Connection.Location.inMemory
/// let inMemoryPath = inMemoryLocation.path // ":memory:"
///
/// let temporaryLocation = Connection.Location.temporary
/// let temporaryPath = temporaryLocation.path // ""
/// ```
///
/// - Note: When using the `.temporary` case, the returned value is an empty string
/// because the database is created as a temporary file that does not have a
/// persistent path.
var path: String {
switch self {
case .file(let path): return path
case .inMemory: return ":memory:"
case .temporary: return ""
case .file(let path): path
case .inMemory: ":memory:"
case .temporary: ""
}
}
}

View File

@@ -2,437 +2,98 @@ import Foundation
import DataLiteC
extension Connection {
/// Options for controlling the connection to a SQLite database.
/// Options that control how the SQLite database connection is opened.
///
/// This type represents a set of options that can be used when opening a connection to a
/// SQLite database. Each option corresponds to one of the flags defined in the SQLite
/// library. For more details, read [Opening A New Database Connection](https://www.sqlite.org/c3ref/open.html).
/// Each option corresponds to a flag from the SQLite C API. Multiple options can be combined
/// using the `OptionSet` syntax.
///
/// ### Usage
///
/// ```swift
/// do {
/// let dbFilePath = "path/to/your/database.db"
/// let options: Connection.Options = [.readwrite, .create]
/// let connection = try Connection(path: dbFilePath, options: options)
/// print("Database connection established successfully!")
/// } catch {
/// print("Error opening database: \(error)")
/// }
/// ```
///
/// ## Topics
///
/// ### Initializers
///
/// - ``init(rawValue:)``
///
/// ### Instance Properties
///
/// - ``rawValue``
///
/// ### Type Properties
///
/// - ``readonly``
/// - ``readwrite``
/// - ``create``
/// - ``uri``
/// - ``memory``
/// - ``nomutex``
/// - ``fullmutex``
/// - ``sharedcache``
/// - ``privatecache``
/// - ``exrescode``
/// - ``nofollow``
/// - SeeAlso: [Opening A New Database Connection](https://sqlite.org/c3ref/open.html)
public struct Options: OptionSet, Sendable {
// MARK: - Properties
/// An integer value representing a combination of option flags.
///
/// This property holds the raw integer representation of the selected options for the
/// SQLite database connection. Each option corresponds to a specific flag defined in the
/// SQLite library, allowing for a flexible and efficient way to specify multiple options
/// using bitwise operations. The value can be combined using the bitwise OR operator (`|`).
///
/// ```swift
/// let options = [
/// Connection.Options.readonly,
/// Connection.Options.create
/// ]
/// ```
///
/// In this example, the `rawValue` will represent a combination of the ``readonly`` and ``create`` options.
///
/// - Important: When combining options, ensure that the selected flags are compatible and do not conflict,
/// as certain combinations may lead to unexpected behavior. For example, setting both ``readonly`` and
/// ``readwrite`` is not allowed.
/// The raw integer value representing the option flags.
public var rawValue: Int32
// MARK: - Instances
/// Option: open the database for read-only access.
///
/// This option configures the SQLite database connection to be opened in read-only mode. When this
/// option is specified, the database can be accessed for querying, but any attempts to modify the
/// data (such as inserting, updating, or deleting records) will result in an error. If the specified
/// database file does not already exist, an error will also be returned.
///
/// This is particularly useful when you want to ensure that your application does not accidentally
/// modify the database, or when you need to work with a database that is being shared among multiple
/// processes or applications that require read-only access.
///
/// ### Usage
///
/// You can specify the `readonly` option when opening a database connection, as shown in the example:
///
/// ```swift
/// let options: Connection.Options = [.readonly]
/// let connection = try Connection(path: dbFilePath, options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: If you attempt to write to a read-only database, an error will be thrown.
///
/// - Note: Ensure that the database file exists before opening it in read-only mode, as the connection
/// will fail if the file does not exist.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let readonly = Self(rawValue: SQLITE_OPEN_READONLY)
/// Option: open the database for reading and writing.
///
/// This option configures the SQLite database connection to be opened in read-write mode. When this
/// option is specified, the database can be accessed for both querying and modifying data. This means
/// you can perform operations such as inserting, updating, or deleting records in addition to reading.
///
/// If the database file does not exist, an error will be returned. If the file is write-protected by
/// the operating system, the connection will be opened in read-only mode instead, as a fallback.
///
/// ### Usage
///
/// You can specify the `readwrite` option when opening a database connection, as shown below:
///
/// ```swift
/// let options: Connection.Options = [.readwrite]
/// let connection = try Connection(path: dbFilePath, options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: If the database file does not exist, an error will be thrown.
/// - Note: If you are unable to open the database in read-write mode due to permissions, it will
/// attempt to open it in read-only mode.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let readwrite = Self(rawValue: SQLITE_OPEN_READWRITE)
/// Option: create the database if it does not exist.
///
/// This option instructs SQLite to create a new database file if it does not already exist. If the
/// specified database file already exists, the connection will open that existing database instead.
///
/// ### Usage
///
/// You can specify the `create` option when opening a database connection, as shown below:
///
/// ```swift
/// let options: Connection.Options = [.create]
/// let connection = try Connection(path: dbFilePath, options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: If the database file exists, it will be opened normally, and no new file will be created.
/// - Note: If the database file does not exist, a new file will be created at the specified path.
///
/// This option is often used in conjunction with other options, such as `readwrite`, to ensure that a
/// new database can be created and written to right away.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let create = Self(rawValue: SQLITE_OPEN_CREATE)
/// Option: specify a URI for opening the database.
///
/// This option allows the filename provided to be interpreted as a Uniform Resource Identifier (URI).
/// When this flag is set, SQLite will parse the filename as a URI, enabling the use of URI features
/// such as special encoding and various URI schemes.
///
/// ### Usage
///
/// You can specify the `uri` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.uri]
/// let connection = try Connection(path: "file:///path/to/database.db", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: Using this option allows you to take advantage of SQLite's URI capabilities, such as
/// specifying various parameters in the URI (e.g., caching, locking, etc.).
/// - Note: If this option is not set, the filename will be treated as a simple path without URI
/// interpretation.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let uri = Self(rawValue: SQLITE_OPEN_URI)
/// Option: open the database in memory.
///
/// This option opens the database as an in-memory database, meaning that all data is stored in RAM
/// rather than on disk. This can be useful for temporary databases or for testing purposes where
/// persistence is not required.
///
/// When using this option, the "filename" argument is ignored, but it is still used for cache-sharing
/// if shared cache mode is enabled.
///
/// ### Usage
///
/// You can specify the `memory` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.memory]
/// let connection = try Connection(path: ":memory:", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: Since the database is stored in memory, all data will be lost when the connection is
/// closed or the program exits. Therefore, this option is best suited for scenarios where data
/// persistence is not necessary.
/// - Note: In-memory databases can be significantly faster than disk-based databases due to the
/// absence of disk I/O operations.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let memory = Self(rawValue: SQLITE_OPEN_MEMORY)
/// Option: do not use mutexes.
///
/// This option configures the new database connection to use the "multi-thread"
/// [threading mode](https://www.sqlite.org/threadsafe.html). In this mode, separate threads can
/// concurrently access SQLite, provided that each thread is utilizing a different
/// [database connection](https://www.sqlite.org/c3ref/sqlite3.html).
///
/// ### Usage
///
/// You can specify the `nomutex` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.nomutex]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: When using this option, ensure that each thread has its own database connection, as
/// concurrent access to the same connection is not safe.
/// - Note: This option can improve performance in multi-threaded applications by reducing the
/// overhead of mutex locking, but it may lead to undefined behavior if not used carefully.
/// - Note: If your application requires safe concurrent access to a single database connection
/// from multiple threads, consider using the ``fullmutex`` option instead.
///
/// For more details, refer to the SQLite documentation on
/// [thread safety](https://www.sqlite.org/threadsafe.html).
public static let nomutex = Self(rawValue: SQLITE_OPEN_NOMUTEX)
/// Option: use full mutexing.
///
/// This option configures the new database connection to utilize the "serialized"
/// [threading mode](https://www.sqlite.org/threadsafe.html). In this mode, multiple threads can safely
/// attempt to access the same database connection simultaneously. Although mutexes will block any
/// actual concurrency, this mode allows for multiple threads to operate without causing data corruption
/// or undefined behavior.
///
/// ### Usage
///
/// You can specify the `fullmutex` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.fullmutex]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: Using the `fullmutex` option is recommended when you need to ensure thread safety when
/// multiple threads access the same database connection.
/// - Note: This option may introduce some performance overhead due to the locking mechanisms in place.
/// If your application is designed for high concurrency and can manage separate connections per thread,
/// consider using the ``nomutex`` option for better performance.
/// - Note: It's essential to be aware of potential deadlocks if multiple threads are competing for the
/// same resources. Proper design can help mitigate these risks.
///
/// For more details, refer to the SQLite documentation on
/// [thread safety](https://www.sqlite.org/threadsafe.html).
public static let fullmutex = Self(rawValue: SQLITE_OPEN_FULLMUTEX)
/// Option: use a shared cache.
///
/// This option enables the database to be opened in [shared cache](https://www.sqlite.org/sharedcache.html)
/// mode. In this mode, multiple database connections can share cached data, potentially improving
/// performance when accessing the same database from different connections.
///
/// ### Usage
///
/// You can specify the `sharedcache` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.sharedcache]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: **Discouraged Usage**: The use of shared cache mode is
/// [discouraged](https://www.sqlite.org/sharedcache.html#dontuse). It may lead to unpredictable behavior,
/// especially in applications with complex threading models or multiple database connections.
///
/// - Note: **Build Variability**: Shared cache capabilities may be omitted from many builds of SQLite.
/// If your SQLite build does not support shared cache, this option will be a no-op, meaning it will
/// have no effect on the behavior of your database connection.
///
/// - Note: **Performance Considerations**: While shared cache can improve performance by reducing memory
/// usage, it may introduce complexity in managing concurrent access. Consider your application's design
/// and the potential for contention among connections when using this option.
///
/// For more information, consult the SQLite documentation on
/// [shared cache mode](https://www.sqlite.org/sharedcache.html).
public static let sharedcache = Self(rawValue: SQLITE_OPEN_SHAREDCACHE)
/// Option: use a private cache.
///
/// This option disables the use of [shared cache](https://www.sqlite.org/sharedcache.html) mode.
/// When a database is opened with this option, it uses a private cache for its connections, meaning
/// that the cached data will not be shared with other database connections.
///
/// ### Usage
///
/// You can specify the `privatecache` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.privatecache]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: **Isolation**: Using a private cache ensures that the database connection operates in
/// isolation, preventing any caching interference from other connections. This can be beneficial
/// in multi-threaded applications where shared cache might lead to unpredictable behavior.
///
/// - Note: **Performance Impact**: While a private cache avoids the complexities associated with
/// shared caching, it may increase memory usage since each connection maintains its own cache.
/// Consider your applications performance requirements when choosing between shared and private
/// cache options.
///
/// - Note: **Build Compatibility**: Ensure that your SQLite build supports the private cache option.
/// While most builds do, its always a good idea to verify if you encounter any issues.
///
/// For more information, refer to the SQLite documentation on
/// [shared cache mode](https://www.sqlite.org/sharedcache.html).
public static let privatecache = Self(rawValue: SQLITE_OPEN_PRIVATECACHE)
/// Option: use extended result code mode.
///
/// This option enables "extended result code mode" for the database connection. When this mode is
/// enabled, SQLite provides additional error codes that can help in diagnosing issues that may
/// arise during database operations.
///
/// ### Usage
///
/// You can specify the `exrescode` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.exrescode]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Benefits
///
/// - **Improved Error Handling**: By using extended result codes, you can get more granular
/// information about errors, which can be particularly useful for debugging and error handling
/// in your application.
///
/// - **Detailed Diagnostics**: Extended result codes may provide context about the failure,
/// allowing for more targeted troubleshooting and resolution of issues.
///
/// ### Considerations
///
/// - **Compatibility**: Make sure your version of SQLite supports extended result codes. This
/// option should be available in most modern builds of SQLite.
///
/// For more information, refer to the SQLite documentation on
/// [extended result codes](https://www.sqlite.org/rescode.html).
public static let exrescode = Self(rawValue: SQLITE_OPEN_EXRESCODE)
/// Option: do not follow symbolic links when opening a file.
///
/// When this option is enabled, the database filename must not contain a symbolic link. If the
/// filename refers to a symbolic link, an error will be returned when attempting to open the
/// database.
///
/// ### Usage
///
/// You can specify the `nofollow` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.nofollow]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Benefits
///
/// - **Increased Security**: By disallowing symbolic links, you reduce the risk of unintended
/// file access or manipulation through links that may point to unexpected locations.
///
/// - **File Integrity**: Ensures that the database connection directly references the intended
/// file without any indirection that symbolic links could introduce.
///
/// ### Considerations
///
/// - **Filesystem Limitations**: This option may limit your ability to use symbolic links in
/// your application. Make sure this behavior is acceptable for your use case.
///
/// For more information, refer to the SQLite documentation on [file opening](https://www.sqlite.org/c3ref/open.html).
public static let nofollow = Self(rawValue: SQLITE_OPEN_NOFOLLOW)
// MARK: - Inits
/// Initializes a set of options for connecting to a SQLite database.
/// Creates a new set of options from a raw integer value.
///
/// This initializer allows you to create a combination of option flags that dictate how the
/// database connection will behave. The `rawValue` parameter should be an integer that
/// represents one or more options, combined using a bitwise OR operation.
///
/// - Parameter rawValue: An integer value representing a combination of option flags. This
/// value can be constructed using the predefined options, e.g., `SQLITE_OPEN_READWRITE |
/// SQLITE_OPEN_CREATE`.
///
/// ### Example
///
/// You can create a set of options as follows:
/// Combine multiple flags using bitwise OR (`|`).
///
/// ```swift
/// let options = Connection.Options(
/// let opts = Connection.Options(
/// rawValue: SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
/// )
/// ```
///
/// In this example, the `options` variable will have both the ``readwrite`` and
/// ``create`` options enabled, allowing for read/write access and creating the database if
/// it does not exist.
///
/// ### Important Notes
///
/// - Note: Be cautious when combining options, as some combinations may lead to conflicts or
/// unintended behavior (e.g., ``readonly`` and ``readwrite`` cannot be set together).
public init(rawValue: Int32) {
self.rawValue = rawValue
}
// MARK: - Instances
/// Opens the database in read-only mode.
///
/// Fails if the database file does not exist.
public static let readonly = Self(rawValue: SQLITE_OPEN_READONLY)
/// Opens the database for reading and writing.
///
/// Fails if the file does not exist or is write-protected.
public static let readwrite = Self(rawValue: SQLITE_OPEN_READWRITE)
/// Creates the database file if it does not exist.
///
/// Commonly combined with `.readwrite`.
public static let create = Self(rawValue: SQLITE_OPEN_CREATE)
/// Interprets the filename as a URI.
///
/// Enables SQLites URI parameters and schemes.
/// - SeeAlso: [Uniform Resource Identifiers](https://sqlite.org/uri.html)
public static let uri = Self(rawValue: SQLITE_OPEN_URI)
/// Opens an in-memory database.
///
/// Data is stored in RAM and discarded when closed.
/// - SeeAlso: [In-Memory Databases](https://sqlite.org/inmemorydb.html)
public static let memory = Self(rawValue: SQLITE_OPEN_MEMORY)
/// Disables mutexes for higher concurrency.
///
/// Each thread must use a separate connection.
/// - SeeAlso: [Using SQLite In Multi-Threaded Applications](
/// https://sqlite.org/threadsafe.html)
public static let nomutex = Self(rawValue: SQLITE_OPEN_NOMUTEX)
/// Enables serialized access using full mutexes.
///
/// Safe for concurrent access from multiple threads.
/// - SeeAlso: [Using SQLite In Multi-Threaded Applications](
/// https://sqlite.org/threadsafe.html)
public static let fullmutex = Self(rawValue: SQLITE_OPEN_FULLMUTEX)
/// Enables shared cache mode.
///
/// Allows multiple connections to share cached data.
/// - SeeAlso: [SQLite Shared-Cache Mode](https://sqlite.org/sharedcache.html)
/// - Warning: Shared cache mode is discouraged by SQLite.
public static let sharedcache = Self(rawValue: SQLITE_OPEN_SHAREDCACHE)
/// Disables shared cache mode.
///
/// Each connection uses a private cache.
/// - SeeAlso: [SQLite Shared-Cache Mode](https://sqlite.org/sharedcache.html)
public static let privatecache = Self(rawValue: SQLITE_OPEN_PRIVATECACHE)
/// Enables extended result codes.
///
/// Provides more detailed SQLite error codes.
/// - SeeAlso: [Result and Error Codes](https://sqlite.org/rescode.html)
public static let exrescode = Self(rawValue: SQLITE_OPEN_EXRESCODE)
/// Disallows following symbolic links.
///
/// Improves security by preventing indirect file access.
public static let nofollow = Self(rawValue: SQLITE_OPEN_NOFOLLOW)
}
}

View File

@@ -1,29 +1,79 @@
import Foundation
import DataLiteC
public final class Connection: ConnectionProtocol {
/// A class representing a connection to an SQLite database.
///
/// The `Connection` class manages the connection to an SQLite database. It provides an interface
/// for preparing SQL queries, managing transactions, and handling errors. This class serves as the
/// main object for interacting with the database.
public final class Connection {
// MARK: - Private Properties
private let connection: OpaquePointer
fileprivate var delegates = [DelegateBox]()
// MARK: - Connection State
public var isAutocommit: Bool {
sqlite3_get_autocommit(connection) != 0
fileprivate var delegates = [DelegateBox]() {
didSet {
switch (oldValue.isEmpty, delegates.isEmpty) {
case (true, false):
let ctx = Unmanaged.passUnretained(self).toOpaque()
sqlite3_update_hook(connection, updateHookCallback(_:_:_:_:_:), ctx)
sqlite3_commit_hook(connection, commitHookCallback(_:), ctx)
sqlite3_rollback_hook(connection, rollbackHookCallback(_:), ctx)
case (false, true):
sqlite3_update_hook(connection, nil, nil)
sqlite3_commit_hook(connection, nil, nil)
sqlite3_rollback_hook(connection, nil, nil)
default:
break
}
}
}
public var isReadonly: Bool {
sqlite3_db_readonly(connection, "main") == 1
}
public var busyTimeout: Int32 {
get { try! get(pragma: .busyTimeout) ?? 0 }
set { try! set(pragma: .busyTimeout, value: newValue) }
fileprivate var traceDelegates = [TraceDelegateBox]() {
didSet {
switch (oldValue.isEmpty, traceDelegates.isEmpty) {
case (true, false):
let ctx = Unmanaged.passUnretained(self).toOpaque()
sqlite3_trace_stmt(connection, traceCallback(_:_:_:_:), ctx)
case (false, true):
sqlite3_trace_stmt(connection, nil, nil)
default:
break
}
}
}
// MARK: - Inits
/// Initializes a new connection to an SQLite database.
///
/// Opens a connection to the database at the specified `location` using the given `options`.
/// If the location represents a file path, this method ensures that the parent directory
/// exists, creating intermediate directories if needed.
///
/// ### Example
///
/// ```swift
/// do {
/// let connection = try Connection(
/// location: .file(path: "/path/to/sqlite.db"),
/// options: .readwrite
/// )
/// // Use the connection to execute SQL statements
/// } catch {
/// print("Failed to open database: \\(error)")
/// }
/// ```
///
/// - Parameters:
/// - location: The location of the database. Can represent a file path, an in-memory
/// database, or a temporary database.
/// - options: Connection options that define behavior such as read-only mode, creation
/// flags, and cache type.
///
/// - Throws: ``SQLiteError`` if the connection cannot be opened or initialized due to
/// SQLite-related issues such as invalid path, missing permissions, or corruption.
/// - Throws: An error if directory creation fails for file-based database locations.
public init(location: Location, options: Options) throws {
if case let Location.file(path) = location, !path.isEmpty {
try FileManager.default.createDirectory(
@@ -35,21 +85,43 @@ public final class Connection: ConnectionProtocol {
var connection: OpaquePointer! = nil
let status = sqlite3_open_v2(location.path, &connection, options.rawValue, nil)
if status == SQLITE_OK, let connection = connection {
self.connection = connection
let ctx = Unmanaged.passUnretained(self).toOpaque()
sqlite3_trace_v2(connection, UInt32(SQLITE_TRACE_STMT), traceCallback(_:_:_:_:), ctx)
sqlite3_update_hook(connection, updateHookCallback(_:_:_:_:_:), ctx)
sqlite3_commit_hook(connection, commitHookCallback(_:), ctx)
sqlite3_rollback_hook(connection, rollbackHookCallback(_:), ctx)
} else {
let error = Error(connection)
guard status == SQLITE_OK, let connection else {
let error = SQLiteError(connection)
sqlite3_close_v2(connection)
throw error
}
self.connection = connection
}
/// Initializes a new connection to an SQLite database using a file path.
///
/// Opens a connection to the SQLite database located at the specified `path` using the provided
/// `options`. Internally, this method calls the designated initializer to perform the actual
/// setup and validation.
///
/// ### Example
///
/// ```swift
/// do {
/// let connection = try Connection(
/// path: "/path/to/sqlite.db",
/// options: .readwrite
/// )
/// // Use the connection to execute SQL statements
/// } catch {
/// print("Failed to open database: \\(error)")
/// }
/// ```
///
/// - Parameters:
/// - path: The file system path to the SQLite database file. Can be absolute or relative.
/// - options: Options that control how the database is opened, such as access mode and
/// cache type.
///
/// - Throws: ``SQLiteError`` if the connection cannot be opened due to SQLite-level errors,
/// invalid path, missing permissions, or corruption.
/// - Throws: An error if the required directory structure cannot be created.
public convenience init(path: String, options: Options) throws {
try self.init(location: .file(path: path), options: options)
}
@@ -57,73 +129,116 @@ public final class Connection: ConnectionProtocol {
deinit {
sqlite3_close_v2(connection)
}
// MARK: - Delegation
public func addDelegate(_ delegate: ConnectionDelegate) {
delegates.removeAll { $0.delegate == nil }
delegates.append(.init(delegate: delegate))
}
// MARK: - ConnectionProtocol
extension Connection: ConnectionProtocol {
public var isAutocommit: Bool {
sqlite3_get_autocommit(connection) != 0
}
public func removeDelegate(_ delegate: ConnectionDelegate) {
delegates.removeAll { $0.delegate == nil || $0.delegate === delegate }
public var isReadonly: Bool {
sqlite3_db_readonly(connection, "main") == 1
}
// MARK: - Custom SQL Functions
public func add(function: Function.Type) throws(Error) {
try function.install(db: connection)
}
public func remove(function: Function.Type) throws(Error) {
try function.uninstall(db: connection)
}
// MARK: - Statement Preparation
public func prepare(sql query: String, options: Statement.Options = []) throws(Error) -> Statement {
try Statement(db: connection, sql: query, options: options)
}
// MARK: - Script Execution
public func execute(raw sql: String) throws(Error) {
let status = sqlite3_exec(connection, sql, nil, nil, nil)
if status != SQLITE_OK {
throw Error(connection)
public static func initialize() throws(SQLiteError) {
let status = sqlite3_initialize()
guard status == SQLITE_OK else {
throw SQLiteError(code: status, message: "")
}
}
// MARK: - Encryption Keys
public static func shutdown() throws(SQLiteError) {
let status = sqlite3_shutdown()
guard status == SQLITE_OK else {
throw SQLiteError(code: status, message: "")
}
}
public func apply(_ key: Key, name: String? = nil) throws(Error) {
public func apply(_ key: Key, name: String?) throws(SQLiteError) {
let status = if let name {
sqlite3_key_v2(connection, name, key.keyValue, key.length)
} else {
sqlite3_key(connection, key.keyValue, key.length)
}
if status != SQLITE_OK {
throw Error(connection)
guard status == SQLITE_OK else {
throw SQLiteError(connection)
}
}
public func rekey(_ key: Key, name: String? = nil) throws(Error) {
public func rekey(_ key: Key, name: String?) throws(SQLiteError) {
let status = if let name {
sqlite3_rekey_v2(connection, name, key.keyValue, key.length)
} else {
sqlite3_rekey(connection, key.keyValue, key.length)
}
if status != SQLITE_OK {
throw Error(connection)
guard status == SQLITE_OK else {
throw SQLiteError(connection)
}
}
public func add(delegate: any ConnectionDelegate) {
delegates.append(.init(delegate: delegate))
delegates.removeAll { $0.delegate == nil }
}
public func remove(delegate: any ConnectionDelegate) {
delegates.removeAll {
$0.delegate === delegate || $0.delegate == nil
}
}
public func add(trace delegate: any ConnectionTraceDelegate) {
traceDelegates.append(.init(delegate: delegate))
traceDelegates.removeAll { $0.delegate == nil }
}
public func remove(trace delegate: any ConnectionTraceDelegate) {
traceDelegates.removeAll {
$0.delegate === delegate || $0.delegate == nil
}
}
public func add(function: Function.Type) throws(SQLiteError) {
try function.install(db: connection)
}
public func remove(function: Function.Type) throws(SQLiteError) {
try function.uninstall(db: connection)
}
public func prepare(
sql query: String, options: Statement.Options
) throws(SQLiteError) -> any StatementProtocol {
try Statement(db: connection, sql: query, options: options)
}
public func execute(raw sql: String) throws(SQLiteError) {
let status = sqlite3_exec(connection, sql, nil, nil, nil)
guard status == SQLITE_OK else { throw SQLiteError(connection) }
}
}
// MARK: - DelegateBox
fileprivate extension Connection {
class DelegateBox {
weak var delegate: ConnectionDelegate?
init(delegate: ConnectionDelegate? = nil) {
init(delegate: ConnectionDelegate) {
self.delegate = delegate
}
}
}
// MARK: - TraceDelegateBox
fileprivate extension Connection {
class TraceDelegateBox {
weak var delegate: ConnectionTraceDelegate?
init(delegate: ConnectionTraceDelegate) {
self.delegate = delegate
}
}
@@ -131,28 +246,60 @@ fileprivate extension Connection {
// MARK: - Functions
private typealias TraceCallback = @convention(c) (
UInt32,
UnsafeMutableRawPointer?,
UnsafeMutableRawPointer?,
UnsafeMutableRawPointer?
) -> Int32
@discardableResult
private func sqlite3_trace_stmt(
_ db: OpaquePointer!,
_ callback: TraceCallback!,
_ ctx: UnsafeMutableRawPointer!
) -> Int32 {
sqlite3_trace_v2(db, SQLITE_TRACE_STMT, callback, ctx)
}
@discardableResult
private func sqlite3_trace_v2(
_ db: OpaquePointer!,
_ mask: Int32,
_ callback: TraceCallback!,
_ ctx: UnsafeMutableRawPointer!
) -> Int32 {
sqlite3_trace_v2(db, UInt32(mask), callback, ctx)
}
private func traceCallback(
_ flag: UInt32,
_ ctx: UnsafeMutableRawPointer?,
_ p: UnsafeMutableRawPointer?,
_ x: UnsafeMutableRawPointer?
) -> Int32 {
guard let ctx = ctx else { return SQLITE_OK }
guard let ctx,
let stmt = OpaquePointer(p)
else { return SQLITE_OK }
let connection = Unmanaged<Connection>
.fromOpaque(ctx)
.takeUnretainedValue()
guard !connection.delegates.isEmpty,
let stmt = OpaquePointer(p),
let pSql = sqlite3_expanded_sql(stmt),
let xSql = x?.assumingMemoryBound(to: CChar.self)
else { return SQLITE_OK }
let xSql = x?.assumingMemoryBound(to: CChar.self)
let pSql = sqlite3_expanded_sql(stmt)
defer { sqlite3_free(pSql) }
guard let xSql, let pSql else {
return SQLITE_OK
}
let pSqlString = String(cString: pSql)
let xSqlString = String(cString: xSql)
let pSqlString = String(cString: pSql)
let trace = (xSqlString, pSqlString)
for box in connection.delegates {
for box in connection.traceDelegates {
box.delegate?.connection(connection, trace: trace)
}
@@ -166,37 +313,36 @@ private func updateHookCallback(
_ tName: UnsafePointer<CChar>?,
_ rowID: sqlite3_int64
) {
guard let ctx = ctx else { return }
guard let ctx else { return }
let connection = Unmanaged<Connection>
.fromOpaque(ctx)
.takeUnretainedValue()
if !connection.delegates.isEmpty {
guard let dName = dName, let tName = tName else { return }
let dbName = String(cString: dName)
let tableName = String(cString: tName)
let updateAction: SQLiteAction
switch action {
case SQLITE_INSERT:
updateAction = .insert(db: dbName, table: tableName, rowID: rowID)
case SQLITE_UPDATE:
updateAction = .update(db: dbName, table: tableName, rowID: rowID)
case SQLITE_DELETE:
updateAction = .delete(db: dbName, table: tableName, rowID: rowID)
default:
return
}
for box in connection.delegates {
box.delegate?.connection(connection, didUpdate: updateAction)
}
guard let dName = dName, let tName = tName else { return }
let dbName = String(cString: dName)
let tableName = String(cString: tName)
let updateAction: SQLiteAction? = switch action {
case SQLITE_INSERT: .insert(db: dbName, table: tableName, rowID: rowID)
case SQLITE_UPDATE: .update(db: dbName, table: tableName, rowID: rowID)
case SQLITE_DELETE: .delete(db: dbName, table: tableName, rowID: rowID)
default: nil
}
guard let updateAction else { return }
for box in connection.delegates {
box.delegate?.connection(connection, didUpdate: updateAction)
}
}
private func commitHookCallback(_ ctx: UnsafeMutableRawPointer?) -> Int32 {
private func commitHookCallback(
_ ctx: UnsafeMutableRawPointer?
) -> Int32 {
guard let ctx = ctx else { return SQLITE_OK }
let connection = Unmanaged<Connection>
.fromOpaque(ctx)
.takeUnretainedValue()
@@ -211,8 +357,11 @@ private func commitHookCallback(_ ctx: UnsafeMutableRawPointer?) -> Int32 {
}
}
private func rollbackHookCallback(_ ctx: UnsafeMutableRawPointer?) {
private func rollbackHookCallback(
_ ctx: UnsafeMutableRawPointer?
) {
guard let ctx = ctx else { return }
let connection = Unmanaged<Connection>
.fromOpaque(ctx)
.takeUnretainedValue()

View File

@@ -2,24 +2,23 @@ import Foundation
import DataLiteC
extension Function {
/// Base class for creating custom SQLite aggregate functions.
/// Base class for defining custom SQLite aggregate functions.
///
/// This class provides a basic implementation for creating aggregate functions in SQLite.
/// Aggregate functions process a set of input values and return a single value.
/// To create a custom aggregate function, subclass `Function.Aggregate` and override the
/// following properties and methods:
/// The `Aggregate` class provides a foundation for creating aggregate
/// functions in SQLite. Aggregate functions operate on multiple rows of
/// input and return a single result value.
///
/// - ``name``: The name of the function used in SQL queries.
/// - ``argc``: The number of arguments the function accepts.
/// - ``options``: Options for the function, such as `deterministic` and `innocuous`.
/// - ``step(args:)``: Method called for each input value.
/// - ``finalize()``: Method called after processing all input values.
/// To define a custom aggregate function, subclass `Function.Aggregate` and
/// override the following:
///
/// - ``name`` The SQL name of the function.
/// - ``argc`` The number of arguments accepted by the function.
/// - ``options`` Function options, such as `.deterministic` or `.innocuous`.
/// - ``step(args:)`` Called for each row's argument values.
/// - ``finalize()`` Called once to compute and return the final result.
///
/// ### Example
///
/// This example shows how to create a custom aggregate function to calculate the sum
/// of integers.
///
/// ```swift
/// final class SumAggregate: Function.Aggregate {
/// enum Error: Swift.Error {
@@ -34,23 +33,20 @@ extension Function {
///
/// private var sum: Int = 0
///
/// override func step(args: Arguments) throws {
/// override func step(args: ArgumentsProtocol) throws {
/// guard let value = args[0] as Int? else {
/// throw Error.argumentsWrong
/// }
/// sum += value
/// }
///
/// override func finalize() throws -> SQLiteRawRepresentable? {
/// override func finalize() throws -> SQLiteRepresentable? {
/// return sum
/// }
/// }
/// ```
///
/// ### Usage
///
/// To use a custom aggregate function, first establish a database connection and
/// register the function.
/// ### Registration
///
/// ```swift
/// let connection = try Connection(
@@ -62,16 +58,13 @@ extension Function {
///
/// ### SQL Example
///
/// Example SQL query using the custom aggregate function to calculate the sum of
/// values in the `value` column of the `my_table`.
///
/// ```sql
/// SELECT sum_aggregate(value) FROM my_table
/// ```
///
/// ## Topics
///
/// ### Initializers
/// ### Initialization
///
/// - ``init()``
///
@@ -80,139 +73,23 @@ extension Function {
/// - ``step(args:)``
/// - ``finalize()``
open class Aggregate: Function {
// MARK: - Context
/// Helper class for storing and managing the context of a custom SQLite aggregate function.
///
/// This class is used to hold a reference to the `Aggregate` function implementation.
/// It is created and managed when the aggregate function is installed in the SQLite
/// database connection. The context is passed to SQLite and is used to invoke the
/// corresponding function implementation when called.
fileprivate final class Context {
// MARK: Properties
/// The type of the aggregate function managed by this context.
///
/// This property holds a reference to the subclass of `Aggregate` that implements
/// the custom aggregate function. It is used to create instances of the function
/// and manage its state during SQL query execution.
private let function: Aggregate.Type
// MARK: Inits
/// Initializes a new `Context` with a reference to the aggregate function type.
///
/// This initializer creates an instance of the `Context` class that will hold
/// a reference to the aggregate function type. It is used to manage state and
/// perform operations with the custom aggregate function in the SQLite context.
///
/// - Parameter function: The subclass of `Aggregate` implementing the custom
/// aggregate function. This parameter specifies which function type will be
/// used in the context.
///
/// - Note: The initializer establishes a link between the context and the function
/// type, allowing extraction of function instances and management of their state
/// during SQL query processing.
init(function: Aggregate.Type) {
self.function = function
}
// MARK: Methods
/// Retrieves or creates an instance of the aggregate function.
///
/// This method retrieves an existing instance of the aggregate function from the
/// SQLite context or creates a new one if it has not yet been created. The returned
/// instance allows management of the aggregate function's state during query execution.
///
/// - Parameter ctx: Pointer to the SQLite context associated with the current
/// query. This parameter is used to access the aggregate context where the
/// function state is stored.
///
/// - Returns: An unmanaged reference to the `Aggregate` instance.
///
/// - Note: The method checks whether an instance of the function already exists in
/// the context. If no instance is found, a new one is created and saved in the
/// context for use in subsequent calls.
func function(ctx: OpaquePointer?) -> Unmanaged<Aggregate> {
let stride = MemoryLayout<Unmanaged<Aggregate>>.stride
let functionBuffer = UnsafeMutableRawBufferPointer(
start: sqlite3_aggregate_context(ctx, Int32(stride)),
count: stride
)
if functionBuffer.contains(where: { $0 != 0 }) {
return functionBuffer.baseAddress!.assumingMemoryBound(
to: Unmanaged<Aggregate>.self
).pointee
} else {
let function = self.function.init()
let unmanagedFunction = Unmanaged.passRetained(function)
let functionPointer = unmanagedFunction.toOpaque()
withUnsafeBytes(of: functionPointer) {
functionBuffer.copyMemory(from: $0)
}
return unmanagedFunction
}
}
}
// MARK: - Properties
/// Flag indicating whether an error occurred during execution.
fileprivate var hasErrored = false
// MARK: - Inits
/// Initializes a new instance of the `Aggregate` class.
/// Initializes a new aggregate function instance.
///
/// This initializer is required for subclasses of ``Aggregate``.
/// In the current implementation, it performs no additional actions but provides
/// a basic structure for creating instances.
/// Subclasses may override this initializer to perform custom setup.
/// The base implementation performs no additional actions.
///
/// Subclasses may override this initializer to implement their own initialization
/// logic, including setting up additional properties or performing other necessary
/// operations.
///
/// ```swift
/// public class MyCustomAggregate: Function.Aggregate {
/// required public init() {
/// super.init() // Call to superclass initializer
/// // Additional initialization if needed
/// }
/// }
/// ```
///
/// - Note: Always call `super.init()` in the overridden initializer to ensure
/// proper initialization of the parent class.
/// - Important: Always call `super.init()` when overriding.
required public override init() {}
// MARK: - Methods
/// Installs the custom SQLite aggregate function into the specified database connection.
///
/// This method registers the custom aggregate function in the SQLite database,
/// allowing it to be used in SQL queries. The method creates a context for the function
/// and passes it to SQLite, as well as specifying callback functions to handle input
/// values and finalize results.
///
/// ```swift
/// // Assuming the database connection is already open
/// let db: OpaquePointer = ...
/// // Register the function in the database
/// try MyCustomAggregate.install(db: db)
/// ```
///
/// - Parameter connection: Pointer to the SQLite database connection where the function
/// will be installed.
///
/// - Throws: ``Connection/Error`` if the function installation fails. The error is thrown if
/// the `sqlite3_create_function_v2` call does not return `SQLITE_OK`.
///
/// - Note: This method must be called to register the custom function before using
/// it in SQL queries. Ensure that the database connection is open and available at
/// the time of this method call.
override class func install(db connection: OpaquePointer) throws(Connection.Error) {
override class func install(db connection: OpaquePointer) throws(SQLiteError) {
let context = Context(function: self)
let ctx = Unmanaged.passRetained(context).toOpaque()
let status = sqlite3_create_function_v2(
@@ -220,105 +97,91 @@ extension Function {
nil, xStep(_:_:_:), xFinal(_:), xDestroy(_:)
)
if status != SQLITE_OK {
throw Connection.Error(connection)
throw SQLiteError(connection)
}
}
/// Called for each input value during aggregate computation.
/// Processes one step of the aggregate computation.
///
/// This method should be overridden by subclasses to implement the specific logic
/// for processing each input value. Your implementation should handle the input
/// arguments and accumulate results for later finalization.
/// This method is called once for each row of input data. Subclasses must override it to
/// accumulate intermediate results.
///
/// ```swift
/// class MyCustomAggregate: Function.Aggregate {
/// // ...
/// - Parameter args: The set of arguments passed to the function.
/// - Throws: An error if the input arguments are invalid or the computation fails.
///
/// private var sum: Int = 0
///
/// override func step(args: Arguments) throws {
/// guard let value = args[0].intValue else {
/// throw MyCustomError.invalidInput
/// }
/// sum += value
/// }
///
/// // ...
/// }
/// ```
///
/// - Parameter args: An ``Arguments`` object that contains the number of
/// arguments and their values.
///
/// - Throws: An error if the function execution fails. Subclasses may throw errors
/// if the input values do not match the expected format or if other issues arise
/// during processing.
///
/// - Note: It is important to override this method in subclasses; otherwise, a
/// runtime error will occur due to calling `fatalError()`.
open func step(args: Arguments) throws {
fatalError("The 'step' method should be overridden.")
/// - Note: The default implementation triggers a runtime error.
open func step(args: any ArgumentsProtocol) throws {
fatalError("Subclasses must override `step(args:)`.")
}
/// Called when the aggregate computation is complete.
/// Finalizes the aggregate computation and returns the result.
///
/// This method should be overridden by subclasses to return the final result
/// of the aggregate computation. Your implementation should return a value that will
/// be used in SQL queries. If the aggregate should not return a value, you can
/// return `nil`.
/// SQLite calls this method once after all input rows have been processed.
/// Subclasses must override it to produce the final result of the aggregate.
///
/// ```swift
/// class MyCustomAggregate: Function.Aggregate {
/// // ...
///
/// private var sum: Int = 0
///
/// override func finalize() throws -> SQLiteRawRepresentable? {
/// return sum
/// }
/// }
/// ```
///
/// - Returns: An optional ``SQLiteRawRepresentable`` representing the result of the
/// aggregate function. The return value may be `nil` if a result is not required.
///
/// - Throws: An error if the function execution fails. Subclasses may throw errors
/// if the aggregate cannot be computed correctly or if other issues arise.
///
/// - Note: It is important to override this method in subclasses; otherwise, a
/// runtime error will occur due to calling `fatalError()`.
open func finalize() throws -> SQLiteRawRepresentable? {
fatalError("The 'finalize' method should be overridden.")
/// - Returns: The final computed value, or `nil` if the function produces no result.
/// - Throws: An error if the computation cannot be finalized.
/// - Note: The default implementation triggers a runtime error.
open func finalize() throws -> SQLiteRepresentable? {
fatalError("Subclasses must override `finalize()`.")
}
}
}
extension Function.Aggregate {
fileprivate final class Context {
// MARK: - Properties
private let function: Aggregate.Type
// MARK: - Inits
init(function: Aggregate.Type) {
self.function = function
}
// MARK: - Methods
func function(
for ctx: OpaquePointer?, isFinal: Bool = false
) -> Unmanaged<Aggregate>? {
typealias U = Unmanaged<Aggregate>
let bytes = isFinal ? 0 : MemoryLayout<U>.stride
let raw = sqlite3_aggregate_context(ctx, Int32(bytes))
guard let raw else { return nil }
let pointer = raw.assumingMemoryBound(to: U?.self)
if let pointer = pointer.pointee {
return pointer
} else {
let function = self.function.init()
pointer.pointee = Unmanaged.passRetained(function)
return pointer.pointee
}
}
}
}
// MARK: - Functions
/// C callback function to perform a step of the custom SQLite aggregate function.
///
/// This function is called by SQLite for each input value passed to the aggregate function.
/// It retrieves the function implementation from the context associated with the SQLite
/// request, calls the `step(args:)` method, and handles any errors that may occur during
/// execution.
///
/// - Parameters:
/// - ctx: Pointer to the SQLite context associated with the current query.
/// - argc: Number of arguments passed to the function.
/// - argv: Array of pointers to the argument values passed to the function.
private func xStep(
_ ctx: OpaquePointer?,
_ argc: Int32,
_ argv: UnsafeMutablePointer<OpaquePointer?>?
) {
let context = Unmanaged<Function.Aggregate.Context>
let function = Unmanaged<Function.Aggregate.Context>
.fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue()
let function = context
.function(ctx: ctx)
.function(for: ctx)?
.takeUnretainedValue()
guard let function else {
sqlite3_result_error_nomem(ctx)
return
}
assert(!function.hasErrored)
do {
@@ -330,31 +193,28 @@ private func xStep(
let message = "Error executing function '\(name)': \(description)"
function.hasErrored = true
sqlite3_result_error(ctx, message, -1)
sqlite3_result_error_code(ctx, SQLITE_ERROR)
}
}
/// C callback function to finalize the result of the custom SQLite aggregate function.
///
/// This function is called by SQLite when the aggregate computation is complete.
/// It retrieves the function implementation from the context, calls the `finalize()`
/// method, and sets the query result based on the returned value.
///
/// - Parameter ctx: Pointer to the SQLite context associated with the current query.
private func xFinal(_ ctx: OpaquePointer?) {
let context = Unmanaged<Function.Aggregate.Context>
let pointer = Unmanaged<Function.Aggregate.Context>
.fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue()
.function(for: ctx, isFinal: true)
let unmanagedFunction = context.function(ctx: ctx)
let function = unmanagedFunction.takeUnretainedValue()
defer { pointer?.release() }
defer { unmanagedFunction.release() }
guard let function = pointer?.takeUnretainedValue() else {
sqlite3_result_null(ctx)
return
}
guard !function.hasErrored else { return }
do {
let result = try function.finalize()
sqlite3_result_value(ctx, result?.sqliteRawValue)
sqlite3_result_value(ctx, result?.sqliteValue)
} catch {
let name = type(of: function).name
let description = error.localizedDescription
@@ -364,12 +224,6 @@ private func xFinal(_ ctx: OpaquePointer?) {
}
}
/// C callback function to destroy the context associated with the custom SQLite aggregate function.
///
/// This function is called by SQLite when the function is uninstalled. It frees the memory
/// allocated for the `Context` object associated with the function to avoid memory leaks.
///
/// - Parameter ctx: Pointer to the SQLite query context.
private func xDestroy(_ ctx: UnsafeMutableRawPointer?) {
guard let ctx else { return }
Unmanaged<AnyObject>.fromOpaque(ctx).release()

View File

@@ -4,29 +4,14 @@ import DataLiteC
extension Function {
/// A collection representing the arguments passed to an SQLite function.
///
/// This structure provides a collection interface to access the arguments passed to an SQLite
/// function. Each argument is represented by an instance of `SQLiteValue`, which can hold
/// various types of SQLite values such as integers, floats, text, blobs, or nulls.
///
/// - Important: This collection does not perform bounds checking when accessing arguments via
/// subscripts. It is the responsibility of the caller to ensure that the provided index is within the bounds
/// of the argument list.
///
/// - Important: The indices of this collection start from 0 and go up to, but not including, the
/// count of arguments.
public struct Arguments: Collection {
/// Alias for the type representing an element in `Arguments`, which is a `SQLiteValue`.
public typealias Element = SQLiteRawValue
/// Alias for the index type used in `Arguments`.
public typealias Index = Int
/// The `Arguments` structure provides a type-safe interface for accessing the arguments
/// received by a user-defined SQLite function. Each element of the collection is represented
/// by a ``SQLiteValue`` instance that can store integers, floating-point numbers, text, blobs,
/// or nulls.
public struct Arguments: ArgumentsProtocol {
// MARK: - Properties
/// The number of arguments passed to the SQLite function.
private let argc: Int32
/// A pointer to an array of `OpaquePointer?` representing SQLite values.
private let argv: UnsafeMutablePointer<OpaquePointer?>?
/// The number of arguments passed to the SQLite function.
@@ -34,28 +19,23 @@ extension Function {
Int(argc)
}
/// A Boolean value indicating whether there are no arguments passed to the SQLite function.
/// A Boolean value indicating whether there are no arguments.
public var isEmpty: Bool {
count == 0
}
/// The starting index of the arguments passed to the SQLite function.
/// The index of the first argument.
public var startIndex: Index {
0
}
/// The ending index of the arguments passed to the SQLite function.
/// The index immediately after the last valid argument.
public var endIndex: Index {
count
}
// MARK: - Inits
/// Initializes the argument list with the provided count and pointer to SQLite values.
///
/// - Parameters:
/// - argc: The number of arguments.
/// - argv: A pointer to an array of `OpaquePointer?` representing SQLite values.
init(argc: Int32, argv: UnsafeMutablePointer<OpaquePointer?>?) {
self.argc = argc
self.argv = argv
@@ -63,18 +43,17 @@ extension Function {
// MARK: - Subscripts
/// Accesses the SQLite value at the specified index.
/// Returns the SQLite value at the specified index.
///
/// - Parameter index: The index of the SQLite value to access.
/// Retrieves the raw value from the SQLite function arguments and returns it as an
/// instance of ``SQLiteValue``.
///
/// - Parameter index: The index of the argument to retrieve.
/// - Returns: The SQLite value at the specified index.
///
/// This subscript allows accessing the SQLite value at a specific index within the argument list.
/// If the index is out of bounds, a fatal error is triggered.
///
/// - Complexity: O(1)
public subscript(index: Index) -> Element {
guard count > index else {
fatalError("\(index) out of bounds")
guard index < count else {
fatalError("Index \(index) out of bounds")
}
let arg = argv.unsafelyUnwrapped[index]
switch sqlite3_value_type(arg) {
@@ -86,31 +65,12 @@ extension Function {
}
}
/// Accesses the SQLite value at the specified index and converts it to a type conforming to
/// `SQLiteConvertible`.
///
/// - Parameter index: The index of the SQLite value to access.
/// - Returns: The SQLite value at the specified index, converted to the specified type,
/// or `nil` if conversion fails.
///
/// This subscript allows accessing the SQLite value at a specific index within the argument
/// list and converting it to a type conforming to `SQLiteConvertible`.
///
/// - Complexity: O(1)
public subscript<T: SQLiteRawRepresentable>(index: Index) -> T? {
T(self[index])
}
// MARK: - Methods
/// Returns the index after the specified index.
///
/// - Parameter i: The index.
/// - Returns: The index immediately after the specified index.
///
/// This method is used to advance to the next index in the argument list when iterating over
/// its elements.
/// Returns the index that follows the specified index.
///
/// - Parameter i: The current index.
/// - Returns: The index immediately after the specified one.
/// - Complexity: O(1)
public func index(after i: Index) -> Index {
i + 1
@@ -120,39 +80,10 @@ extension Function {
// MARK: - Functions
/// Retrieves the textual data from an SQLite value.
///
/// - Parameter value: An opaque pointer to an SQLite value.
/// - Returns: A `String` representing the text value extracted from the SQLite value.
///
/// This function retrieves the textual data from an SQLite value and converts it into a Swift `String`.
///
/// - Note: The returned string may contain UTF-8 encoded text.
/// - Note: Ensure the provided `OpaquePointer` is valid and points to a valid SQLite value.
/// Passing a null pointer will result in undefined behavior.
///
/// - Important: This function does not perform error checking for null pointers or invalid SQLite values.
/// It is the responsibility of the caller to ensure the validity of the provided pointer.
///
/// - SeeAlso: [SQLite Documentation](https://www.sqlite.org/index.html)
private func sqlite3_value_text(_ value: OpaquePointer!) -> String {
String(cString: DataLiteC.sqlite3_value_text(value))
}
/// Retrieves binary data from an SQLite value.
///
/// - Parameter value: An opaque pointer to an SQLite value.
/// - Returns: A `Data` object representing the binary data extracted from the SQLite value.
///
/// This function retrieves binary data from an SQLite value and converts it into a Swift `Data` object.
///
/// - Note: Ensure the provided `OpaquePointer` is valid and points to a valid SQLite value.
/// Passing a null pointer will result in undefined behavior.
///
/// - Important: This function does not perform error checking for null pointers or invalid SQLite values.
/// It is the responsibility of the caller to ensure the validity of the provided pointer.
///
/// - SeeAlso: [SQLite Documentation](https://www.sqlite.org/index.html)
private func sqlite3_value_blob(_ value: OpaquePointer!) -> Data {
Data(
bytes: sqlite3_value_blob(value),

View File

@@ -2,57 +2,50 @@ import Foundation
import DataLiteC
extension Function {
/// An option set representing the options for an SQLite function.
/// An option set representing the configuration flags for an SQLite function.
///
/// This structure defines an option set to configure various options for an SQLite function.
/// Options can be combined using bitwise OR operations.
/// The `Options` structure defines a set of flags that control the behavior of a user-defined
/// SQLite function. Multiple options can be combined using bitwise OR operations.
///
/// Example usage:
/// ```swift
/// let options: Function.Options = [.deterministic, .directonly]
/// ```
///
/// - SeeAlso: [SQLite Function Flags](https://www.sqlite.org/c3ref/c_deterministic.html)
/// - SeeAlso: [Function Flags](https://sqlite.org/c3ref/c_deterministic.html)
public struct Options: OptionSet, Hashable, Sendable {
// MARK: - Properties
/// The raw value type used to store the SQLite function options.
/// The raw integer value representing the combined SQLite function options.
public var rawValue: Int32
// MARK: - Options
/// Indicates that the function is deterministic.
/// Marks the function as deterministic.
///
/// A deterministic function always gives the same output when it has the same input parameters.
/// For example, a mathematical function like sqrt() is deterministic.
/// A deterministic function always produces the same output for the same input parameters.
/// For example, mathematical functions like `sqrt()` or `abs()` are deterministic.
public static let deterministic = Self(rawValue: SQLITE_DETERMINISTIC)
/// Indicates that the function may only be invoked from top-level SQL.
/// Restricts the function to be invoked only from top-level SQL.
///
/// A function with the `directonly` option cannot be used in a VIEWs or TRIGGERs, or in schema structures
/// such as CHECK constraints, DEFAULT clauses, expression indexes, partial indexes, or generated columns.
/// A function with the `directonly` flag cannot be used in views, triggers, or schema
/// definitions such as `CHECK` constraints, `DEFAULT` clauses, expression indexes, partial
/// indexes, or generated columns.
///
/// The `directonly` option is recommended for any application-defined SQL function that has side-effects
/// or that could potentially leak sensitive information. This will prevent attacks in which an application
/// is tricked into using a database file that has had its schema surreptitiously modified to invoke the
/// application-defined function in ways that are harmful.
/// This option is recommended for functions that may have side effects or expose sensitive
/// information. It helps prevent attacks involving maliciously crafted database schemas
/// that attempt to invoke such functions implicitly.
public static let directonly = Self(rawValue: SQLITE_DIRECTONLY)
/// Indicates that the function is innocuous.
/// Marks the function as innocuous.
///
/// The `innocuous` option means that the function is unlikely to cause problems even if misused.
/// An innocuous function should have no side effects and should not depend on any values other
/// than its input parameters.
/// The `abs()` function is an example of an innocuous function.
/// The `load_extension()` SQL function is not innocuous because of its side effects.
/// The `innocuous` flag indicates that the function is safe even if misused. Such a
/// function should have no side effects and depend only on its input parameters. For
/// instance, `abs()` is innocuous, while `load_extension()` is not due to its side effects.
///
/// `innocuous` is similar to `deterministic`, but is not exactly the same.
/// The `random()` function is an example of a function that is innocuous but not deterministic.
/// This option is similar to ``deterministic`` but not identical. For example, `random()`
/// is innocuous but not deterministic.
public static let innocuous = Self(rawValue: SQLITE_INNOCUOUS)
// MARK: - Inits
/// Creates an SQLite function option set from a raw value.
/// Creates a new set of SQLite function options from the specified raw value.
///
/// - Parameter rawValue: The raw value representing the SQLite function options.
public init(rawValue: Int32) {

View File

@@ -13,7 +13,7 @@ extension Function {
/// )
/// try connection.add(function: Function.Regexp.self)
///
/// try connection.execute(sql: """
/// try connection.execute(raw: """
/// SELECT * FROM users WHERE name REGEXP 'John.*';
/// """)
/// ```
@@ -64,8 +64,8 @@ extension Function {
/// - Throws: ``Error/invalidArguments`` if the arguments are invalid or missing.
/// - Throws: ``Error/regexError(_:)`` if an error occurs during regex evaluation.
public override class func invoke(
args: Arguments
) throws -> SQLiteRawRepresentable? {
args: any ArgumentsProtocol
) throws -> SQLiteRepresentable? {
guard let regex = args[0] as String?,
let value = args[1] as String?
else { throw Error.invalidArguments }

View File

@@ -2,19 +2,21 @@ import Foundation
import DataLiteC
extension Function {
/// A base class for creating custom scalar SQLite functions.
/// A base class for defining custom scalar SQLite functions.
///
/// This class provides a base implementation for creating scalar functions in SQLite.
/// Scalar functions take one or more input arguments and return a single value. To
/// create a custom scalar function, subclass `Function.Scalar` and override the
/// ``name``, ``argc``, ``options``, and ``invoke(args:)`` methods.
/// The `Scalar` class provides a foundation for defining scalar functions in SQLite. Scalar
/// functions take one or more input arguments and return a single value for each function call.
///
/// To define a custom scalar function, subclass `Function.Scalar` and override the following
/// members:
///
/// - ``name`` The SQL name of the function.
/// - ``argc`` The number of arguments the function accepts.
/// - ``options`` Function options, such as `.deterministic` or `.innocuous`.
/// - ``invoke(args:)`` The method implementing the functions logic.
///
/// ### Example
///
/// To create a custom scalar function, subclass `Function.Scalar` and implement the
/// required methods. Here's an example of creating a custom `REGEXP` function that
/// checks if a string matches a regular expression.
///
/// ```swift
/// @available(macOS 13.0, *)
/// final class Regexp: Function.Scalar {
@@ -23,19 +25,15 @@ extension Function {
/// case regexError(Swift.Error)
/// }
///
/// // MARK: - Properties
///
/// override class var argc: Int32 { 2 }
/// override class var name: String { "REGEXP" }
/// override class var options: Function.Options {
/// [.deterministic, .innocuous]
/// }
///
/// // MARK: - Methods
///
/// override class func invoke(
/// args: Function.Arguments
/// ) throws -> SQLiteRawRepresentable? {
/// args: ArgumentsProtocol
/// ) throws -> SQLiteRepresentable? {
/// guard let regex = args[0] as String?,
/// let value = args[1] as String?
/// else { throw Error.argumentsWrong }
@@ -50,8 +48,7 @@ extension Function {
///
/// ### Usage
///
/// Once you've created your custom function, you need to install it into the SQLite database
/// connection. Here's how you can add the `Regexp` function to a ``Connection`` instance:
/// To use a custom function, register it with an SQLite connection:
///
/// ```swift
/// let connection = try Connection(
@@ -63,71 +60,15 @@ extension Function {
///
/// ### SQL Example
///
/// With the `Regexp` function installed, you can use it in your SQL queries. For
/// example, to find rows where the `name` column matches the regular expression
/// `John.*`, you would write:
/// After registration, the function becomes available in SQL expressions:
///
/// ```sql
/// -- Find rows where 'name' column matches the regular expression 'John.*'
/// SELECT * FROM users WHERE REGEXP('John.*', name);
/// ```
open class Scalar: Function {
// MARK: - Context
/// A helper class to store and manage context for a custom scalar SQLite function.
///
/// This class is used internally to hold a reference to the `Scalar` function
/// implementation. It is created and managed during the installation of the scalar
/// function into the SQLite database connection. The context is passed to SQLite
/// and used to call the appropriate function implementation when the function is
/// invoked.
fileprivate final class Context {
// MARK: Properties
/// The type of the `Scalar` function being managed.
///
/// This property holds a reference to the `Scalar` subclass that implements the
/// custom scalar function logic. It is used to invoke the function with the
/// provided arguments.
let function: Scalar.Type
// MARK: Inits
/// Initializes a new `Context` with a reference to the `Scalar` function type.
///
/// - Parameter function: The `Scalar` subclass that implements the custom scalar
/// function.
init(function: Scalar.Type) {
self.function = function
}
}
// MARK: - Methods
/// Installs a custom scalar SQLite function into the specified database connection.
///
/// This method registers the scalar function with the SQLite database. It creates
/// a `Context` object to hold a reference to the function implementation and sets up
/// the function using `sqlite3_create_function_v2`. The context is passed to SQLite,
/// allowing the implementation to be called later.
///
/// ```swift
/// // Assume the database connection is already open
/// let db: OpaquePointer = ...
/// // Registering the function in the database
/// try MyCustomScalar.install(db: db)
/// ```
///
/// - Parameter connection: A pointer to the SQLite database connection where the
/// function will be installed.
///
/// - Throws: ``Connection/Error`` if the function installation fails. This error occurs if
/// the call to `sqlite3_create_function_v2` does not return `SQLITE_OK`.
///
/// - Note: This method should be called to register the custom function before using
/// it in SQL queries. Ensure the database connection is open and available at the
/// time of this method call.
override class func install(db connection: OpaquePointer) throws(Connection.Error) {
override class func install(db connection: OpaquePointer) throws(SQLiteError) {
let context = Context(function: self)
let ctx = Unmanaged.passRetained(context).toOpaque()
let status = sqlite3_create_function_v2(
@@ -135,69 +76,58 @@ extension Function {
xFunc(_:_:_:), nil, nil, xDestroy(_:)
)
if status != SQLITE_OK {
throw Connection.Error(connection)
throw SQLiteError(connection)
}
}
/// Implementation of the custom scalar function.
/// Implements the logic of the custom scalar function.
///
/// This method must be overridden by subclasses to implement the specific logic
/// of the function. Your implementation should handle the input arguments and return
/// the result as ``SQLiteRawRepresentable``.
/// Subclasses must override this method to process the provided arguments and return a
/// result value for the scalar function call.
///
/// - Parameter args: An ``Arguments`` object containing the input arguments of the
/// function.
/// - Parameter args: The set of arguments passed to the function.
/// - Returns: The result of the function call, represented as ``SQLiteRepresentable``.
/// - Throws: An error if the arguments are invalid or the computation fails.
///
/// - Returns: The result of the function execution, represented as
/// ``SQLiteRawRepresentable``.
///
/// - Throws: An error if the function execution fails. Subclasses can throw
/// errors for invalid input values or other issues during processing.
///
/// - Note: It is important to override this method in subclasses; otherwise,
/// a runtime error will occur due to calling `fatalError()`.
open class func invoke(args: Arguments) throws -> SQLiteRawRepresentable? {
/// - Note: The default implementation triggers a runtime error.
open class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
fatalError("Subclasses must override this method to implement function logic.")
}
}
}
extension Function.Scalar {
fileprivate final class Context {
// MARK: - Properties
let function: Scalar.Type
// MARK: - Inits
init(function: Scalar.Type) {
self.function = function
}
}
}
// MARK: - Functions
/// The C function callback for executing a custom SQLite scalar function.
///
/// This function is called by SQLite when the scalar function is invoked. It retrieves the
/// function implementation from the context associated with the SQLite query, invokes the
/// function with the provided arguments, and sets the result of the query based on the
/// returned value. If an error occurs during the function invocation, it sets an error
/// message.
///
/// - Parameters:
/// - ctx: A pointer to the SQLite context associated with the current query. This context
/// contains information about the query execution and is used to set the result or error.
/// - argc: The number of arguments passed to the function. This is used to determine how
/// many arguments are available in the `argv` array.
/// - argv: An array of pointers to the values of the arguments passed to the function. Each
/// pointer corresponds to a value that the function will process.
///
/// - Note: The `xFunc` function should handle the function invocation logic, including
/// argument extraction and result setting. It should also handle errors by setting
/// appropriate error messages using `sqlite3_result_error`.
private func xFunc(
_ ctx: OpaquePointer?,
_ argc: Int32,
_ argv: UnsafeMutablePointer<OpaquePointer?>?
) {
let context = Unmanaged<Function.Scalar.Context>
let function = Unmanaged<Function.Scalar.Context>
.fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue()
.function
do {
let args = Function.Arguments(argc: argc, argv: argv)
let result = try context.function.invoke(args: args)
sqlite3_result_value(ctx, result?.sqliteRawValue)
let result = try function.invoke(args: args)
sqlite3_result_value(ctx, result?.sqliteValue)
} catch {
let name = context.function.name
let name = function.name
let description = error.localizedDescription
let message = "Error executing function '\(name)': \(description)"
sqlite3_result_error(ctx, message, -1)
@@ -205,17 +135,6 @@ private func xFunc(
}
}
/// The C function callback for destroying the context associated with a custom SQLite scalar function.
///
/// This function is called by SQLite when the function is uninstalled. It releases the memory
/// allocated for the `Context` object associated with the function to avoid memory leaks.
///
/// - Parameter ctx: A pointer to the context of the SQLite query. This context contains the
/// `Context` object that should be released.
///
/// - Note: The `xDestroy` function should only release the memory allocated for the `Context`
/// object. It should not perform any other operations or access the context beyond freeing
/// the memory.
private func xDestroy(_ ctx: UnsafeMutableRawPointer?) {
guard let ctx else { return }
Unmanaged<AnyObject>.fromOpaque(ctx).release()

View File

@@ -1,70 +1,66 @@
import Foundation
import DataLiteC
/// A base class representing a custom SQLite function.
/// A base class representing a user-defined SQLite function.
///
/// This class provides a framework for defining custom functions in SQLite. Subclasses must
/// override specific properties and methods to define the function's behavior, including
/// its name, argument count, and options.
/// The `Function` class defines the common interface and structure for implementing custom SQLite
/// functions. Subclasses are responsible for specifying the function name, argument count, and
/// behavior. This class should not be used directly instead, use one of its specialized
/// subclasses, such as ``Scalar`` or ``Aggregate``.
///
/// To create a custom SQLite function, you should subclass either ``Scalar`` or
/// ``Aggregate`` depending on whether your function is a scalar function (returns
/// a single result) or an aggregate function (returns a result accumulated from multiple
/// rows). The subclass will then override the necessary properties and methods to implement
/// the function's behavior.
/// To define a new SQLite function, subclass either ``Scalar`` or ``Aggregate`` depending on
/// whether the function computes a value from a single row or aggregates results across multiple
/// rows. Override the required properties and implement the necessary logic to define the
/// functions behavior.
///
/// ## Topics
///
/// ### Base Function Classes
///
/// - ``Aggregate``
/// - ``Scalar``
/// - ``Aggregate``
///
/// ### Custom Function Classes
///
/// - ``Regexp``
///
/// ### Configuration
///
/// - ``argc``
/// - ``name``
/// - ``options``
/// - ``Options``
open class Function {
// MARK: - Properties
/// The number of arguments that the custom SQLite function accepts.
/// The number of arguments that the function accepts.
///
/// This property must be overridden by subclasses to specify how many arguments
/// the function expects. The value should be a positive integer representing the
/// number of arguments, or zero if the function does not accept arguments.
/// Subclasses must override this property to specify the expected number of arguments. The
/// value should be a positive integer, or zero if the function does not accept any arguments.
open class var argc: Int32 {
fatalError("Subclasses must override this property to specify the number of arguments.")
}
/// The name of the custom SQLite function.
/// The name of the function.
///
/// This property must be overridden by subclasses to provide the name that the SQLite
/// engine will use to identify the function. The name should be a valid SQLite function
/// name according to SQLite naming conventions.
/// Subclasses must override this property to provide the name by which the SQLite engine
/// identifies the function. The name must comply with SQLite function naming rules.
open class var name: String {
fatalError("Subclasses must override this property to provide the function name.")
}
/// The options for the custom SQLite function.
/// The configuration options for the function.
///
/// This property must be overridden by subclasses to specify options such as whether the
/// function is deterministic or not. Options are represented as a bitmask of `Function.Options`.
/// Subclasses must override this property to specify the functions behavioral flags, such as
/// whether it is deterministic, direct-only, or innocuous.
open class var options: Options {
fatalError("Subclasses must override this property to specify function options.")
}
/// The encoding used by the function, which defaults to UTF-8.
///
/// This is used to set the encoding for text data in the custom SQLite function. The default
/// encoding is UTF-8, but this can be modified if necessary. This encoding is combined with
/// the function's options to configure the function.
class var encoding: Function.Options {
Function.Options(rawValue: SQLITE_UTF8)
}
/// The combined options for the custom SQLite function.
///
/// This property combines the function's options with the encoding. The result is used when
/// registering the function with SQLite. This property is derived from `options` and `encoding`.
class var opts: Int32 {
var options = options
options.insert(encoding)
@@ -73,91 +69,35 @@ open class Function {
// MARK: - Methods
/// Installs the custom SQLite function into the specified database connection.
///
/// Subclasses must override this method to provide the implementation for installing
/// the function into the SQLite database. This typically involves registering the function
/// with SQLite using `sqlite3_create_function_v2` or similar APIs.
///
/// - Parameter connection: A pointer to the SQLite database connection where the function
/// will be installed.
/// - Throws: An error if the function installation fails. The method will throw an exception
/// if the installation cannot be completed successfully.
class func install(db connection: OpaquePointer) throws(Connection.Error) {
class func install(db connection: OpaquePointer) throws(SQLiteError) {
fatalError("Subclasses must override this method to implement function installation.")
}
/// Uninstalls the custom SQLite function from the specified database connection.
///
/// This method unregisters the function from the SQLite database using `sqlite3_create_function_v2`
/// with `NULL` for the function implementations. This effectively removes the function from the
/// database.
///
/// - Parameter connection: A pointer to the SQLite database connection from which the function
/// will be uninstalled.
/// - Throws: An error if the function uninstallation fails. An exception is thrown if the function
/// cannot be removed successfully.
class func uninstall(db connection: OpaquePointer) throws(Connection.Error) {
class func uninstall(db connection: OpaquePointer) throws(SQLiteError) {
let status = sqlite3_create_function_v2(
connection,
name, argc, opts,
nil, nil, nil, nil, nil
)
if status != SQLITE_OK {
throw Connection.Error(connection)
throw SQLiteError(connection)
}
}
}
// MARK: - Functions
/// Sets the result of an SQLite query as a text string.
///
/// This function sets the result of the query to the specified text string. SQLite will store
/// this string inside the database as the result of the custom function.
///
/// - Parameters:
/// - ctx: A pointer to the SQLite context that provides information about the current query.
/// - string: A `String` that will be returned as the result of the query.
///
/// - Note: The `SQLITE_TRANSIENT` flag is used, meaning that SQLite makes a copy of the passed
/// data. This ensures that the string remains valid after the function execution is completed.
func sqlite3_result_text(_ ctx: OpaquePointer!, _ string: String) {
sqlite3_result_text(ctx, string, -1, SQLITE_TRANSIENT)
}
/// Sets the result of an SQLite query as binary data (BLOB).
///
/// This function sets the result of the query to the specified binary data. This is useful for
/// returning non-textual data such as images or other binary content from a custom function.
///
/// - Parameters:
/// - ctx: A pointer to the SQLite context that provides information about the current query.
/// - data: A `Data` object representing the binary data to be returned as the result.
///
/// - Note: The `SQLITE_TRANSIENT` flag is used, ensuring that SQLite makes a copy of the binary
/// data. This prevents issues related to memory management if the original data is modified
/// or deallocated after the function completes.
func sqlite3_result_blob(_ ctx: OpaquePointer!, _ data: Data) {
data.withUnsafeBytes {
sqlite3_result_blob(ctx, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT)
}
}
/// Sets the result of an SQLite query based on the `SQLiteRawValue` type.
///
/// This function sets the result of the query according to the type of the provided value. It can
/// handle integers, floating-point numbers, strings, binary data, or `NULL` values.
///
/// - Parameters:
/// - ctx: A pointer to the SQLite context that provides information about the current query.
/// - value: A `SQLiteRawValue` that represents the result to be returned. If the value is `nil`,
/// the result will be set to `NULL`.
///
/// - Note: The function uses a `switch` statement to determine the type of the value and then
/// calls the appropriate SQLite function to set the result. This ensures that the correct SQLite
/// result type is used based on the provided value.
func sqlite3_result_value(_ ctx: OpaquePointer!, _ value: SQLiteRawValue?) {
func sqlite3_result_value(_ ctx: OpaquePointer!, _ value: SQLiteValue?) {
switch value ?? .null {
case .int(let value): sqlite3_result_int64(ctx, value)
case .real(let value): sqlite3_result_double(ctx, value)

View File

@@ -1,413 +0,0 @@
import Foundation
import OrderedCollections
extension Statement {
/// A structure representing a set of arguments used in database statements.
///
/// `Arguments` provides a convenient way to manage and pass parameters to database queries.
/// It supports both indexed and named tokens, allowing flexibility in specifying parameters.
///
/// ## Argument Tokens
///
/// A "token" in this context refers to a placeholder in the SQL statement for a value that is provided at runtime.
/// There are two types of tokens:
///
/// - Indexed Tokens: Represented by numerical indices (`?NNNN`, `?`).
/// These placeholders correspond to specific parameter positions.
/// - Named Tokens: Represented by string names (`:AAAA`, `@AAAA`, `$AAAA`).
/// These placeholders are identified by unique names.
///
/// More information on SQLite parameters can be found [here](https://www.sqlite.org/lang_expr.html#varparam).
/// The `Arguments` structure supports indexed (?) and named (:AAAA) forms of tokens.
///
/// ## Creating Arguments
///
/// You can initialize `Arguments` using arrays or dictionaries:
///
/// - **Indexed Arguments**: Initialize with an array of values or use an array literal.
/// ```swift
/// let args: Statement.Arguments = ["John", 30]
/// ```
/// - **Named Arguments**: Initialize with a dictionary of named values or use a dictionary literal.
/// ```swift
/// let args: Statement.Arguments = ["name": "John", "age": 30]
/// ```
///
/// ## Combining Arguments
///
/// You can combine two sets of `Arguments` using the ``merge(with:using:)-23pzs``or
/// ``merged(with:using:)-23p3q``methods. These methods allow you to define how to resolve
/// conflicts when the same parameter token exists in both argument sets.
///
/// ```swift
/// var base: Statement.Arguments = ["name": "Alice"]
/// let update: Statement.Arguments = ["name": "Bob", "age": 30]
///
/// base.merge(with: update) { token, current, new in
/// return .replace
/// }
/// ```
///
/// Alternatively, you can create a new merged instance without modifying the original:
///
/// ```swift
/// let merged = base.merged(with: update) { token, current, new in
/// return .ignore
/// }
/// ```
///
/// Conflict resolution is controlled by the closure you provide, which receives the token, the current value,
/// and the new value. It returns a value of type ``ConflictResolution``, specifying how to handle the
/// conflict.. This ensures that merging is performed explicitly and predictably, avoiding accidental overwrites.
///
/// - Important: Although mixing parameter styles is technically allowed, it is generally not recommended.
/// For clarity and maintainability, you should consistently use either indexed or named parameters
/// throughout a query. Mixing styles may lead to confusion or hard-to-diagnose bugs in more complex queries.
///
/// ## Topics
///
/// ### Subtypes
///
/// - ``Token``
/// - ``ConflictResolution``
///
/// ### Type Aliases
///
/// - ``Resolver``
/// - ``Elements``
/// - ``RawValue``
/// - ``Index``
/// - ``Element``
///
/// ### Initializers
///
/// - ``init()``
/// - ``init(_:)-1v7s``
/// - ``init(_:)-bfj9``
/// - ``init(arrayLiteral:)``
/// - ``init(dictionaryLiteral:)``
///
/// ### Instance Properties
///
/// - ``tokens``
/// - ``count``
/// - ``isEmpty``
/// - ``startIndex``
/// - ``endIndex``
/// - ``description``
///
/// ### Instance Methods
///
/// - ``index(after:)``
/// - ``contains(_:)``
/// - ``merged(with:using:)-23p3q``
/// - ``merged(with:using:)-89krm``
/// - ``merge(with:using:)-23pzs``
/// - ``merge(with:using:)-4r21o``
///
/// ### Subscripts
///
/// - ``subscript(_:)``
public struct Arguments: Collection, ExpressibleByArrayLiteral, ExpressibleByDictionaryLiteral, CustomStringConvertible {
/// Represents a token used in database statements, either indexed or named.
///
/// Tokens are used to identify placeholders for values in SQL statements.
/// They can either be indexed, represented by an integer index, or named, represented by a string name.
public enum Token: Hashable {
/// Represents an indexed token with a numerical index.
case indexed(index: Int)
/// Represents a named token with a string name.
case named(name: String)
}
/// A strategy for resolving conflicts when merging two sets of arguments.
///
/// When two `Arguments` instances contain the same token, a `ConflictResolution` value
/// determines how the conflict should be handled.
public enum ConflictResolution {
/// Keeps the current value and ignores the new one.
case ignore
/// Replaces the current value with the new one.
case replace
}
/// A closure used to resolve conflicts when merging two sets of arguments.
///
/// This closure is invoked when both argument sets contain the same token.
/// It determines whether to keep the existing value or replace it with the new one.
///
/// - Parameters:
/// - token: The conflicting parameter token.
/// - current: The value currently associated with the token.
/// - new: The new value from the other argument set.
/// - Returns: A strategy indicating how to resolve the conflict.
public typealias Resolver = (
_ token: Token,
_ current: SQLiteRawValue,
_ new: SQLiteRawValue
) -> ConflictResolution
/// The underlying storage for `Arguments`, mapping tokens to their raw values while preserving order.
///
/// Keys are tokens (either indexed or named), and values are the corresponding SQLite-compatible values.
public typealias Elements = OrderedDictionary<Token, SQLiteRawValue>
/// The value type used in the underlying elements dictionary.
///
/// This represents a SQLite-compatible raw value, such as a string, number, or null.
public typealias RawValue = Elements.Value
/// The index type used to traverse the arguments collection.
public typealias Index = Elements.Index
/// A keyvalue pair representing an argument token and its associated value.
public typealias Element = (token: Token, value: RawValue)
// MARK: - Private Properties
private var elements: Elements
// MARK: - Public Properties
/// The starting index of the arguments collection, which is always zero.
///
/// This property represents the initial position in the arguments collection.
/// Since the elements are indexed starting from zero, it consistently returns zero,
/// allowing predictable forward iteration.
///
/// - Complexity: `O(1)`
public var startIndex: Index {
0
}
/// The ending index of the arguments collection, equal to the number of elements.
///
/// This property marks the position one past the last element in the collection.
/// It returns the total number of arguments and defines the upper bound for iteration
/// over tokens and their associated values.
///
/// - Complexity: `O(1)`
public var endIndex: Index {
elements.count
}
/// A Boolean value indicating whether the arguments collection is empty.
///
/// Returns `true` if the collection contains no arguments; otherwise, returns `false`.
///
/// - Complexity: `O(1)`
public var isEmpty: Bool {
elements.isEmpty
}
/// The number of arguments in the collection.
///
/// This property reflects the total number of tokenvalue pairs
/// currently stored in the arguments set.
///
/// - Complexity: `O(1)`
public var count: Int {
elements.count
}
/// A textual representation of the arguments collection.
///
/// The description includes all tokens and their associated values
/// in the order they appear in the collection. This is useful for debugging.
///
/// - Complexity: `O(n)`
public var description: String {
elements.description
}
/// An array of all tokens present in the arguments collection.
///
/// The tokens are returned in insertion order and include both
/// indexed and named forms, depending on how the arguments were constructed.
///
/// - Complexity: `O(1)`
public var tokens: [Token] {
elements.keys.elements
}
// MARK: - Inits
/// Initializes an empty `Arguments`.
///
/// - Complexity: `O(1)`
public init() {
self.elements = [:]
}
/// Initializes `Arguments` with an array of values.
///
/// - Parameter elements: An array of `SQLiteRawBindable` values.
///
/// - Complexity: `O(n)`, where `n` is the number of elements in the input array.
public init(_ elements: [SQLiteRawBindable?]) {
self.elements = .init(
uniqueKeysWithValues: elements.enumerated().map { offset, value in
(.indexed(index: offset + 1), value?.sqliteRawValue ?? .null)
}
)
}
/// Initializes `Arguments` with a dictionary of named values.
///
/// - Parameter elements: A dictionary mapping names to `SQLiteRawBindable` values.
///
/// - Complexity: `O(n)`, where `n` is the number of elements in the input dictionary.
public init(_ elements: [String: SQLiteRawBindable?]) {
self.elements = .init(
uniqueKeysWithValues: elements.map { name, value in
(.named(name: name), value?.sqliteRawValue ?? .null)
}
)
}
/// Initializes `Arguments` from an array literal.
///
/// This initializer enables array literal syntax for positional (indexed) arguments.
///
/// ```swift
/// let args: Statement.Arguments = ["Alice", 42]
/// ```
///
/// Each value is bound to a token of the form `?1`, `?2`, etc., based on its position.
///
/// - Complexity: `O(n)`, where `n` is the number of elements.
public init(arrayLiteral elements: SQLiteRawBindable?...) {
self.elements = .init(
uniqueKeysWithValues: elements.enumerated().map { offset, value in
(.indexed(index: offset + 1), value?.sqliteRawValue ?? .null)
}
)
}
/// Initializes `Arguments` from a dictionary literal.
///
/// This initializer enables dictionary literal syntax for named arguments.
///
/// ```swift
/// let args: Statement.Arguments = ["name": "Alice", "age": 42]
/// ```
///
/// Each key becomes a named token (`:name`, `:age`, etc.).
///
/// - Complexity: `O(n)`, where `n` is the number of elements.
public init(dictionaryLiteral elements: (String, SQLiteRawBindable?)...) {
self.elements = .init(
uniqueKeysWithValues: elements.map { name, value in
(.named(name: name), value?.sqliteRawValue ?? .null)
}
)
}
// MARK: - Subscripts
/// Accesses the element at the specified position.
///
/// This subscript returns the `(token, value)` pair located at the given index
/// in the arguments collection. The order of elements reflects their insertion order.
///
/// - Parameter index: The position of the element to access.
/// - Returns: A tuple containing the token and its associated value.
///
/// - Complexity: `O(1)`
public subscript(index: Index) -> Element {
let element = elements.elements[index]
return (element.key, element.value)
}
// MARK: - Methods
/// Returns the position immediately after the given index.
///
/// Use this method to advance an index when iterating over the arguments collection.
///
/// - Parameter i: A valid index of the collection.
/// - Returns: The index value immediately following `i`.
///
/// - Complexity: `O(1)`
public func index(after i: Index) -> Index {
i + 1
}
/// Returns a Boolean value indicating whether the specified token exists in the arguments.
///
/// Use this method to check whether a tokeneither indexed or namedis present in the collection.
///
/// - Parameter token: The token to search for in the arguments.
/// - Returns: `true` if the token exists in the collection; otherwise, `false`.
///
/// - Complexity: On average, the complexity is `O(1)`.
public func contains(_ token: Token) -> Bool {
elements.keys.contains(token)
}
/// Merges the contents of another `Arguments` instance into this one using a custom resolver.
///
/// For each token present in `other`, the method either inserts the new value
/// or resolves conflicts when the token already exists in the current collection.
///
/// - Parameters:
/// - other: Another `Arguments` instance whose contents will be merged into this one.
/// - resolve: A closure that determines how to resolve conflicts between existing and new values.
/// - Complexity: `O(n)`, where `n` is the number of elements in `other`.
public mutating func merge(with other: Self, using resolve: Resolver) {
for (token, newValue) in other.elements {
if let index = elements.index(forKey: token) {
let currentValue = elements.values[index]
switch resolve(token, currentValue, newValue) {
case .ignore: continue
case .replace: elements[token] = newValue
}
} else {
elements[token] = newValue
}
}
}
/// Merges the contents of another `Arguments` instance into this one using a fixed conflict resolution strategy.
///
/// This variant applies the same resolution strategy to all conflicts without requiring a custom closure.
///
/// - Parameters:
/// - other: Another `Arguments` instance whose contents will be merged into this one.
/// - resolution: A fixed strategy to apply when a token conflict occurs.
/// - Complexity: `O(n)`, where `n` is the number of elements in `other`.
public mutating func merge(with other: Self, using resolution: ConflictResolution) {
merge(with: other) { _, _, _ in resolution }
}
/// Returns a new `Arguments` instance by merging the contents of another one using a custom resolver.
///
/// This method creates a copy of the current arguments and merges `other` into it.
/// For each conflicting token, the provided resolver determines whether to keep the existing value
/// or replace it with the new one.
///
/// - Parameters:
/// - other: Another `Arguments` instance whose contents will be merged into the copy.
/// - resolve: A closure that determines how to resolve conflicts between existing and new values.
/// - Returns: A new `Arguments` instance containing the merged values.
/// - Complexity: `O(n)`, where `n` is the number of elements in `other`.
public func merged(with other: Self, using resolve: Resolver) -> Self {
var copy = self
copy.merge(with: other, using: resolve)
return copy
}
/// Returns a new `Arguments` instance by merging the contents of another one using a fixed strategy.
///
/// This variant uses the same resolution strategy for all conflicts without requiring a custom closure.
///
/// - Parameters:
/// - other: Another `Arguments` instance whose contents will be merged into the copy.
/// - resolution: A fixed strategy to apply when a token conflict occurs.
/// - Returns: A new `Arguments` instance containing the merged values.
/// - Complexity: `O(n)`, where `n` is the number of elements in `other`.
public func merged(with other: Self, using resolution: ConflictResolution) -> Self {
merged(with: other) { _, _, _ in resolution }
}
}
}

View File

@@ -2,34 +2,19 @@ import Foundation
import DataLiteC
extension Statement {
/// Provides a set of options for preparing SQLite statements.
/// A set of options that control how an SQLite statement is prepared.
///
/// This struct conforms to the `OptionSet` protocol, allowing multiple options to be combined using
/// bitwise operations. Each option corresponds to a specific SQLite preparation flag.
/// `Options` conforms to the `OptionSet` protocol, allowing multiple flags to be combined.
/// Each option corresponds to a specific SQLite preparation flag.
///
/// ## Example
///
/// ```swift
/// let options: Statement.Options = [.persistent, .noVtab]
///
/// if options.contains(.persistent) {
/// print("Persistent option is set")
/// }
///
/// if options.contains(.noVtab) {
/// print("noVtab option is set")
/// }
/// ```
///
/// The example demonstrates how to create an `Options` instance with `persistent` and `noVtab`
/// options set, and then check each option using the `contains` method.
/// - SeeAlso: [Prepare Flags](https://sqlite.org/c3ref/c_prepare_normalize.html)
///
/// ## Topics
///
/// ### Initializers
///
/// - ``init(rawValue:)-(Int32)``
/// - ``init(rawValue:)-(UInt32)``
/// - ``init(rawValue:)-(Int32)``
///
/// ### Instance Properties
///
@@ -42,83 +27,44 @@ extension Statement {
public struct Options: OptionSet, Sendable {
// MARK: - Properties
/// The underlying raw value representing the set of options as a bitmask.
/// The raw bitmask value that represents the combined options.
///
/// Each bit in the raw value corresponds to a specific option in the `Statement.Options` set. You can
/// use this value to perform low-level bitmask operations or to directly initialize an `Options`
/// instance.
///
/// ## Example
///
/// ```swift
/// let options = Statement.Options(
/// rawValue: SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB
/// )
/// print(options.rawValue) // Output: bitmask representing the combined options
/// ```
///
/// The example shows how to access the raw bitmask value from an `Options` instance.
/// Each bit in the mask corresponds to a specific SQLite preparation flag. ou can use this
/// value for low-level bitwise operations or to construct an `Options` instance directly.
public var rawValue: UInt32
/// Specifies that the prepared statement should be persistent and reusable.
/// Indicates that the prepared statement is persistent and reusable.
///
/// The `persistent` flag hints to SQLite that the prepared statement will be retained and reused
/// multiple times. Without this flag, SQLite assumes the statement will be used only once or a few
/// times and then destroyed.
/// This flag hints to SQLite that the prepared statement will be kept and reused multiple
/// times. Without this hint, SQLite assumes the statement will be used only a few times and
/// then destroyed.
///
/// The current implementation uses this hint to avoid depleting the limited store of lookaside
/// memory, potentially improving performance for frequently executed statements. Future versions
/// of SQLite may handle this flag differently.
public static let persistent = Self(rawValue: UInt32(SQLITE_PREPARE_PERSISTENT))
/// Using `.persistent` can help avoid excessive lookaside memory usage and improve
/// performance for frequently executed statements.
public static let persistent = Self(rawValue: SQLITE_PREPARE_PERSISTENT)
/// Specifies that virtual tables should not be used in the prepared statement.
/// Disables the use of virtual tables in the prepared statement.
///
/// The `noVtab` flag instructs SQLite to prevent the use of virtual tables when preparing the SQL
/// statement. This can be useful in cases where the use of virtual tables is undesirable or
/// restricted by the application logic. If this flag is set, any attempt to access a virtual table
/// during the execution of the prepared statement will result in an error.
///
/// This option ensures that the prepared statement will only work with standard database tables.
public static let noVtab = Self(rawValue: UInt32(SQLITE_PREPARE_NO_VTAB))
/// When this flag is set, any attempt to reference a virtual table during statement
/// preparation results in an error. Use this option when virtual tables are restricted or
/// undesirable for security or policy reasons.
public static let noVtab = Self(rawValue: SQLITE_PREPARE_NO_VTAB)
// MARK: - Inits
/// Initializes an `Options` instance with the given `UInt32` raw value.
/// Creates a new set of options from a raw `UInt32` bitmask value.
///
/// Use this initializer to create a set of options using the raw bitmask value, where each bit
/// corresponds to a specific option.
///
/// ## Example
///
/// ```swift
/// let options = Statement.Options(
/// rawValue: UInt32(SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB)
/// )
/// print(options.contains(.persistent)) // Output: true
/// print(options.contains(.noVtab)) // Output: true
/// ```
///
/// - Parameter rawValue: The `UInt32` raw bitmask value representing the set of options.
/// - Parameter rawValue: The bitmask value that represents the combined options.
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
/// Initializes an `Options` instance with the given `Int32` raw value.
/// Creates a new set of options from a raw `Int32` bitmask value.
///
/// This initializer allows the use of `Int32` values directly, converting them to the `UInt32` type
/// required for bitmask operations.
/// This initializer allows working directly with SQLite C constants that use
/// 32-bit integers.
///
/// ## Example
///
/// ```swift
/// let options = Statement.Options(
/// rawValue: SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB
/// )
/// print(options.contains(.persistent)) // Output: true
/// print(options.contains(.noVtab)) // Output: true
/// ```
///
/// - Parameter rawValue: The `Int32` raw bitmask value representing the set of options.
/// - Parameter rawValue: The bitmask value that represents the combined options.
public init(rawValue: Int32) {
self.rawValue = UInt32(rawValue)
}

View File

@@ -1,687 +1,139 @@
import Foundation
import DataLiteC
/// A value representing a static destructor for SQLite.
/// A prepared SQLite statement used to execute SQL commands.
///
/// `SQLITE_STATIC` is used to indicate that the SQLite library should not free the associated
/// memory when the statement is finalized.
let SQLITE_STATIC = unsafeBitCast(
OpaquePointer(bitPattern: 0),
to: sqlite3_destructor_type.self
)
/// A value representing a transient destructor for SQLite.
/// `Statement` encapsulates the lifecycle of a compiled SQL statement, including parameter binding,
/// execution, and result retrieval. The statement is finalized automatically when the instance is
/// deallocated.
///
/// `SQLITE_TRANSIENT` is used to indicate that the SQLite library should make a copy of the
/// associated memory and free the original memory when the statement is finalized.
let SQLITE_TRANSIENT = unsafeBitCast(
OpaquePointer(bitPattern: -1),
to: sqlite3_destructor_type.self
)
/// A class representing a prepared SQL statement in SQLite.
///
/// ## Overview
///
/// This class provides functionality for preparing, binding parameters, and executing SQL
/// statements using SQLite. It also supports retrieving results and resource management, ensuring
/// the statement is finalized when no longer needed.
///
/// ## Preparing an SQL Statement
///
/// To create a prepared SQL statement, use the ``Connection/prepare(sql:options:)`` method of the
/// ``Connection`` object.
///
/// ```swift
/// do {
/// let statement = try connection.prepare(
/// sql: "SELECT id, name FROM users WHERE age > ?",
/// options: [.persistent, .normalize]
/// )
/// } catch {
/// print("Error: \(error)")
/// }
/// ```
///
/// ## Binding Parameters
///
/// SQL queries can contain parameters whose values can be bound after the statement is prepared.
/// This prevents SQL injection and makes the code more secure.
///
/// ### Binding Parameters by Index
///
/// When preparing an SQL query, you can use the question mark (`?`) as a placeholder for parameter
/// values. Parameter indexing starts from one (1). It is important to keep this in mind for
/// correctly binding values to parameters in the SQL query. The method ``bind(_:at:)-(T?,_)``
/// is used to bind values to parameters.
///
/// ```swift
/// do {
/// let query = "INSERT INTO users (name, age) VALUES (?, ?)"
/// let statement = try connection.prepare(sql: query)
/// try statement.bind("John Doe", at: 1)
/// try statement.bind(30, at: 2)
/// } catch {
/// print("Error binding parameters: \(error)")
/// }
/// ```
///
/// ### Binding Parameters by Explicit Index
///
/// Parameters can be explicitly bound by indices `?1`, `?2`, and so on. This improves readability
/// and simplifies working with queries containing many parameters. Explicit indices do not need to
/// start from one, be sequential, or contiguous.
///
/// ```swift
/// do {
/// let query = "INSERT INTO users (name, age) VALUES (?1, ?2)"
/// let statement = try connection.prepare(sql: query)
/// try statement.bind("Jane Doe", at: 1)
/// try statement.bind(25, at: 2)
/// } catch {
/// print("Error binding parameters: \(error)")
/// }
/// ```
///
/// ### Binding Parameters by Name
///
/// Parameters can also be bound by names. This increases code readability and simplifies managing
/// complex queries. Use ``bind(parameterIndexBy:)`` to retrieve the index of a named parameter.
///
/// ```swift
/// do {
/// let query = "INSERT INTO users (name, age) VALUES (:userName, :userAge)"
/// let statement = try connection.prepare(sql: query)
///
/// let indexName = statement.bind(parameterIndexBy: ":userName")
/// let indexAge = statement.bind(parameterIndexBy: ":userAge")
///
/// try statement.bind("Jane Doe", at: indexName)
/// try statement.bind(25, at: indexAge)
/// } catch {
/// print("Error binding parameters: \(error)")
/// }
/// ```
///
/// ### Duplicating Parameters
///
/// Parameters with explicit indices or names can be duplicated. This allows the same value to be
/// bound to multiple places in the query.
///
/// ```swift
/// do {
/// let query = """
/// INSERT INTO users (name, age)
/// VALUES
/// (:userName, :userAge),
/// (:userName, :userAge)
/// """
/// let statement = try connection.prepare(sql: query)
///
/// let indexName = statement.bind(parameterIndexBy: ":userName")
/// let indexAge = statement.bind(parameterIndexBy: ":userAge")
///
/// try statement.bind("Jane Doe", at: indexName)
/// try statement.bind(25, at: indexAge)
/// } catch {
/// print("Error binding parameters: \(error)")
/// }
/// ```
///
/// ### Mixing Indexed and Named Parameters
///
/// You can mix positional (`?`, `?NNN`) and named (`:name`, `@name`, `$name`) parameters
/// in a single SQL statement. This is supported by SQLite and allows you to use different parameter
/// styles simultaneously.
///
/// ```swift
/// do {
/// let query = """
/// SELECT * FROM users WHERE age = ? AND name = :name
/// """
/// let statement = try connection.prepare(sql: query)
/// let nameIndex = statement.bind(parameterIndexBy: ":name")
///
/// try statement.bind(88, at: 1)
/// try statement.bind("Alice", at: nameIndex)
/// } catch {
/// print("Error binding parameters: \(error)")
/// }
/// ```
///
/// - Important: Although mixing parameter styles is technically allowed, it is generally not recommended.
/// For clarity and maintainability, you should consistently use either indexed or named parameters
/// throughout a query. Mixing styles may lead to confusion or hard-to-diagnose bugs in more complex queries.
///
/// ## Generating SQL Using SQLiteRow
///
/// The ``SQLiteRow`` type can be used not only for retrieving query results, but also for dynamically
/// generating SQL statements. Its ordered keys and parameter-friendly formatting make it especially
/// convenient for constructing `INSERT`, `UPDATE`, and similar queries with named parameters.
///
/// ### Inserting a Row
///
/// To insert a new row into a table using values from a ``SQLiteRow``, you can use the
/// ``SQLiteRow/columns`` and ``SQLiteRow/namedParameters`` properties.
/// This ensures the correct number and order of columns and parameters.
///
/// ```swift
/// var row = SQLiteRow()
/// row["name"] = .text("Alice")
/// row["age"] = .int(30)
/// row["email"] = .text("alice@example.com")
///
/// let columns = row.columns.joined(separator: ", ") // name, age, email
/// let values = row.namedParameters.joined(separator: ", ") // :name, :age, :email
///
/// let sql = "INSERT INTO users (\(columns)) VALUES (\(values))"
/// let statement = try connection.prepare(sql: sql)
/// try statement.bind(row)
/// ```
///
/// This approach eliminates the need to manually write parameter placeholders or maintain their order.
/// It also ensures full compatibility with the ``bind(_:)-(SQLiteRow)`` method.
///
/// ### Updating a Row
///
/// To construct an `UPDATE` statement using a ``SQLiteRow``, you can dynamically
/// map the column names to SQL assignments in the form `column = :column`.
///
/// ```swift
/// var row = SQLiteRow()
/// row["id"] = .int(123)
/// row["name"] = .text("Alice")
/// row["age"] = .int(30)
/// row["email"] = .text("alice@example.com")
///
/// let assignments = zip(row.columns, row.namedParameters)
/// .map { "\($0.0) = \($0.1)" }
/// .joined(separator: ", ")
///
/// let sql = "UPDATE users SET \(assignments) WHERE id = :id"
/// let statement = try connection.prepare(sql: sql)
/// try statement.bind(row)
/// try statement.step()
/// ```
///
/// - Important: Ensure the SQLiteRow includes any values used in conditions
/// (e.g., `:id` in `WHERE`), or binding will fail.
///
/// ## Executing an SQL Statement
///
/// The SQL statement is executed using the ``step()`` method. It returns `true` if there is a
/// result to process, and `false` when execution is complete. To retrieve the results of an SQL
/// statement, use ``columnCount()``, ``columnType(at:)``, ``columnName(at:)``,
/// ``columnValue(at:)->SQLiteRawValue``, and ``currentRow()``.
///
/// ```swift
/// do {
/// let query = "SELECT id, name FROM users WHERE age > ?"
/// let statement = try connection.prepare(sql: query)
/// try statement.bind(18, at: 1)
/// while try statement.step() {
/// for index in 0..<statement.columnCount() {
/// let columnName = statement.columnName(at: index)
/// let columnValue = statement.columnValue(at: index)
/// print("\(columnName): \(columnValue)")
/// }
/// }
/// } catch {
/// print("Error: \(error)")
/// }
/// ```
///
/// ## Preparing for Reuse
///
/// Before reusing a prepared SQL statement, you should call the ``clearBindings()`` method to
/// remove the values bound to the parameters and then call the ``reset()`` method to restore it to
/// its original state.
///
/// ```swift
/// do {
/// let query = "INSERT INTO users (name, age) VALUES (?, ?)"
/// let statement = try connection.prepare(sql: query)
///
/// try statement.bind("John Doe", at: 1)
/// try statement.bind(30, at: 2)
/// try statement.step()
///
/// try statement.clearBindings()
/// try statement.reset()
///
/// try statement.bind("Jane Doe", at: 1)
/// try statement.bind(25, at: 2)
/// try statement.step()
/// } catch {
/// print("Error: \(error)")
/// }
/// ```
/// This class serves as a thin, type-safe wrapper over the SQLite C API, providing a Swift
/// interface for managing prepared statements.
///
/// ## Topics
///
/// ### Subtypes
/// ### Statement Options
///
/// - ``Options``
/// - ``Arguments``
///
/// ### Binding Parameters
///
/// - ``bindParameterCount()``
/// - ``bind(parameterIndexBy:)``
/// - ``bind(parameterNameBy:)``
/// - ``bind(_:at:)-(SQLiteRawValue,_)``
/// - ``bind(_:at:)-(T?,_)``
/// - ``bind(_:)-2ymd1``
/// - ``bind(_:)-6887r``
/// - ``clearBindings()``
///
/// ### Getting Results
///
/// - ``columnCount()``
/// - ``columnType(at:)``
/// - ``columnName(at:)``
/// - ``columnValue(at:)->SQLiteRawValue``
/// - ``columnValue(at:)->T?``
/// - ``currentRow()``
///
/// ### Evaluating
///
/// - ``step()``
/// - ``reset()``
/// - ``execute(rows:)``
/// - ``execute(args:)``
///
/// ### Hashing
///
/// - ``hash(into:)``
public final class Statement: Equatable, Hashable {
public final class Statement {
// MARK: - Private Properties
/// The SQLite statement pointer associated with this `Statement` instance.
private let statement: OpaquePointer
/// The SQLite database connection pointer used to create this statement.
private let connection: OpaquePointer
// MARK: - Inits
/// Initializes a new `Statement` instance with a given SQL query and options.
///
/// This initializer prepares the SQL statement for execution and sets up any necessary
/// options. It throws an ``Connection/Error`` if the SQL preparation fails.
///
/// - Parameters:
/// - connection: A pointer to the SQLite database connection to use.
/// - query: The SQL query string to prepare.
/// - options: The options to use when preparing the SQL statement.
/// - Throws: ``Connection/Error`` if the SQL statement preparation fails.
init(db connection: OpaquePointer, sql query: String, options: Options) throws(Connection.Error) {
init(
db connection: OpaquePointer,
sql query: String,
options: Options
) throws(SQLiteError) {
var statement: OpaquePointer! = nil
let status = sqlite3_prepare_v3(connection, query, -1, options.rawValue, &statement, nil)
let status = sqlite3_prepare_v3(
connection, query, -1,
options.rawValue, &statement, nil
)
if status == SQLITE_OK, let statement {
self.statement = statement
self.connection = connection
} else {
sqlite3_finalize(statement)
throw Connection.Error(connection)
throw SQLiteError(connection)
}
}
/// Finalizes the SQL statement, releasing any associated resources.
deinit {
sqlite3_finalize(statement)
}
// MARK: - Binding Parameters
/// Returns the count of parameters that can be bound to this statement.
///
/// This method provides the number of parameters that can be bound in the SQL statement,
/// allowing you to determine how many parameters need to be set.
///
/// - Returns: The number of bindable parameters in the statement.
public func bindParameterCount() -> Int32 {
}
// MARK: - StatementProtocol
extension Statement: StatementProtocol {
public func parameterCount() -> Int32 {
sqlite3_bind_parameter_count(statement)
}
/// Returns the index of a parameter by its name.
///
/// This method is used to find the index of a parameter in the SQL statement given its name.
/// This is useful for binding values to named parameters.
///
/// - Parameters:
/// - name: The name of the parameter.
/// - Returns: The index of the parameter, or 0 if the parameter does not exist.
public func bind(parameterIndexBy name: String) -> Int32 {
public func parameterIndexBy(_ name: String) -> Int32 {
sqlite3_bind_parameter_index(statement, name)
}
/// Returns the name of a parameter by its index.
///
/// This method retrieves the name of a parameter based on its index in the SQL statement. This
/// is useful for debugging or when parameter names are needed.
///
/// - Parameters:
/// - index: The index of the parameter (1-based).
/// - Returns: The name of the parameter, or `nil` if the name could not be retrieved.
public func bind(parameterNameBy index: Int32) -> String? {
guard let cString = sqlite3_bind_parameter_name(statement, index) else {
return nil
}
return String(cString: cString)
public func parameterNameBy(_ index: Int32) -> String? {
sqlite3_bind_parameter_name(statement, index)
}
/// Binds a value to a parameter at a specified index.
///
/// This method allows you to bind various types of values (integer, real, text, or blob) to a
/// parameter in the SQL statement. The appropriate SQLite function is called based on the type
/// of value being bound.
///
/// - Parameters:
/// - value: The value to bind to the parameter.
/// - index: The index of the parameter to bind (1-based).
/// - Throws: ``Connection/Error`` if the binding operation fails.
public func bind(_ value: SQLiteRawValue, at index: Int32) throws {
let status: Int32
switch value {
case .int(let value): status = sqlite3_bind_int64(statement, index, value)
case .real(let value): status = sqlite3_bind_double(statement, index, value)
case .text(let value): status = sqlite3_bind_text(statement, index, value)
case .blob(let value): status = sqlite3_bind_blob(statement, index, value)
case .null: status = sqlite3_bind_null(statement, index)
public func bind(_ value: SQLiteValue, at index: Int32) throws(SQLiteError) {
let status = switch value {
case .int(let value): sqlite3_bind_int64(statement, index, value)
case .real(let value): sqlite3_bind_double(statement, index, value)
case .text(let value): sqlite3_bind_text(statement, index, value)
case .blob(let value): sqlite3_bind_blob(statement, index, value)
case .null: sqlite3_bind_null(statement, index)
}
if status != SQLITE_OK {
throw Connection.Error(connection)
throw SQLiteError(connection)
}
}
/// Binds a value conforming to `RawBindable` to a parameter at a specified index.
///
/// This method provides a generic way to bind values that conform to `RawBindable`,
/// allowing for flexibility in the types of values that can be bound to SQL statements.
///
/// - Parameters:
/// - value: The value to bind to the parameter.
/// - index: The index of the parameter to bind (1-based).
/// - Throws: ``Connection/Error`` if the binding operation fails.
public func bind<T: SQLiteRawBindable>(_ value: T?, at index: Int32) throws {
try bind(value?.sqliteRawValue ?? .null, at: index)
}
/// Binds all values from a `SQLiteRow` to their corresponding named parameters in the statement.
///
/// This method iterates through each key-value pair in the given `SQLiteRow` and binds the value to
/// the statements named parameter using the `:<column>` syntax. Column names from the row must
/// match named parameters defined in the SQL statement.
///
/// For example, a column named `"userID"` will be bound to a parameter `:userID` in the SQL.
///
/// - Throws: ``Connection/Error`` if a parameter is missing or if a binding operation fails.
public func bind(_ row: SQLiteRow) throws {
try row.forEach { column, value in
try bind(value, at: bind(parameterIndexBy: ":\(column)"))
}
}
/// Binds all values from an `Arguments` instance to their corresponding parameters in the statement.
///
/// This method iterates through each tokenvalue pair in the provided `Arguments` collection and binds
/// the value to the appropriate parameter in the SQL statement. Both indexed (`?NNN`) and named (`:name`)
/// parameters are supported.
///
/// - Parameter arguments: The `Arguments` instance containing tokens and their associated values.
/// - Throws: ``Connection/Error`` if a parameter is not found or if the binding fails.
public func bind(_ arguments: Arguments) throws {
try arguments.forEach { token, value in
let index = switch token {
case .indexed(let index):
Int32(index)
case .named(let name):
bind(parameterIndexBy: ":\(name)")
}
try bind(value, at: index)
}
}
/// Clears all parameter bindings from the statement.
///
/// This method resets any parameter bindings, allowing you to reuse the same SQL statement
/// with different parameter values. This is useful for executing the same statement multiple
/// times with different parameters.
///
/// - Throws: ``Connection/Error`` if the operation to clear bindings fails.
public func clearBindings() throws {
public func clearBindings() throws(SQLiteError) {
if sqlite3_clear_bindings(statement) != SQLITE_OK {
throw Connection.Error(connection)
throw SQLiteError(connection)
}
}
// MARK: - Retrieving Results
@discardableResult
public func step() throws(SQLiteError) -> Bool {
switch sqlite3_step(statement) {
case SQLITE_ROW: true
case SQLITE_DONE: false
default: throw SQLiteError(connection)
}
}
public func reset() throws(SQLiteError) {
if sqlite3_reset(statement) != SQLITE_OK {
throw SQLiteError(connection)
}
}
/// Returns the number of columns in the result set.
///
/// This method provides the count of columns returned by the SQL statement result, which is
/// useful for iterating over query results and processing data.
///
/// - Returns: The number of columns in the result set.
public func columnCount() -> Int32 {
sqlite3_column_count(statement)
}
/// Returns the type of data stored in a column at a specified index.
///
/// This method retrieves the type of data stored in a particular column of the result set,
/// allowing you to handle different data types appropriately.
///
/// - Parameters:
/// - index: The index of the column (0-based).
/// - Returns: The type of data in the column as `SQLiteRawType`.
public func columnType(at index: Int32) -> SQLiteRawType {
.init(rawValue: sqlite3_column_type(statement, index)) ?? .null
public func columnName(at index: Int32) -> String? {
sqlite3_column_name(statement, index)
}
/// Returns the name of a column at a specified index.
///
/// This method retrieves the name of a column, which is useful for debugging or when you need
/// to work with column names directly.
///
/// - Parameters:
/// - index: The index of the column (0-based).
/// - Returns: The name of the column as a `String`.
public func columnName(at index: Int32) -> String {
String(cString: sqlite3_column_name(statement, index))
}
/// Retrieves the value from a column at a specified index.
///
/// This method extracts the value from a column and returns it as an `SQLiteRawValue`, which
/// can represent different data types like integer, real, text, or blob.
///
/// - Parameters:
/// - index: The index of the column (0-based).
/// - Returns: The value from the column as `SQLiteRawValue`.
public func columnValue(at index: Int32) -> SQLiteRawValue {
switch columnType(at: index) {
case .int: return .int(sqlite3_column_int64(statement, index))
case .real: return .real(sqlite3_column_double(statement, index))
case .text: return .text(sqlite3_column_text(statement, index))
case .blob: return .blob(sqlite3_column_blob(statement, index))
case .null: return .null
public func columnValue(at index: Int32) -> SQLiteValue {
switch sqlite3_column_type(statement, index) {
case SQLITE_INTEGER: .int(sqlite3_column_int64(statement, index))
case SQLITE_FLOAT: .real(sqlite3_column_double(statement, index))
case SQLITE_TEXT: .text(sqlite3_column_text(statement, index))
case SQLITE_BLOB: .blob(sqlite3_column_blob(statement, index))
default: .null
}
}
/// Retrieves the value from a column at a specified index and converts it to a value
/// conforming to `SQLiteRawRepresentable`.
///
/// This method provides a way to convert column values into types that conform to
/// ``SQLiteRawRepresentable``, allowing for easier integration with custom data models.
///
/// - Parameters:
/// - index: The index of the column (0-based).
/// - Returns: The value from the column converted to `T`, or `nil` if conversion fails.
public func columnValue<T: SQLiteRawRepresentable>(at index: Int32) -> T? {
T(columnValue(at: index))
}
/// Retrieves the current row of the result set as a `SQLiteRow` instance.
///
/// This method iterates over the columns of the current row in the result set.
/// For each column, it retrieves the column name and the corresponding value using the
/// ``columnName(at:)`` and ``columnValue(at:)->SQLiteRawValue`` methods.
/// It then populates a ``SQLiteRow`` instance with these column-value pairs.
///
/// - Returns: A `SQLiteRow` instance representing the current row of the result set.
public func currentRow() -> SQLiteRow {
var row = SQLiteRow()
for index in 0..<columnCount() {
let name = columnName(at: index)
let value = columnValue(at: index)
row[name] = value
}
return row
}
// MARK: - Evaluating
/// Advances to the next row in the result set.
///
/// This method steps through the result set row by row, returning `true` if there is a row
/// available and `false` if the end of the result set is reached.
///
/// - Returns: `true` if there is a row available, `false` if the end of the result set is
/// reached.
/// - Throws: ``Connection/Error`` if an error occurs during execution.
@discardableResult
public func step() throws(Connection.Error) -> Bool {
switch sqlite3_step(statement) {
case SQLITE_ROW: return true
case SQLITE_DONE: return false
default: throw Connection.Error(connection)
}
}
/// Resets the prepared SQL statement to its initial state.
///
/// Use this method before re-executing the statement. It does not clear the bound parameters,
/// allowing their values to persist between executions. To clear the parameters, use the
/// `clearBindings()` method.
///
/// - Throws: ``Connection/Error`` if the statement reset fails.
public func reset() throws {
if sqlite3_reset(statement) != SQLITE_OK {
throw Connection.Error(connection)
}
}
/// Executes the statement once for each row, returning the collected result rows if any.
///
/// This method binds each rows named values to the statement parameters and executes the
/// statement. After each execution, any resulting rows are collected and returned. If the `rows`
/// array is empty, the statement will still execute once with no parameters bound.
///
/// Use this method for queries such as `INSERT` or `UPDATE` statements with changing
/// parameter values.
///
/// - Note: If `rows` is empty, the statement executes once with no bound values.
///
/// - Parameter rows: A list of `SQLiteRow` values to bind to the statement.
/// - Returns: An array of result rows collected from all executions of the statement.
/// - Throws: ``Connection/Error`` if binding or execution fails.
@discardableResult
public func execute(rows: [SQLiteRow]) throws -> [SQLiteRow] {
var result = [SQLiteRow]()
var index = 0
repeat {
if rows.count > index {
try bind(rows[index])
}
while try step() {
result.append(currentRow())
}
try clearBindings()
try reset()
index += 1
} while index < rows.count
return result
}
/// Executes the statement once for each arguments set, returning any resulting rows.
///
/// This method binds each `Arguments` set (indexed or named) to the statement and executes it. All
/// result rows from each execution are collected and returned. If no arguments are provided, the
/// statement executes once with no values bound.
///
/// Use this method for queries such as `SELECT`, `INSERT`, `UPDATE`, or `DELETE` where results
/// may be expected and multiple executions are needed.
///
/// ```swift
/// let stmt = try connection.prepare(
/// sql: "SELECT * FROM logs WHERE level = :level"
/// )
/// let result = try stmt.execute(args: [
/// ["level": "info"],
/// ["level": "error"]
/// ])
/// ```
///
/// - Note: If `args` is `nil` or empty, the statement executes once with no bound values.
///
/// - Parameter args: A list of `Arguments` to bind and execute. Defaults to `nil`.
/// - Returns: A flat array of result rows produced by all executions.
/// - Throws: ``Connection/Error`` if binding or execution fails.
@discardableResult
public func execute(args: [Arguments]? = nil) throws -> [SQLiteRow] {
var result = [SQLiteRow]()
var index = 0
repeat {
if let args, args.count > index {
try bind(args[index])
}
while try step() {
result.append(currentRow())
}
try clearBindings()
try reset()
index += 1
} while index < args?.count ?? 0
return result
}
// MARK: - Equatable
/// Compares two `Statement` instances for equality.
///
/// This method checks whether two `Statement` instances are equal by comparing their
/// underlying SQLite statement pointers and connection pointers.
///
/// - Parameters:
/// - lhs: The first `Statement` instance.
/// - rhs: The second `Statement` instance.
/// - Returns: `true` if the two instances are equal, `false` otherwise.
public static func == (lhs: Statement, rhs: Statement) -> Bool {
lhs.statement == rhs.statement && lhs.connection == rhs.connection
}
// MARK: - Hashable
/// Computes a hash value for the `Statement` instance.
///
/// This method computes a hash value based on the SQLite statement pointer and connection
/// pointer. It is used to support hash-based collections like sets and dictionaries.
///
/// - Parameter hasher: The hasher to use for computing the hash value.
public func hash(into hasher: inout Hasher) {
hasher.combine(statement)
hasher.combine(connection)
}
}
// MARK: - Functions
// MARK: - Constants
let SQLITE_STATIC = unsafeBitCast(
OpaquePointer(bitPattern: 0),
to: sqlite3_destructor_type.self
)
let SQLITE_TRANSIENT = unsafeBitCast(
OpaquePointer(bitPattern: -1),
to: sqlite3_destructor_type.self
)
// MARK: - Private Sunctions
private func sqlite3_bind_parameter_name(_ stmt: OpaquePointer!, _ index: Int32) -> String? {
guard let cString = DataLiteC.sqlite3_bind_parameter_name(stmt, index) else { return nil }
return String(cString: cString)
}
private func sqlite3_bind_text(_ stmt: OpaquePointer!, _ index: Int32, _ string: String) -> Int32 {
sqlite3_bind_text(stmt, index, string, -1, SQLITE_TRANSIENT)
@@ -693,6 +145,13 @@ private func sqlite3_bind_blob(_ stmt: OpaquePointer!, _ index: Int32, _ data: D
}
}
private func sqlite3_column_name(_ stmt: OpaquePointer!, _ iCol: Int32) -> String? {
guard let cString = DataLiteC.sqlite3_column_name(stmt, iCol) else {
return nil
}
return String(cString: cString)
}
private func sqlite3_column_text(_ stmt: OpaquePointer!, _ iCol: Int32) -> String {
String(cString: DataLiteC.sqlite3_column_text(stmt, iCol))
}