Refactor entire codebase and rewrite documentation
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. Here’s 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. Here’s 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. Here’s 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. Here’s 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. Here’s 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. Here’s 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 application’s 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, it’s 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. Here’s 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. Here’s 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 SQLite’s 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 function’s 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()
|
||||
|
||||
@@ -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
|
||||
/// function’s 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 function’s 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)
|
||||
|
||||
@@ -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 key–value 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 token–value 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 token—either indexed or named—is 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 statement’s 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 token–value 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 row’s 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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user