DataLiteCore swift package

This commit is contained in:
2025-04-24 23:48:46 +03:00
parent b0e52a72b7
commit 6f955b2c43
70 changed files with 7939 additions and 1 deletions

View File

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

View File

@@ -0,0 +1,43 @@
import Foundation
extension Connection {
/// An encryption key for accessing 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.
public enum Key {
/// A passphrase used to derive an encryption key.
///
/// Intended for human-readable strings such as passwords or PIN codes.
/// The string is passed directly without escaping or quoting.
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.
case rawKey(Data)
/// The string value to be 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'...'`.
public var keyValue: String {
switch self {
case .passphrase(let string):
return string
case .rawKey(let data):
return 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.
public var length: Int32 {
Int32(keyValue.utf8.count)
}
}
}

View File

@@ -0,0 +1,141 @@
import Foundation
extension Connection {
/// The `Location` enum represents different locations for a SQLite database.
///
/// 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
/// ```
public enum Location {
/// A database located 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.
///
/// - 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).
case file(path: String)
/// An 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.
///
/// 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).
case inMemory
/// A temporary database on disk.
///
/// 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.
///
/// 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).
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 ""
}
}
}
}

View File

@@ -0,0 +1,438 @@
import Foundation
import DataLiteC
extension Connection {
/// Options for controlling the connection to a SQLite database.
///
/// 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).
///
/// ### 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``
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.
public var rawValue: Int32
// MARK: - Instances
/// Option: open the database for read-only access.
///
/// This option configures the SQLite database connection to be opened in read-only mode. When this
/// option is specified, the database can be accessed for querying, but any attempts to modify the
/// data (such as inserting, updating, or deleting records) will result in an error. If the specified
/// database file does not already exist, an error will also be returned.
///
/// This is particularly useful when you want to ensure that your application does not accidentally
/// modify the database, or when you need to work with a database that is being shared among multiple
/// processes or applications that require read-only access.
///
/// ### Usage
///
/// You can specify the `readonly` option when opening a database connection, as shown in the example:
///
/// ```swift
/// let options: Connection.Options = [.readonly]
/// let connection = try Connection(path: dbFilePath, options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: If you attempt to write to a read-only database, an error will be thrown.
///
/// - Note: Ensure that the database file exists before opening it in read-only mode, as the connection
/// will fail if the file does not exist.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let readonly = Self(rawValue: SQLITE_OPEN_READONLY)
/// Option: open the database for reading and writing.
///
/// This option configures the SQLite database connection to be opened in read-write mode. When this
/// option is specified, the database can be accessed for both querying and modifying data. This means
/// you can perform operations such as inserting, updating, or deleting records in addition to reading.
///
/// If the database file does not exist, an error will be returned. If the file is write-protected by
/// the operating system, the connection will be opened in read-only mode instead, as a fallback.
///
/// ### Usage
///
/// You can specify the `readwrite` option when opening a database connection, as shown below:
///
/// ```swift
/// let options: Connection.Options = [.readwrite]
/// let connection = try Connection(path: dbFilePath, options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: If the database file does not exist, an error will be thrown.
/// - Note: If you are unable to open the database in read-write mode due to permissions, it will
/// attempt to open it in read-only mode.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let readwrite = Self(rawValue: SQLITE_OPEN_READWRITE)
/// Option: create the database if it does not exist.
///
/// This option instructs SQLite to create a new database file if it does not already exist. If the
/// specified database file already exists, the connection will open that existing database instead.
///
/// ### Usage
///
/// You can specify the `create` option when opening a database connection, as shown below:
///
/// ```swift
/// let options: Connection.Options = [.create]
/// let connection = try Connection(path: dbFilePath, options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: If the database file exists, it will be opened normally, and no new file will be created.
/// - Note: If the database file does not exist, a new file will be created at the specified path.
///
/// This option is often used in conjunction with other options, such as `readwrite`, to ensure that a
/// new database can be created and written to right away.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let create = Self(rawValue: SQLITE_OPEN_CREATE)
/// Option: specify a URI for opening the database.
///
/// This option allows the filename provided to be interpreted as a Uniform Resource Identifier (URI).
/// When this flag is set, SQLite will parse the filename as a URI, enabling the use of URI features
/// such as special encoding and various URI schemes.
///
/// ### Usage
///
/// You can specify the `uri` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.uri]
/// let connection = try Connection(path: "file:///path/to/database.db", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: Using this option allows you to take advantage of SQLite's URI capabilities, such as
/// specifying various parameters in the URI (e.g., caching, locking, etc.).
/// - Note: If this option is not set, the filename will be treated as a simple path without URI
/// interpretation.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let uri = Self(rawValue: SQLITE_OPEN_URI)
/// Option: open the database in memory.
///
/// This option opens the database as an in-memory database, meaning that all data is stored in RAM
/// rather than on disk. This can be useful for temporary databases or for testing purposes where
/// persistence is not required.
///
/// When using this option, the "filename" argument is ignored, but it is still used for cache-sharing
/// if shared cache mode is enabled.
///
/// ### Usage
///
/// You can specify the `memory` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.memory]
/// let connection = try Connection(path: ":memory:", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: Since the database is stored in memory, all data will be lost when the connection is
/// closed or the program exits. Therefore, this option is best suited for scenarios where data
/// persistence is not necessary.
/// - Note: In-memory databases can be significantly faster than disk-based databases due to the
/// absence of disk I/O operations.
///
/// For more details, refer to the SQLite documentation on
/// [opening a new database connection](https://www.sqlite.org/c3ref/open.html).
public static let memory = Self(rawValue: SQLITE_OPEN_MEMORY)
/// Option: do not use mutexes.
///
/// This option configures the new database connection to use the "multi-thread"
/// [threading mode](https://www.sqlite.org/threadsafe.html). In this mode, separate threads can
/// concurrently access SQLite, provided that each thread is utilizing a different
/// [database connection](https://www.sqlite.org/c3ref/sqlite3.html).
///
/// ### Usage
///
/// You can specify the `nomutex` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.nomutex]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: When using this option, ensure that each thread has its own database connection, as
/// concurrent access to the same connection is not safe.
/// - Note: This option can improve performance in multi-threaded applications by reducing the
/// overhead of mutex locking, but it may lead to undefined behavior if not used carefully.
/// - Note: If your application requires safe concurrent access to a single database connection
/// from multiple threads, consider using the ``fullmutex`` option instead.
///
/// For more details, refer to the SQLite documentation on
/// [thread safety](https://www.sqlite.org/threadsafe.html).
public static let nomutex = Self(rawValue: SQLITE_OPEN_NOMUTEX)
/// Option: use full mutexing.
///
/// This option configures the new database connection to utilize the "serialized"
/// [threading mode](https://www.sqlite.org/threadsafe.html). In this mode, multiple threads can safely
/// attempt to access the same database connection simultaneously. Although mutexes will block any
/// actual concurrency, this mode allows for multiple threads to operate without causing data corruption
/// or undefined behavior.
///
/// ### Usage
///
/// You can specify the `fullmutex` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.fullmutex]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: Using the `fullmutex` option is recommended when you need to ensure thread safety when
/// multiple threads access the same database connection.
/// - Note: This option may introduce some performance overhead due to the locking mechanisms in place.
/// If your application is designed for high concurrency and can manage separate connections per thread,
/// consider using the ``nomutex`` option for better performance.
/// - Note: It's essential to be aware of potential deadlocks if multiple threads are competing for the
/// same resources. Proper design can help mitigate these risks.
///
/// For more details, refer to the SQLite documentation on
/// [thread safety](https://www.sqlite.org/threadsafe.html).
public static let fullmutex = Self(rawValue: SQLITE_OPEN_FULLMUTEX)
/// Option: use a shared cache.
///
/// This option enables the database to be opened in [shared cache](https://www.sqlite.org/sharedcache.html)
/// mode. In this mode, multiple database connections can share cached data, potentially improving
/// performance when accessing the same database from different connections.
///
/// ### Usage
///
/// You can specify the `sharedcache` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.sharedcache]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: **Discouraged Usage**: The use of shared cache mode is
/// [discouraged](https://www.sqlite.org/sharedcache.html#dontuse). It may lead to unpredictable behavior,
/// especially in applications with complex threading models or multiple database connections.
///
/// - Note: **Build Variability**: Shared cache capabilities may be omitted from many builds of SQLite.
/// If your SQLite build does not support shared cache, this option will be a no-op, meaning it will
/// have no effect on the behavior of your database connection.
///
/// - Note: **Performance Considerations**: While shared cache can improve performance by reducing memory
/// usage, it may introduce complexity in managing concurrent access. Consider your application's design
/// and the potential for contention among connections when using this option.
///
/// For more information, consult the SQLite documentation on
/// [shared cache mode](https://www.sqlite.org/sharedcache.html).
public static let sharedcache = Self(rawValue: SQLITE_OPEN_SHAREDCACHE)
/// Option: use a private cache.
///
/// This option disables the use of [shared cache](https://www.sqlite.org/sharedcache.html) mode.
/// When a database is opened with this option, it uses a private cache for its connections, meaning
/// that the cached data will not be shared with other database connections.
///
/// ### Usage
///
/// You can specify the `privatecache` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.privatecache]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Important Notes
///
/// - Note: **Isolation**: Using a private cache ensures that the database connection operates in
/// isolation, preventing any caching interference from other connections. This can be beneficial
/// in multi-threaded applications where shared cache might lead to unpredictable behavior.
///
/// - Note: **Performance Impact**: While a private cache avoids the complexities associated with
/// shared caching, it may increase memory usage since each connection maintains its own cache.
/// Consider your applications performance requirements when choosing between shared and private
/// cache options.
///
/// - Note: **Build Compatibility**: Ensure that your SQLite build supports the private cache option.
/// While most builds do, its always a good idea to verify if you encounter any issues.
///
/// For more information, refer to the SQLite documentation on
/// [shared cache mode](https://www.sqlite.org/sharedcache.html).
public static let privatecache = Self(rawValue: SQLITE_OPEN_PRIVATECACHE)
/// Option: use extended result code mode.
///
/// This option enables "extended result code mode" for the database connection. When this mode is
/// enabled, SQLite provides additional error codes that can help in diagnosing issues that may
/// arise during database operations.
///
/// ### Usage
///
/// You can specify the `exrescode` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.exrescode]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Benefits
///
/// - **Improved Error Handling**: By using extended result codes, you can get more granular
/// information about errors, which can be particularly useful for debugging and error handling
/// in your application.
///
/// - **Detailed Diagnostics**: Extended result codes may provide context about the failure,
/// allowing for more targeted troubleshooting and resolution of issues.
///
/// ### Considerations
///
/// - **Compatibility**: Make sure your version of SQLite supports extended result codes. This
/// option should be available in most modern builds of SQLite.
///
/// For more information, refer to the SQLite documentation on
/// [extended result codes](https://www.sqlite.org/rescode.html).
public static let exrescode = Self(rawValue: SQLITE_OPEN_EXRESCODE)
/// Option: do not follow symbolic links when opening a file.
///
/// When this option is enabled, the database filename must not contain a symbolic link. If the
/// filename refers to a symbolic link, an error will be returned when attempting to open the
/// database.
///
/// ### Usage
///
/// You can specify the `nofollow` option when opening a database connection. Heres an example:
///
/// ```swift
/// let options: Connection.Options = [.nofollow]
/// let connection = try Connection(path: "myDatabase.sqlite", options: options)
/// ```
///
/// ### Benefits
///
/// - **Increased Security**: By disallowing symbolic links, you reduce the risk of unintended
/// file access or manipulation through links that may point to unexpected locations.
///
/// - **File Integrity**: Ensures that the database connection directly references the intended
/// file without any indirection that symbolic links could introduce.
///
/// ### Considerations
///
/// - **Filesystem Limitations**: This option may limit your ability to use symbolic links in
/// your application. Make sure this behavior is acceptable for your use case.
///
/// For more information, refer to the SQLite documentation on [file opening](https://www.sqlite.org/c3ref/open.html).
public static let nofollow = Self(rawValue: SQLITE_OPEN_NOFOLLOW)
// MARK: - Inits
/// Initializes a set of options for connecting to a SQLite database.
///
/// 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:
///
/// ```swift
/// let options = 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
}
}
}

View File

@@ -0,0 +1,199 @@
import Foundation
import DataLiteC
public final class Connection: ConnectionProtocol {
// MARK: - Private Properties
private let connection: OpaquePointer
// MARK: - Delegation
public weak var delegate: (any ConnectionDelegate)?
// MARK: - Connection State
public var isAutocommit: Bool {
sqlite3_get_autocommit(connection) != 0
}
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) }
}
// MARK: - Inits
public init(location: Location, options: Options) throws {
if case let Location.file(path) = location, !path.isEmpty {
try FileManager.default.createDirectory(
at: URL(fileURLWithPath: path).deletingLastPathComponent(),
withIntermediateDirectories: true
)
}
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)
sqlite3_close_v2(connection)
throw error
}
}
public convenience init(path: String, options: Options) throws {
try self.init(location: .file(path: path), options: options)
}
deinit {
sqlite3_close_v2(connection)
}
// 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)
}
}
// MARK: - Encryption Keys
public func apply(_ key: Key, name: String? = nil) throws(Error) {
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)
}
}
public func rekey(_ key: Key, name: String? = nil) throws(Error) {
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)
}
}
}
// MARK: - Functions
private func traceCallback(
_ flag: UInt32,
_ ctx: UnsafeMutableRawPointer?,
_ p: UnsafeMutableRawPointer?,
_ x: UnsafeMutableRawPointer?
) -> Int32 {
guard let ctx = ctx else { return SQLITE_OK }
let connection = Unmanaged<Connection>
.fromOpaque(ctx)
.takeUnretainedValue()
if let delegate = connection.delegate {
guard let stmt = OpaquePointer(p),
let pSql = sqlite3_expanded_sql(stmt),
let xSql = x?.assumingMemoryBound(to: CChar.self)
else { return SQLITE_OK }
let pSqlString = String(cString: pSql)
let xSqlString = String(cString: xSql)
let trace = (xSqlString, pSqlString)
delegate.connection(connection, trace: trace)
}
return SQLITE_OK
}
private func updateHookCallback(
_ ctx: UnsafeMutableRawPointer?,
_ action: Int32,
_ dName: UnsafePointer<CChar>?,
_ tName: UnsafePointer<CChar>?,
_ rowID: sqlite3_int64
) {
guard let ctx = ctx else { return }
let connection = Unmanaged<Connection>
.fromOpaque(ctx)
.takeUnretainedValue()
if let delegate = connection.delegate {
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
}
delegate.connection(connection, didUpdate: updateAction)
}
}
private func commitHookCallback(_ ctx: UnsafeMutableRawPointer?) -> Int32 {
do {
guard let ctx = ctx else { return SQLITE_OK }
let connection = Unmanaged<Connection>
.fromOpaque(ctx)
.takeUnretainedValue()
if let delegate = connection.delegate {
try delegate.connectionDidCommit(connection)
}
return SQLITE_OK
} catch {
return SQLITE_ERROR
}
}
private func rollbackHookCallback(_ ctx: UnsafeMutableRawPointer?) {
guard let ctx = ctx else { return }
let connection = Unmanaged<Connection>
.fromOpaque(ctx)
.takeUnretainedValue()
if let delegate = connection.delegate {
delegate.connectionDidRollback(connection)
}
}

View File

@@ -0,0 +1,375 @@
import Foundation
import DataLiteC
extension Function {
/// Base class for creating 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:
///
/// - ``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.
///
/// ### 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 {
/// case argumentsWrong
/// }
///
/// override class var argc: Int32 { 1 }
/// override class var name: String { "sum_aggregate" }
/// override class var options: Function.Options {
/// [.deterministic, .innocuous]
/// }
///
/// private var sum: Int = 0
///
/// override func step(args: Arguments) throws {
/// guard let value = args[0] as Int? else {
/// throw Error.argumentsWrong
/// }
/// sum += value
/// }
///
/// override func finalize() throws -> SQLiteRawRepresentable? {
/// return sum
/// }
/// }
/// ```
///
/// ### Usage
///
/// To use a custom aggregate function, first establish a database connection and
/// register the function.
///
/// ```swift
/// let connection = try Connection(
/// path: dbFileURL.path,
/// options: [.create, .readwrite]
/// )
/// try connection.add(function: SumAggregate.self)
/// ```
///
/// ### 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
///
/// - ``init()``
///
/// ### Instance Methods
///
/// - ``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.
///
/// 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 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.
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) {
let context = Context(function: self)
let ctx = Unmanaged.passRetained(context).toOpaque()
let status = sqlite3_create_function_v2(
connection, name, argc, opts, ctx,
nil, xStep(_:_:_:), xFinal(_:), xDestroy(_:)
)
if status != SQLITE_OK {
throw Connection.Error(connection)
}
}
/// Called for each input value during 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.
///
/// ```swift
/// class MyCustomAggregate: Function.Aggregate {
/// // ...
///
/// 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.")
}
/// Called when the aggregate computation is complete.
///
/// 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`.
///
/// ```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.")
}
}
}
// 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>
.fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue()
let function = context
.function(ctx: ctx)
.takeUnretainedValue()
assert(!function.hasErrored)
do {
let args = Function.Arguments(argc: argc, argv: argv)
try function.step(args: args)
} catch {
let name = type(of: function).name
let description = error.localizedDescription
let message = "Error executing function '\(name)': \(description)"
function.hasErrored = true
sqlite3_result_error(ctx, message, -1)
}
}
/// 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>
.fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue()
let unmanagedFunction = context.function(ctx: ctx)
let function = unmanagedFunction.takeUnretainedValue()
defer { unmanagedFunction.release() }
guard !function.hasErrored else { return }
do {
let result = try function.finalize()
sqlite3_result_value(ctx, result?.sqliteRawValue)
} catch {
let name = type(of: function).name
let description = error.localizedDescription
let message = "Error executing function '\(name)': \(description)"
sqlite3_result_error(ctx, message, -1)
}
}
/// C callback function to destroy the context associated with the custom SQLite aggregate function.
///
/// This function is called by SQLite when the function is uninstalled. It frees the memory
/// allocated for the `Context` object associated with the function to avoid memory leaks.
///
/// - Parameter ctx: Pointer to the SQLite query context.
private func xDestroy(_ ctx: UnsafeMutableRawPointer?) {
guard let ctx else { return }
Unmanaged<AnyObject>.fromOpaque(ctx).release()
}

View File

@@ -0,0 +1,161 @@
import Foundation
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
// 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.
public var count: Int {
Int(argc)
}
/// A Boolean value indicating whether there are no arguments passed to the SQLite function.
public var isEmpty: Bool {
count == 0
}
/// The starting index of the arguments passed to the SQLite function.
public var startIndex: Index {
0
}
/// The ending index of the arguments passed to the SQLite function.
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
}
// MARK: - Subscripts
/// Accesses the SQLite value at the specified index.
///
/// - Parameter index: The index of the SQLite value to access.
/// - 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")
}
let arg = argv.unsafelyUnwrapped[index]
switch sqlite3_value_type(arg) {
case SQLITE_INTEGER: return .int(sqlite3_value_int64(arg))
case SQLITE_FLOAT: return .real(sqlite3_value_double(arg))
case SQLITE_TEXT: return .text(sqlite3_value_text(arg))
case SQLITE_BLOB: return .blob(sqlite3_value_blob(arg))
default: return .null
}
}
/// 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.
///
/// - Complexity: O(1)
public func index(after i: Index) -> Index {
i + 1
}
}
}
// 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),
count: Int(sqlite3_value_bytes(value))
)
}

View File

@@ -0,0 +1,62 @@
import Foundation
import DataLiteC
extension Function {
/// An option set representing the options 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.
///
/// Example usage:
/// ```swift
/// let options: Function.Options = [.deterministic, .directonly]
/// ```
///
/// - SeeAlso: [SQLite Function Flags](https://www.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.
public var rawValue: Int32
// MARK: - Options
/// Indicates that the function is 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.
public static let deterministic = Self(rawValue: SQLITE_DETERMINISTIC)
/// Indicates that the function may only be invoked 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.
///
/// 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.
public static let directonly = Self(rawValue: SQLITE_DIRECTONLY)
/// Indicates that the function is 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.
///
/// `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.
public static let innocuous = Self(rawValue: SQLITE_INNOCUOUS)
// MARK: - Inits
/// Creates an SQLite function option set from a raw value.
///
/// - Parameter rawValue: The raw value representing the SQLite function options.
public init(rawValue: Int32) {
self.rawValue = rawValue
}
}
}

View File

@@ -0,0 +1,80 @@
import Foundation
extension Function {
/// A scalar SQL function that performs regular expression matching.
///
/// This function checks whether a given string matches a specified regular expression pattern
/// and returns `true` if it matches, or `false` otherwise.
///
/// ```swift
/// let connection = try Connection(
/// location: .inMemory,
/// options: [.create, .readwrite]
/// )
/// try connection.add(function: Function.Regexp.self)
///
/// try connection.execute(sql: """
/// SELECT * FROM users WHERE name REGEXP 'John.*';
/// """)
/// ```
@available(iOS 16.0, *)
@available(macOS 13.0, *)
public final class Regexp: Scalar {
/// Errors that can occur during the evaluation of the `REGEXP` function.
public enum Error: Swift.Error {
/// Thrown when the arguments provided to the function are invalid.
case invalidArguments
/// Thrown when an error occurs while processing the regular expression.
/// - Parameter error: The underlying error from the regex operation.
case regexError(Swift.Error)
}
// MARK: - Properties
/// The number of arguments required by the function.
///
/// The `REGEXP` function expects exactly two arguments:
/// 1. A string containing the regular expression pattern.
/// 2. A string value to be evaluated against the pattern.
public override class var argc: Int32 { 2 }
/// The name of the SQL function.
///
/// The SQL function can be invoked in queries using the name `REGEXP`.
public override class var name: String { "REGEXP" }
/// Options that define the behavior of the SQL function.
///
/// - `deterministic`: Ensures the same result is returned for identical inputs.
/// - `innocuous`: Indicates the function does not have any side effects.
public override class var options: Options {
[.deterministic, .innocuous]
}
// MARK: - Methods
/// Invokes the `REGEXP` function to evaluate whether a string matches a regular expression.
///
/// - Parameters:
/// - args: The arguments provided to the SQL function.
/// - `args[0]`: The regular expression pattern as a `String`.
/// - `args[1]`: The value to match against the pattern as a `String`.
/// - Returns: `true` if the value matches the pattern, `false` otherwise.
/// - 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? {
guard let regex = args[0] as String?,
let value = args[1] as String?
else { throw Error.invalidArguments }
do {
return try Regex(regex).wholeMatch(in: value) != nil
} catch {
throw Error.regexError(error)
}
}
}
}

View File

@@ -0,0 +1,221 @@
import Foundation
import DataLiteC
extension Function {
/// A base class for creating 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.
///
/// ### 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 {
/// enum Error: Swift.Error {
/// case argumentsWrong
/// 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? {
/// guard let regex = args[0] as String?,
/// let value = args[1] as String?
/// else { throw Error.argumentsWrong }
/// do {
/// return try Regex(regex).wholeMatch(in: value) != nil
/// } catch {
/// throw Error.regexError(error)
/// }
/// }
/// }
/// ```
///
/// ### 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:
///
/// ```swift
/// let connection = try Connection(
/// path: dbFileURL.path,
/// options: [.create, .readwrite]
/// )
/// try connection.add(function: Regexp.self)
/// ```
///
/// ### 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:
///
/// ```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) {
let context = Context(function: self)
let ctx = Unmanaged.passRetained(context).toOpaque()
let status = sqlite3_create_function_v2(
connection, name, argc, opts, ctx,
xFunc(_:_:_:), nil, nil, xDestroy(_:)
)
if status != SQLITE_OK {
throw Connection.Error(connection)
}
}
/// Implementation 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``.
///
/// - Parameter args: An ``Arguments`` object containing the input arguments of the
/// function.
///
/// - 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? {
fatalError("Subclasses must override this method to implement function logic.")
}
}
}
// 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>
.fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue()
do {
let args = Function.Arguments(argc: argc, argv: argv)
let result = try context.function.invoke(args: args)
sqlite3_result_value(ctx, result?.sqliteRawValue)
} catch {
let name = context.function.name
let description = error.localizedDescription
let message = "Error executing function '\(name)': \(description)"
sqlite3_result_error(ctx, message, -1)
}
}
/// The C function callback for destroying the context associated with a custom SQLite scalar function.
///
/// This function is called by SQLite when the function is uninstalled. It releases the memory
/// allocated for the `Context` object associated with the function to avoid memory leaks.
///
/// - Parameter ctx: A pointer to the context of the SQLite query. This context contains the
/// `Context` object that should be released.
///
/// - Note: The `xDestroy` function should only release the memory allocated for the `Context`
/// object. It should not perform any other operations or access the context beyond freeing
/// the memory.
private func xDestroy(_ ctx: UnsafeMutableRawPointer?) {
guard let ctx else { return }
Unmanaged<AnyObject>.fromOpaque(ctx).release()
}

View File

@@ -0,0 +1,168 @@
import Foundation
import DataLiteC
/// A base class representing a custom 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.
///
/// 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.
///
/// ## Topics
///
/// ### Base Function Classes
///
/// - ``Aggregate``
/// - ``Scalar``
///
/// ### Custom Function Classes
///
/// - ``Regexp``
open class Function {
// MARK: - Properties
/// The number of arguments that the custom SQLite 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.
open class var argc: Int32 {
fatalError("Subclasses must override this property to specify the number of arguments.")
}
/// The name of the custom SQLite 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.
open class var name: String {
fatalError("Subclasses must override this property to provide the function name.")
}
/// The options for the custom SQLite 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`.
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)
return options.rawValue
}
// 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) {
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) {
let status = sqlite3_create_function_v2(
connection,
name, argc, opts,
nil, nil, nil, nil, nil
)
if status != SQLITE_OK {
throw Connection.Error(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?) {
switch value ?? .null {
case .int(let value): sqlite3_result_int64(ctx, value)
case .real(let value): sqlite3_result_double(ctx, value)
case .text(let value): sqlite3_result_text(ctx, value)
case .blob(let value): sqlite3_result_blob(ctx, value)
case .null: sqlite3_result_null(ctx)
}
}

View File

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

View File

@@ -0,0 +1,126 @@
import Foundation
import DataLiteC
extension Statement {
/// Provides a set of options for preparing SQLite statements.
///
/// 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.
///
/// ## 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.
///
/// ## Topics
///
/// ### Initializers
///
/// - ``init(rawValue:)-(Int32)``
/// - ``init(rawValue:)-(UInt32)``
///
/// ### Instance Properties
///
/// - ``rawValue``
///
/// ### Type Properties
///
/// - ``persistent``
/// - ``noVtab``
public struct Options: OptionSet, Sendable {
// MARK: - Properties
/// The underlying raw value representing the set of options as a bitmask.
///
/// 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.
public var rawValue: UInt32
/// Specifies that the prepared statement should be 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.
///
/// 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))
/// Specifies that virtual tables should not be used 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))
// MARK: - Inits
/// Initializes an `Options` instance with the given `UInt32` raw 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.
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
/// Initializes an `Options` instance with the given `Int32` raw value.
///
/// This initializer allows the use of `Int32` values directly, converting them to the `UInt32` type
/// required for bitmask operations.
///
/// ## 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.
public init(rawValue: Int32) {
self.rawValue = UInt32(rawValue)
}
}
}

View File

@@ -0,0 +1,731 @@
import Foundation
import DataLiteC
/// A value representing a static destructor for SQLite.
///
/// `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.
///
/// `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)")
/// }
/// ```
///
/// ## Topics
///
/// ### Subtypes
///
/// - ``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 {
// 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) {
var statement: OpaquePointer! = 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)
}
}
/// 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 {
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 {
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)
}
/// 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)
}
if status != SQLITE_OK {
throw Connection.Error(connection)
}
}
/// Binds a value conforming to `RawBindable` to a parameter at a specified index.
///
/// This method provides a generic way to bind values that conform to `RawBindable`,
/// allowing for flexibility in the types of values that can be bound to SQL statements.
///
/// - Parameters:
/// - value: The value to bind to the parameter.
/// - index: The index of the parameter to bind (1-based).
/// - Throws: ``Connection/Error`` if the binding operation fails.
public func bind<T: SQLiteRawBindable>(_ value: T?, at index: Int32) throws {
try bind(value?.sqliteRawValue ?? .null, at: index)
}
/// Binds all values from a `SQLiteRow` to their corresponding named parameters in the statement.
///
/// This method iterates through each key-value pair in the given `SQLiteRow` and binds the value to
/// the statements named parameter using the `:<column>` syntax. Column names from the row must
/// match named parameters defined in the SQL statement.
///
/// For example, a column named `"userID"` will be bound to a parameter `:userID` in the SQL.
///
/// - Throws: ``Connection/Error`` if a parameter is missing or if a binding operation fails.
public func bind(_ row: SQLiteRow) throws {
try row.forEach { column, value in
try bind(value, at: bind(parameterIndexBy: ":\(column)"))
}
}
/// Binds all values from an `Arguments` instance to their corresponding parameters in the statement.
///
/// This method iterates through each tokenvalue pair in the provided `Arguments` collection and binds
/// the value to the appropriate parameter in the SQL statement. Both indexed (`?NNN`) and named (`:name`)
/// parameters are supported.
///
/// - Parameter arguments: The `Arguments` instance containing tokens and their associated values.
/// - Throws: ``Connection/Error`` if a parameter is not found or if the binding fails.
public func bind(_ arguments: Arguments) throws {
try arguments.forEach { token, value in
let index = switch token {
case .indexed(let index):
Int32(index)
case .named(let name):
bind(parameterIndexBy: ":\(name)")
}
try bind(value, at: index)
}
}
/// Clears all parameter bindings from the statement.
///
/// This method resets any parameter bindings, allowing you to reuse the same SQL statement
/// with different parameter values. This is useful for executing the same statement multiple
/// times with different parameters.
///
/// - Throws: ``Connection/Error`` if the operation to clear bindings fails.
public func clearBindings() throws {
if sqlite3_clear_bindings(statement) != SQLITE_OK {
throw Connection.Error(connection)
}
}
// MARK: - Retrieving Results
/// 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
}
/// 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
}
}
/// Retrieves the value from a column at a specified index and converts it to a value
/// conforming to `SQLiteRawRepresentable`.
///
/// This method provides a way to convert column values into types that conform to
/// ``SQLiteRawRepresentable``, allowing for easier integration with custom data models.
///
/// - Parameters:
/// - index: The index of the column (0-based).
/// - Returns: The value from the column converted to `T`, or `nil` if conversion fails.
public func columnValue<T: SQLiteRawRepresentable>(at index: Int32) -> T? {
T(columnValue(at: index))
}
/// Retrieves the current row of the result set as a `SQLiteRow` instance.
///
/// This method iterates over the columns of the current row in the result set.
/// For each column, it retrieves the column name and the corresponding value using the
/// ``columnName(at:)`` and ``columnValue(at:)->SQLiteRawValue`` methods.
/// It then populates a ``SQLiteRow`` instance with these column-value pairs.
///
/// - Returns: A `SQLiteRow` instance representing the current row of the result set.
public func currentRow() -> SQLiteRow {
var row = SQLiteRow()
for index in 0..<columnCount() {
let name = columnName(at: index)
let value = columnValue(at: index)
row[name] = value
}
return row
}
// MARK: - Evaluating
/// Advances to the next row in the result set.
///
/// This method steps through the result set row by row, returning `true` if there is a row
/// available and `false` if the end of the result set is reached.
///
/// - Returns: `true` if there is a row available, `false` if the end of the result set is
/// reached.
/// - Throws: ``Connection/Error`` if an error occurs during execution.
@discardableResult
public func step() throws(Connection.Error) -> Bool {
switch sqlite3_step(statement) {
case SQLITE_ROW: return true
case SQLITE_DONE: return false
default: throw Connection.Error(connection)
}
}
/// Resets the prepared SQL statement to its initial state.
///
/// Use this method before re-executing the statement. It does not clear the bound parameters,
/// allowing their values to persist between executions. To clear the parameters, use the
/// `clearBindings()` method.
///
/// - Throws: ``Connection/Error`` if the statement reset fails.
public func reset() throws {
if sqlite3_reset(statement) != SQLITE_OK {
throw Connection.Error(connection)
}
}
/// Executes the statement once for each row, returning the collected result rows if any.
///
/// This method binds each rows named values to the statement parameters and executes the
/// statement. After each execution, any resulting rows are collected and returned. If the `rows`
/// array is empty, the statement will still execute once with no parameters bound.
///
/// Use this method for queries such as `INSERT` or `UPDATE` statements with changing
/// parameter values.
///
/// - Note: If `rows` is empty, the statement executes once with no bound values.
///
/// - Parameter rows: A list of `SQLiteRow` values to bind to the statement.
/// - Returns: An array of result rows collected from all executions of the statement.
/// - Throws: ``Connection/Error`` if binding or execution fails.
@discardableResult
public func execute(rows: [SQLiteRow]) throws -> [SQLiteRow] {
var result = [SQLiteRow]()
var index = 0
repeat {
if rows.count > index {
try bind(rows[index])
}
while try step() {
result.append(currentRow())
}
try clearBindings()
try reset()
index += 1
} while index < rows.count
return result
}
/// Executes the statement once for each arguments set, returning any resulting rows.
///
/// This method binds each `Arguments` set (indexed or named) to the statement and executes it. All
/// result rows from each execution are collected and returned. If no arguments are provided, the
/// statement executes once with no values bound.
///
/// Use this method for queries such as `SELECT`, `INSERT`, `UPDATE`, or `DELETE` where results
/// may be expected and multiple executions are needed.
///
/// ```swift
/// let stmt = try connection.prepare(
/// sql: "SELECT * FROM logs WHERE level = :level"
/// )
/// let result = try stmt.execute(args: [
/// ["level": "info"],
/// ["level": "error"]
/// ])
/// ```
///
/// - Note: If `args` is `nil` or empty, the statement executes once with no bound values.
///
/// - Parameter args: A list of `Arguments` to bind and execute. Defaults to `nil`.
/// - Returns: A flat array of result rows produced by all executions.
/// - Throws: ``Connection/Error`` if binding or execution fails.
@discardableResult
public func execute(args: [Arguments]? = nil) throws -> [SQLiteRow] {
var result = [SQLiteRow]()
var index = 0
repeat {
if let args, args.count > index {
try bind(args[index])
}
while try step() {
result.append(currentRow())
}
try clearBindings()
try reset()
index += 1
} while index < args?.count ?? 0
return result
}
// MARK: - Equatable
/// Compares two `Statement` instances for equality.
///
/// This method checks whether two `Statement` instances are equal by comparing their
/// underlying SQLite statement pointers and connection pointers.
///
/// - Parameters:
/// - lhs: The first `Statement` instance.
/// - rhs: The second `Statement` instance.
/// - Returns: `true` if the two instances are equal, `false` otherwise.
public static func == (lhs: Statement, rhs: Statement) -> Bool {
lhs.statement == rhs.statement && lhs.connection == rhs.connection
}
// MARK: - Hashable
/// Computes a hash value for the `Statement` instance.
///
/// This method computes a hash value based on the SQLite statement pointer and connection
/// pointer. It is used to support hash-based collections like sets and dictionaries.
///
/// - Parameter hasher: The hasher to use for computing the hash value.
public func hash(into hasher: inout Hasher) {
hasher.combine(statement)
hasher.combine(connection)
}
}
// MARK: - Functions
/// Binds a string to a parameter in an SQL statement.
///
/// - Parameters:
/// - stmt: A pointer to the prepared SQL statement.
/// - index: The index of the parameter (1-based).
/// - string: The string to be bound to the parameter.
/// - Returns: SQLite error code if binding fails.
private func sqlite3_bind_text(_ stmt: OpaquePointer!, _ index: Int32, _ string: String) -> Int32 {
sqlite3_bind_text(stmt, index, string, -1, SQLITE_TRANSIENT)
}
/// Binds binary data to a parameter in an SQL statement.
///
/// - Parameters:
/// - stmt: A pointer to the prepared SQL statement.
/// - index: The index of the parameter (1-based).
/// - data: The `Data` to be bound to the parameter.
/// - Returns: SQLite error code if binding fails.
private func sqlite3_bind_blob(_ stmt: OpaquePointer!, _ index: Int32, _ data: Data) -> Int32 {
data.withUnsafeBytes {
sqlite3_bind_blob(stmt, index, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT)
}
}
/// Retrieves text data from a result column of an SQL statement.
///
/// - Parameters:
/// - stmt: A pointer to the prepared SQL statement.
/// - iCol: The column index.
/// - Returns: A `String` containing the text data from the specified column.
private func sqlite3_column_text(_ stmt: OpaquePointer!, _ iCol: Int32) -> String {
String(cString: DataLiteC.sqlite3_column_text(stmt, iCol))
}
/// Retrieves binary data from a result column of an SQL statement.
///
/// - Parameters:
/// - stmt: A pointer to the prepared SQL statement.
/// - iCol: The column index.
/// - Returns: A `Data` object containing the binary data from the specified column.
private func sqlite3_column_blob(_ stmt: OpaquePointer!, _ iCol: Int32) -> Data {
Data(
bytes: sqlite3_column_blob(stmt, iCol),
count: Int(sqlite3_column_bytes(stmt, iCol))
)
}

View File

@@ -0,0 +1,30 @@
# ``DataLiteCore/Connection/init(location:options:)``
Initializes a new connection to an SQLite database.
This initializer opens a connection to the SQLite database at the specified `location`
with the provided `options`. If the location is a file path, it ensures the necessary
directory exists, creating intermediate directories if needed.
```swift
do {
let connection = try Connection(
location: .file(path: "~/example.db"),
options: .readwrite
)
// Use the connection to execute queries
} catch {
print("Error establishing connection: \(error)")
}
```
- Parameters:
- location: Specifies where the database is located.
Can be a file path, an in-memory database, or a temporary database.
- options: Configures connection behavior,
such as read-only or read-write access and cache mode.
- Throws: ``Connection/Error`` if the connection fails to open due to SQLite errors,
invalid path, permission issues, or other underlying failures.
- Throws: An error if directory creation fails for file-based database locations.

View File

@@ -0,0 +1,31 @@
# ``DataLiteCore/Connection/init(path:options:)``
Initializes a new connection to an SQLite database using a file path.
This convenience initializer sets up a connection to the SQLite database located at the
specified `path` with the provided `options`. It internally calls the main initializer
to manage the connection setup.
### Usage Example
```swift
do {
let connection = try Connection(
path: "~/example.db",
options: .readwrite
)
// Use the connection to execute queries
} catch {
print("Error establishing connection: \(error)")
}
```
- Parameters:
- path: A string representing the file path to the SQLite database.
- options: Configures the connection behavior,
such as read-only or read-write access and cache mode.
- Throws: ``Connection/Error`` if the connection fails to open due to SQLite errors,
invalid path, permission issues, or other underlying failures.
- Throws: An error if subdirectories for the database file cannot be created.

View File

@@ -0,0 +1,298 @@
# ``DataLiteCore/Connection``
A class representing a connection to an SQLite database.
## Overview
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.
## Opening a New Connection
Use the ``init(location:options:)`` initializer to open a database connection. Specify the
database's location using the ``Location`` parameter and configure connection settings with the
``Options`` parameter.
```swift
do {
let connection = try Connection(
location: .file(path: "~/example.db"),
options: [.readwrite, .create]
)
print("Connection established")
} catch {
print("Failed to connect: \(error)")
}
```
## Closing the Connection
The `Connection` class automatically closes the database connection when the object is
deallocated (`deinit`). This ensures proper cleanup even if the object goes out of scope.
## Delegate
The `Connection` class can optionally use a delegate to handle specific events during the
connection lifecycle, such as tracing SQL statements or responding to transaction actions.
The delegate must conform to the ``ConnectionDelegate`` protocol, which provides methods for
handling these events.
## Custom SQL Functions
The `Connection` class allows you to add custom SQL functions using subclasses of ``Function``.
You can create either **scalar** functions (which return a single value) or **aggregate**
functions (which perform operations across multiple rows). Both types can be used directly in
SQL queries.
To add or remove custom functions, use the ``add(function:)`` and ``remove(function:)`` methods
of the `Connection` class.
## Preparing SQL Statements
The `Connection` class provides functionality for preparing SQL statements that can be
executed multiple times with different parameter values. The ``prepare(sql:options:)`` method
takes a SQL query as a string and an optional ``Statement/Options`` parameter to configure
the behavior of the statement. It returns a ``Statement`` object that can be executed.
```swift
do {
let statement = try connection.prepare(
sql: "SELECT * FROM users WHERE age > ?",
options: [.persistent]
)
// Bind parameters and execute the statement
} catch {
print("Error preparing statement: \(error)")
}
```
## Executing SQL Scripts
The `Connection` class allows you to execute a series of SQL statements using the ``SQLScript``
structure. The ``SQLScript`` structure is designed to load and process multiple SQL queries
from a file, URL, or string.
You can create an instance of ``SQLScript`` with the SQL script content and then pass it to the
``execute(sql:)`` method of the `Connection` class to execute the script.
```swift
let script: SQLScript = """
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO users (name) VALUES ('Alice');
INSERT INTO users (name) VALUES ('Bob');
"""
do {
try connection.execute(sql: script)
print("Script executed successfully")
} catch {
print("Error executing script: \(error)")
}
```
## Transaction Handling
By default, the `Connection` class operates in **autocommit mode**, where each SQL statement is
automatically committed after execution. In this mode, each statement is treated as a separate
transaction, eliminating the need for explicit transaction management. To determine whether the
connection is in autocommit mode, use the ``isAutocommit`` property.
For manual transaction management, use ``beginTransaction(_:)`` to start a transaction, and
``commitTransaction()`` or ``rollbackTransaction()`` to either commit or roll back the
transaction.
```swift
do {
try connection.beginTransaction()
try connection.execute(sql: "INSERT INTO users (name) VALUES ('Alice')")
try connection.execute(sql: "INSERT INTO users (name) VALUES ('Bob')")
try connection.commitTransaction()
print("Transaction committed successfully")
} catch {
try? connection.rollbackTransaction()
print("Error during transaction: \(error)")
}
```
Learn more in the [SQLite Transaction Documentation](https://www.sqlite.org/lang_transaction.html).
## Error Handling
The `Connection` class uses Swift's throwing mechanism to handle errors. Errors in database
operations are propagated using `throws`, allowing you to catch and handle specific issues in
your application.
SQLite-related errors, such as invalid SQL queries, connection failures, or issues with
transaction management, throw an ``Connection/Error`` struct. These errors conform to the
`Error` protocol, and you can handle them using Swift's `do-catch` syntax to manage exceptions
in your code.
```swift
do {
let statement = try connection.prepare(
sql: "SELECT * FROM users WHERE age > ?",
options: []
)
} catch let error as Error {
print("SQLite error: \(error.mesg), Code: \(error.code)")
} catch {
print("Unexpected error: \(error)")
}
```
## Multithreading
The `Connection` class supports multithreading, but its behavior depends on the selected
thread-safety mode. You can configure the desired mode using the ``Options`` parameter in the
``init(location:options:)`` method.
**Multi-thread** (``Options/nomutex``): This mode allows SQLite to be used across multiple
threads. However, it requires that no `Connection` instance or its derived objects (e.g.,
prepared statements) are accessed simultaneously by multiple threads.
```swift
let connection = try Connection(
location: .file(path: "~/example.db"),
options: [.readwrite, .nomutex]
)
```
**Serialized** (``Options/fullmutex``): In this mode, SQLite uses internal mutexes to ensure
thread safety. This allows multiple threads to safely share `Connection` instances and their
derived objects.
```swift
let connection = try Connection(
location: .file(path: "~/example.db"),
options: [.readwrite, .fullmutex]
)
```
- Important: The `Connection` class does not include built-in synchronization for shared
resources. Developers must implement custom synchronization mechanisms, such as using
`DispatchQueue`, when sharing resources across threads.
For more details, see the [Using SQLite in Multi-Threaded Applications](https://www.sqlite.org/threadsafe.html).
## Encryption
The `Connection` class supports transparent encryption and re-encryption of databases using the
``apply(_:name:)`` and ``rekey(_:name:)`` methods. This allows sensitive data to be securely
stored on disk.
### Applying an Encryption Key
To open an encrypted database or encrypt a new one, call ``apply(_:name:)`` immediately after
initializing the connection, and before executing any SQL statements.
```swift
let connection = try Connection(
path: "~/secure.db",
options: [.readwrite, .create]
)
try connection.apply(Key.passphrase("secret-password"))
```
- If the database is already encrypted, the key must match the one previously used.
- If the database is unencrypted, applying a key will encrypt it on first write.
You can use either a **passphrase**, which is internally transformed into a key,
or a **raw key**:
```swift
try connection.apply(Key.raw(data: rawKeyData))
```
- Important: The encryption key must be applied *before* any SQL queries are executed.
Otherwise, the database may remain unencrypted or unreadable.
### Rekeying the Database
To change the encryption key of an existing database, you must first apply the current key
using ``apply(_:name:)``, then call ``rekey(_:name:)`` with the new key.
```swift
let connection = try Connection(
path: "~/secure.db",
options: [.readwrite]
)
try connection.apply(Key.passphrase("old-password"))
try connection.rekey(Key.passphrase("new-password"))
```
- Important: ``rekey(_:name:)`` requires that the correct current key has already been applied
via ``apply(_:name:)``. If the wrong key is used, the operation will fail with an error.
### Attached Databases
Both ``apply(_:name:)`` and ``rekey(_:name:)`` accept an optional `name` parameter to operate
on an attached database. If omitted, they apply to the main database.
## Topics
### Errors
- ``Error``
### Initializers
- ``Location``
- ``Options``
- ``init(location:options:)``
- ``init(path:options:)``
### Delegation
- ``ConnectionDelegate``
- ``delegate``
### Connection State
- ``isAutocommit``
- ``isReadonly``
- ``busyTimeout``
### PRAGMA Accessors
- ``applicationID``
- ``foreignKeys``
- ``journalMode``
- ``synchronous``
- ``userVersion``
### SQLite Lifecycle
- ``initialize()``
- ``shutdown()``
### Custom SQL Functions
- ``add(function:)``
- ``remove(function:)``
### Statement Preparation
- ``prepare(sql:options:)``
### Script Execution
- ``execute(sql:)``
- ``execute(raw:)``
### PRAGMA Execution
- ``get(pragma:)``
- ``set(pragma:value:)``
### Transactions
- ``beginTransaction(_:)``
- ``commitTransaction()``
- ``rollbackTransaction()``
### Encryption Keys
- ``Connection/Key``
- ``apply(_:name:)``
- ``rekey(_:name:)``

View File

@@ -0,0 +1,7 @@
# ``DataLiteCore``
**DataLiteCore** is an intuitive library for working with SQLite in Swift applications.
## Overview
**DataLiteCore** provides an object-oriented API over the C interface, allowing developers to easily integrate SQLite functionality into their projects. The library offers powerful capabilities for database management and executing SQL queries while maintaining the simplicity and flexibility of the native Swift interface.

View File

@@ -0,0 +1,82 @@
import Foundation
/// Represents the journal modes available for an SQLite database.
///
/// The journal mode determines how the database handles transactions and how it
/// maintains the journal for rollback and recovery. For more details, refer to
/// [Journal Mode Pragma](https://www.sqlite.org/pragma.html#pragma_journal_mode).
public enum JournalMode: String, SQLiteRawRepresentable {
/// DELETE journal mode.
///
/// This is the default behavior. The rollback journal is deleted at the conclusion
/// of each transaction. The delete operation itself causes the transaction to commit.
/// For more details, refer to the
/// [Atomic Commit In SQLite](https://www.sqlite.org/atomiccommit.html).
case delete
/// TRUNCATE journal mode.
///
/// In this mode, the rollback journal is truncated to zero length at the end of each transaction
/// instead of being deleted. On many systems, truncating a file is much faster than deleting it
/// because truncating does not require modifying the containing directory.
case truncate
/// PERSIST journal mode.
///
/// In this mode, the rollback journal is not deleted at the end of each transaction. Instead,
/// the header of the journal is overwritten with zeros. This prevents other database connections
/// from rolling the journal back. The PERSIST mode is useful as an optimization on platforms
/// where deleting or truncating a file is more expensive than overwriting the first block of a file
/// with zeros. For additional configuration, refer to
/// [journal_size_limit](https://www.sqlite.org/pragma.html#pragma_journal_size_limit).
case persist
/// MEMORY journal mode.
///
/// In this mode, the rollback journal is stored entirely in volatile RAM rather than on disk.
/// This saves disk I/O but at the expense of database safety and integrity. If the application
/// crashes during a transaction, the database file will likely become corrupt.
case memory
/// Write-Ahead Logging (WAL) journal mode.
///
/// This mode uses a write-ahead log instead of a rollback journal to implement transactions.
/// The WAL mode is persistent, meaning it stays in effect across multiple database connections
/// and persists even after closing and reopening the database. For more details, refer to the
/// [Write-Ahead Logging](https://www.sqlite.org/wal.html).
case wal
/// OFF journal mode.
///
/// In this mode, the rollback journal is completely disabled, meaning no rollback journal is ever created.
/// This disables SQLite's atomic commit and rollback capabilities. The `ROLLBACK` command will no longer work
/// and behaves in an undefined way. Applications must avoid using the `ROLLBACK` command when the journal mode is OFF.
/// If the application crashes in the middle of a transaction, the database file will likely become corrupt,
/// as there is no way to unwind partially completed operations. For example, if a duplicate entry causes a
/// `CREATE UNIQUE INDEX` statement to fail halfway through, it will leave behind a partially created index,
/// resulting in a corrupted database state.
case off
public var rawValue: String {
switch self {
case .delete: "DELETE"
case .truncate: "TRUNCATE"
case .persist: "PERSIST"
case .memory: "MEMORY"
case .wal: "WAL"
case .off: "OFF"
}
}
public init?(rawValue: String) {
switch rawValue.uppercased() {
case "DELETE": self = .delete
case "TRUNCATE": self = .truncate
case "PERSIST": self = .persist
case "MEMORY": self = .memory
case "WAL": self = .wal
case "OFF": self = .off
default: return nil
}
}
}

View File

@@ -0,0 +1,40 @@
import Foundation
/// Represents different types of database update actions.
///
/// The `SQLiteAction` enum is used to identify the type of action
/// performed on a database, such as insertion, updating, or deletion.
public enum SQLiteAction {
/// Indicates the insertion of a new row into a table.
///
/// This case is used to represent the action of adding a new
/// row to a specific table in a database.
///
/// - Parameters:
/// - db: The name of the database where the insertion occurred.
/// - table: The name of the table where the insertion occurred.
/// - rowID: The row ID of the newly inserted row.
case insert(db: String, table: String, rowID: Int64)
/// Indicates the modification of an existing row in a table.
///
/// This case is used to represent the action of updating an
/// existing row within a specific table in a database.
///
/// - Parameters:
/// - db: The name of the database where the update occurred.
/// - table: The name of the table where the update occurred.
/// - rowID: The row ID of the updated row.
case update(db: String, table: String, rowID: Int64)
/// Indicates the removal of a row from a table.
///
/// This case is used to represent the action of deleting a
/// row from a specific table in a database.
///
/// - Parameters:
/// - db: The name of the database from which the row was deleted.
/// - table: The name of the table from which the row was deleted.
/// - rowID: The row ID of the deleted row.
case delete(db: String, table: String, rowID: Int64)
}

View File

@@ -0,0 +1,75 @@
import Foundation
import DataLiteC
/// Represents different types of columns in an SQLite database.
///
/// The `SQLiteRawType` enum encapsulates the various data types that SQLite supports for columns.
/// Each case in the enum corresponds to a specific SQLite data type, providing a way to work with these
/// types in a type-safe manner. This enum allows for easier handling of SQLite column types by abstracting
/// their raw representations and offering more readable code.
/// For more details, refer to [Datatypes In SQLite](https://www.sqlite.org/datatype3.html).
///
/// ## Topics
///
/// ### Enumeration Cases
///
/// - ``int``
/// - ``real``
/// - ``text``
/// - ``blob``
/// - ``null``
///
/// ### Instance Properties
///
/// - ``rawValue``
///
/// ### Initializers
///
/// - ``init(rawValue:)``
public enum SQLiteRawType: Int32 {
/// The data type of an integer column.
case int
/// The data type of a real (floating point) column.
case real
/// The data type of a text (string) column.
case text
/// The data type of a blob (binary large object) column.
case blob
/// The data type of a NULL column.
case null
/// Returns the raw SQLite data type value corresponding to the column type.
///
/// This computed property provides the raw integer value used by SQLite to represent each column type.
///
/// - Returns: An `Int32` representing the SQLite data type constant.
public var rawValue: Int32 {
switch self {
case .int: return SQLITE_INTEGER
case .real: return SQLITE_FLOAT
case .text: return SQLITE_TEXT
case .blob: return SQLITE_BLOB
case .null: return SQLITE_NULL
}
}
/// Initializes a `SQLiteRawType` enum case from its raw value.
///
/// This initializer maps a raw `Int32` value (SQLite constant) to the corresponding enum case.
///
/// - Parameter rawValue: The raw value representing the column type as defined by SQLite.
public init?(rawValue: Int32) {
switch rawValue {
case SQLITE_INTEGER: self = .int
case SQLITE_FLOAT: self = .real
case SQLITE_TEXT: self = .text
case SQLITE_BLOB: self = .blob
case SQLITE_NULL: self = .null
default: return nil
}
}
}

View File

@@ -0,0 +1,99 @@
import Foundation
/// An enumeration that represents the different types of raw values in an SQLite database.
///
/// This type is used to store values retrieved from or stored in an SQLite database. It supports
/// various data types such as integers, floating-point numbers, text, binary data, and null values.
/// For more details, refer to [Datatypes In SQLite](https://www.sqlite.org/datatype3.html).
///
/// ## Example
///
/// ```swift
/// let integerValue: SQLiteRawValue = .int(42)
/// let realValue: SQLiteRawValue = .real(3.14)
/// let textValue: SQLiteRawValue = .text("Hello, SQLite")
/// let blobValue: SQLiteRawValue = .blob(Data([0x01, 0x02, 0x03]))
/// let nullValue: SQLiteRawValue = .null
/// ```
///
/// ## Topics
///
/// ### Enumeration Cases
///
/// - ``int(_:)``
/// - ``real(_:)``
/// - ``text(_:)``
/// - ``blob(_:)``
/// - ``null``
public enum SQLiteRawValue: Equatable {
/// Represents a 64-bit integer value.
case int(Int64)
/// Represents a floating-point number.
case real(Double)
/// Represents a text string.
case text(String)
/// Represents binary large objects (BLOBs).
case blob(Data)
/// Represents a SQL `NULL` value.
case null
}
extension SQLiteRawValue: SQLiteLiteralable {
/// Returns a string representation of the value suitable for use in SQL queries.
///
/// This method converts the `SQLiteRawValue` into a format that is directly usable in SQL statements:
/// - For `.int`: Converts the integer to its string representation.
/// - For `.real`: Converts the floating-point number to its string representation.
/// - For `.text`: Escapes single quotes within the string and wraps the result in single quotes.
/// - For `.blob`: Converts the binary data to a hexadecimal string representation, formatted as `X'...'`.
/// - For `.null`: Returns the SQL literal `"NULL"`.
///
/// The resulting string is formatted for inclusion in SQL queries, ensuring proper handling of the value
/// according to SQL syntax.
///
/// - Returns: A string representation of the value, formatted for use in SQL queries.
public var sqliteLiteral: String {
switch self {
case .int(let int): return "\(int)"
case .real(let real): return "\(real)"
case .text(let text): return "'\(text.replacingOccurrences(of: "'", with: "''"))'"
case .blob(let data): return "X'\(data.hex)'"
case .null: return "NULL"
}
}
}
extension SQLiteRawValue: CustomStringConvertible {
/// A textual representation of the `SQLiteRawValue`.
///
/// This property returns the string representation of the `SQLiteRawValue` as defined by the `sqliteLiteral` method.
/// It provides a clear and readable format of the value, useful for debugging and logging purposes.
///
/// - Returns: A string that represents the `SQLiteRawValue` in a format suitable for display.
public var description: String {
return sqliteLiteral
}
}
extension Data {
/// Converts the data to a hexadecimal string representation.
///
/// This method converts each byte of the `Data` instance into its two-digit hexadecimal representation.
/// The hexadecimal values are concatenated into a single string. This is useful for representing binary data
/// in a human-readable format, particularly for SQL BLOB literals.
///
/// ## Example
/// ```swift
/// let data = Data([0x01, 0x02, 0x03])
/// print(data.hex) // Output: "010203"
/// ```
///
/// - Returns: A hexadecimal string representation of the data.
var hex: String {
return map { String(format: "%02hhX", $0) }.joined()
}
}

View File

@@ -0,0 +1,45 @@
import Foundation
/// Represents different synchronous modes available for an SQLite database.
///
/// The synchronous mode determines how SQLite handles data synchronization with the database.
/// For more details, refer to [Synchronous Pragma](https://www.sqlite.org/pragma.html#pragma_synchronous).
public enum Synchronous: UInt8, SQLiteRawRepresentable {
/// Synchronous mode off. Disables synchronization for maximum performance.
///
/// With synchronous OFF, SQLite continues without syncing as soon as it has handed data off
/// to the operating system. If the application running SQLite crashes, the data will be safe,
/// but the database might become corrupted if the operating system crashes or the computer loses
/// power before the data is written to the disk surface. On the other hand, commits can be orders
/// of magnitude faster with synchronous OFF.
case off = 0
/// Normal synchronous mode.
///
/// The SQLite database engine syncs at the most critical moments, but less frequently
/// than in FULL mode. While there is a very small chance of corruption in
/// `journal_mode=DELETE` on older filesystems during a power failure, WAL
/// mode is safe from corruption with synchronous=NORMAL. Modern filesystems
/// likely make DELETE mode safe too. However, WAL mode in synchronous=NORMAL
/// loses some durability, as a transaction committed in WAL mode might roll back
/// after a power loss or system crash. Transactions are still durable across application
/// crashes regardless of the synchronous setting or journal mode. This setting is a
/// good choice for most applications running in WAL mode.
case normal = 1
/// Full synchronous mode.
///
/// Uses the xSync method of the VFS to ensure that all content is safely written
/// to the disk surface prior to continuing. This ensures that an operating system
/// crash or power failure will not corrupt the database. FULL synchronous is very
/// safe but also slower. It is the most commonly used synchronous setting when
/// not in WAL mode.
case full = 2
/// Extra synchronous mode.
///
/// Similar to FULL mode, but ensures the directory containing the rollback journal
/// is synced after the journal is unlinked, providing additional durability in case of
/// power loss shortly after a commit.
case extra = 3
}

View File

@@ -0,0 +1,40 @@
import Foundation
/// An enumeration representing different types of SQLite transactions.
///
/// SQLite transactions determine how the database engine handles concurrency and locking
/// during a transaction. The default transaction behavior is DEFERRED. For more detailed information
/// about SQLite transactions, refer to the [SQLite documentation](https://www.sqlite.org/lang_transaction.html).
public enum TransactionType: String, CustomStringConvertible {
/// A deferred transaction.
///
/// A deferred transaction does not start until the database is first accessed. Internally,
/// the `BEGIN DEFERRED` statement merely sets a flag on the database connection to prevent
/// the automatic commit that normally occurs when the last statement finishes. If the first
/// statement after `BEGIN DEFERRED` is a `SELECT`, a read transaction begins. If it is a write
/// statement, a write transaction starts. Subsequent write operations may upgrade the transaction
/// to a write transaction if possible, or return `SQLITE_BUSY`. The transaction persists until
/// an explicit `COMMIT` or `ROLLBACK` or until a rollback is provoked by an error or an `ON CONFLICT ROLLBACK` clause.
case deferred = "DEFERRED"
/// An immediate transaction.
///
/// An immediate transaction starts a new write immediately, without waiting for the first
/// write statement. The `BEGIN IMMEDIATE` statement may fail with `SQLITE_BUSY` if another
/// write transaction is active on a different database connection.
case immediate = "IMMEDIATE"
/// An exclusive transaction.
///
/// Similar to `IMMEDIATE`, an exclusive transaction starts a write immediately. However,
/// in non-WAL modes, `EXCLUSIVE` prevents other database connections from reading the database
/// while the transaction is in progress. In WAL mode, `EXCLUSIVE` behaves the same as `IMMEDIATE`.
case exclusive = "EXCLUSIVE"
/// A textual representation of the transaction type.
///
/// Returns the raw value of the transaction type (e.g., "DEFERRED", "IMMEDIATE", "EXCLUSIVE").
public var description: String {
rawValue
}
}

View File

@@ -0,0 +1,34 @@
import Foundation
public extension SQLiteRawBindable where Self: BinaryFloatingPoint {
/// Provides the `SQLiteRawValue` representation for floating-point types.
///
/// This implementation converts the floating-point value to a `real` SQLite raw value.
///
/// - Returns: An `SQLiteRawValue` of type `.real`, containing the floating-point value.
var sqliteRawValue: SQLiteRawValue {
.real(.init(self))
}
}
public extension SQLiteRawRepresentable where Self: BinaryFloatingPoint {
/// Initializes an instance of the conforming type from an `SQLiteRawValue`.
///
/// This initializer handles `SQLiteRawValue` of type `.real`, converting it to the floating-point value.
/// It also handles `SQLiteRawValue` of type `.int`, converting it to the floating-point value.
///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance.
init?(_ sqliteRawValue: SQLiteRawValue) {
switch sqliteRawValue {
case .int(let value):
self.init(Double(value))
case .real(let value):
self.init(value)
default:
return nil
}
}
}
extension Float: SQLiteRawRepresentable {}
extension Double: SQLiteRawRepresentable {}

View File

@@ -0,0 +1,43 @@
import Foundation
public extension SQLiteRawBindable where Self: BinaryInteger {
/// Provides the `SQLiteRawValue` representation for integer types.
///
/// This implementation converts the integer value to an `SQLiteRawValue` of type `.int`.
///
/// - Returns: An `SQLiteRawValue` of type `.int`, containing the integer value.
var sqliteRawValue: SQLiteRawValue {
.int(Int64(self))
}
}
public extension SQLiteRawRepresentable where Self: BinaryInteger {
/// Initializes an instance of the conforming type from an `SQLiteRawValue`.
///
/// This initializer handles `SQLiteRawValue` of type `.int`, converting it to the integer value.
/// It uses the `init(exactly:)` initializer to ensure that the value fits within the range of the
/// integer type. If the value cannot be exactly represented by the integer type, the initializer
/// will return `nil`.
///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance.
init?(_ sqliteRawValue: SQLiteRawValue) {
switch sqliteRawValue {
case .int(let value):
self.init(exactly: value)
default:
return nil
}
}
}
extension Int: SQLiteRawRepresentable {}
extension Int8: SQLiteRawRepresentable {}
extension Int16: SQLiteRawRepresentable {}
extension Int32: SQLiteRawRepresentable {}
extension Int64: SQLiteRawRepresentable {}
extension UInt: SQLiteRawRepresentable {}
extension UInt8: SQLiteRawRepresentable {}
extension UInt16: SQLiteRawRepresentable {}
extension UInt32: SQLiteRawRepresentable {}
extension UInt64: SQLiteRawRepresentable {}

View File

@@ -0,0 +1,32 @@
import Foundation
extension Bool: SQLiteRawRepresentable {
/// Provides the `SQLiteRawValue` representation for boolean types.
///
/// This implementation converts the boolean value to an `SQLiteRawValue` of type `.int`.
/// - `true` is represented as `1`.
/// - `false` is represented as `0`.
///
/// - Returns: An `SQLiteRawValue` of type `.int`, containing `1` for `true` and `0` for `false`.
public var sqliteRawValue: SQLiteRawValue {
.int(self ? 1 : 0)
}
/// Initializes an instance of the conforming type from an `SQLiteRawValue`.
///
/// This initializer handles `SQLiteRawValue` of type `.int`, converting it to a boolean value.
/// - `1` is converted to `true`.
/// - `0` is converted to `false`.
///
/// If the integer value is not `0` or `1`, the initializer returns `nil`.
///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance.
public init?(_ sqliteRawValue: SQLiteRawValue) {
switch sqliteRawValue {
case .int(let value) where value == 0 || value == 1:
self = value == 1
default:
return nil
}
}
}

View File

@@ -0,0 +1,26 @@
import Foundation
extension Data: SQLiteRawRepresentable {
/// Provides the `SQLiteRawValue` representation for `Data` types.
///
/// This implementation converts the `Data` value to an `SQLiteRawValue` of type `.blob`.
///
/// - Returns: An `SQLiteRawValue` of type `.blob`, containing the data.
public var sqliteRawValue: SQLiteRawValue {
.blob(self)
}
/// Initializes an instance of the conforming type from an `SQLiteRawValue`.
///
/// This initializer handles `SQLiteRawValue` of type `.blob`, converting it to `Data`.
///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance.
public init?(_ sqliteRawValue: SQLiteRawValue) {
switch sqliteRawValue {
case .blob(let data):
self = data
default:
return nil
}
}
}

View File

@@ -0,0 +1,39 @@
import Foundation
extension Date: SQLiteRawRepresentable {
/// Provides the `SQLiteRawValue` representation for `Date` types.
///
/// This implementation converts the `Date` value to an `SQLiteRawValue` of type `.text`.
/// The date is formatted as an ISO 8601 string.
///
/// - Returns: An `SQLiteRawValue` of type `.text`, containing the ISO 8601 string representation of the date.
public var sqliteRawValue: SQLiteRawValue {
let formatter = ISO8601DateFormatter()
let dateString = formatter.string(from: self)
return .text(dateString)
}
/// Initializes an instance of `Date` from an `SQLiteRawValue`.
///
/// This initializer handles `SQLiteRawValue` of type `.text`, converting it from an ISO 8601 string.
/// It also supports `.int` and `.real` types representing time intervals since 1970.
///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance.
public init?(_ sqliteRawValue: SQLiteRawValue) {
switch sqliteRawValue {
case .int(let value):
self.init(timeIntervalSince1970: TimeInterval(value))
case .real(let value):
self.init(timeIntervalSince1970: value)
case .text(let value):
let formatter = ISO8601DateFormatter()
if let date = formatter.date(from: value) {
self = date
} else {
return nil
}
default:
return nil
}
}
}

View File

@@ -0,0 +1,31 @@
import Foundation
public extension SQLiteRawBindable where Self: RawRepresentable, RawValue: SQLiteRawBindable {
/// Provides the `SQLiteRawValue` representation for `RawRepresentable` types.
///
/// This implementation converts the `RawRepresentable` type's `rawValue` to its corresponding
/// `SQLiteRawValue` representation. The `rawValue` itself must conform to `SQLiteRawBindable`.
///
/// - Returns: An `SQLiteRawValue` representation of the `RawRepresentable` type.
var sqliteRawValue: SQLiteRawValue {
rawValue.sqliteRawValue
}
}
public extension SQLiteRawRepresentable where Self: RawRepresentable, RawValue: SQLiteRawRepresentable {
/// Initializes an instance of the conforming type from an `SQLiteRawValue`.
///
/// This initializer converts the `SQLiteRawValue` to the `RawRepresentable` type's `rawValue`.
/// It first attempts to create a `RawValue` from the `SQLiteRawValue`, then uses that to initialize
/// the `RawRepresentable` instance. If the `SQLiteRawValue` cannot be converted to the `RawValue`, the
/// initializer returns `nil`.
///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance.
init?(_ sqliteRawValue: SQLiteRawValue) {
if let value = RawValue(sqliteRawValue) {
self.init(rawValue: value)
} else {
return nil
}
}
}

View File

@@ -0,0 +1,731 @@
import Foundation
extension String {
/// Removes SQL comments from the string while preserving string literals.
///
/// This function preserves escaped single quotes inside string literals
/// and removes both single-line (`-- ...`) and multi-line (`/* ... */`) comments.
///
/// The implementation of this function is generated using
/// [`re2swift`](https://re2c.org/manual/manual_swift.html)
/// from the following parsing template:
///
/// ```swift
/// @inline(__always)
/// func removingComments() -> String {
/// withCString {
/// let yyinput = $0
/// let yylimit = strlen($0)
/// var yyoutput = [CChar]()
/// var yycursor = 0
/// loop: while yycursor < yylimit {
/// var llmarker = yycursor/*!re2c
/// re2c:define:YYCTYPE = "CChar";
/// re2c:yyfill:enable = 0;
///
/// "'" ([^'] | "''")* "'" {
/// while llmarker < yycursor {
/// yyoutput.append(yyinput[llmarker])
/// llmarker += 1
/// }
/// continue loop }
///
/// "--" [^\r\n\x00]* {
/// continue loop }
///
/// "/*" ([^*] | "*"[^/])* "*/" {
/// continue loop }
///
/// [^] {
/// yyoutput.append(yyinput[llmarker])
/// continue loop }
/// */}
/// yyoutput.append(0)
/// return String(
/// cString: yyoutput,
/// encoding: .utf8
/// ) ?? ""
/// }
/// }
/// ```
@inline(__always)
func removingComments() -> String {
withCString {
let yyinput = $0
let yylimit = strlen($0)
var yyoutput = [CChar]()
var yycursor = 0
loop: while yycursor < yylimit {
var llmarker = yycursor
var yych: CChar = 0
var yystate: UInt = 0
yyl: while true {
switch yystate {
case 0:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 3
continue yyl
case 0x2D:
yystate = 4
continue yyl
case 0x2F:
yystate = 5
continue yyl
default:
yystate = 1
continue yyl
}
case 1:
yystate = 2
continue yyl
case 2:
yyoutput.append(yyinput[llmarker])
continue loop
case 3:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 6
continue yyl
default:
yystate = 3
continue yyl
}
case 4:
yych = yyinput[yycursor]
switch yych {
case 0x2D:
yycursor += 1
yystate = 8
continue yyl
default:
yystate = 2
continue yyl
}
case 5:
yych = yyinput[yycursor]
switch yych {
case 0x2A:
yycursor += 1
yystate = 10
continue yyl
default:
yystate = 2
continue yyl
}
case 6:
yych = yyinput[yycursor]
switch yych {
case 0x27:
yycursor += 1
yystate = 3
continue yyl
default:
yystate = 7
continue yyl
}
case 7:
while llmarker < yycursor {
yyoutput.append(yyinput[llmarker])
llmarker += 1
}
continue loop
case 8:
yych = yyinput[yycursor]
switch yych {
case 0x00:
fallthrough
case 0x0A:
fallthrough
case 0x0D:
yystate = 9
continue yyl
default:
yycursor += 1
yystate = 8
continue yyl
}
case 9:
continue loop
case 10:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x2A:
yystate = 11
continue yyl
default:
yystate = 10
continue yyl
}
case 11:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x2F:
yystate = 12
continue yyl
default:
yystate = 10
continue yyl
}
case 12:
continue loop
default: fatalError("internal lexer error")
}
}
}
yyoutput.append(0)
return String(cString: yyoutput, encoding: .utf8) ?? ""
}
}
/// Trims empty lines and trailing whitespace outside string literals.
///
/// This function preserves line breaks and whitespace inside string literals,
/// removing only redundant empty lines and trailing whitespace outside literals.
///
/// The implementation of this function is generated using
/// [`re2swift`](https://re2c.org/manual/manual_swift.html)
/// from the following parsing template:
///
/// ```swift
/// @inline(__always)
/// func trimmingLines() -> String {
/// withCString {
/// let yyinput = $0
/// let yylimit = strlen($0)
/// var yyoutput = [CChar]()
/// var yycursor = 0
/// var yymarker = 0
/// loop: while yycursor < yylimit {
/// var llmarker = yycursor/*!re2c
/// re2c:define:YYCTYPE = "CChar";
/// re2c:yyfill:enable = 0;
///
/// "'" ([^'] | "''")* "'" {
/// while llmarker < yycursor {
/// yyoutput.append(yyinput[llmarker])
/// llmarker += 1
/// }
/// continue loop }
///
/// [ \t]* "\x00" {
/// continue loop }
///
/// [ \t]* "\n\x00" {
/// continue loop }
///
/// [ \t\n]* "\n"+ {
/// if llmarker > 0 && yycursor < yylimit {
/// yyoutput.append(0x0A)
/// }
/// continue loop }
///
/// [^] {
/// yyoutput.append(yyinput[llmarker])
/// continue loop }
/// */}
/// yyoutput.append(0)
/// return String(
/// cString: yyoutput,
/// encoding: .utf8
/// ) ?? ""
/// }
/// }
/// ```
@inline(__always)
func trimmingLines() -> String {
withCString {
let yyinput = $0
let yylimit = strlen($0)
var yyoutput = [CChar]()
var yycursor = 0
var yymarker = 0
loop: while yycursor < yylimit {
var llmarker = yycursor
var yych: CChar = 0
var yyaccept: UInt = 0
var yystate: UInt = 0
yyl: while true {
switch yystate {
case 0:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x00:
yystate = 1
continue yyl
case 0x09:
fallthrough
case 0x20:
yystate = 4
continue yyl
case 0x0A:
yystate = 5
continue yyl
case 0x27:
yystate = 7
continue yyl
default:
yystate = 2
continue yyl
}
case 1:
continue loop
case 2:
yystate = 3
continue yyl
case 3:
yyoutput.append(yyinput[llmarker])
continue loop
case 4:
yyaccept = 0
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x00:
fallthrough
case 0x09...0x0A:
fallthrough
case 0x20:
yystate = 9
continue yyl
default:
yystate = 3
continue yyl
}
case 5:
yyaccept = 1
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x00:
yycursor += 1
yystate = 11
continue yyl
case 0x09...0x0A:
fallthrough
case 0x20:
yystate = 13
continue yyl
default:
yystate = 6
continue yyl
}
case 6:
if llmarker > 0 && yycursor < yylimit {
yyoutput.append(0x0A)
}
continue loop
case 7:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 15
continue yyl
default:
yystate = 7
continue yyl
}
case 8:
yych = yyinput[yycursor]
yystate = 9
continue yyl
case 9:
switch yych {
case 0x00:
yycursor += 1
yystate = 1
continue yyl
case 0x09:
fallthrough
case 0x20:
yycursor += 1
yystate = 8
continue yyl
case 0x0A:
yycursor += 1
yystate = 5
continue yyl
default:
yystate = 10
continue yyl
}
case 10:
yycursor = yymarker
if yyaccept == 0 {
yystate = 3
continue yyl
} else {
yystate = 6
continue yyl
}
case 11:
continue loop
case 12:
yych = yyinput[yycursor]
yystate = 13
continue yyl
case 13:
switch yych {
case 0x09:
fallthrough
case 0x20:
yycursor += 1
yystate = 12
continue yyl
case 0x0A:
yycursor += 1
yystate = 14
continue yyl
default:
yystate = 10
continue yyl
}
case 14:
yyaccept = 1
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x09:
fallthrough
case 0x20:
yycursor += 1
yystate = 12
continue yyl
case 0x0A:
yycursor += 1
yystate = 14
continue yyl
default:
yystate = 6
continue yyl
}
case 15:
yych = yyinput[yycursor]
switch yych {
case 0x27:
yycursor += 1
yystate = 7
continue yyl
default:
yystate = 16
continue yyl
}
case 16:
while llmarker < yycursor {
yyoutput.append(yyinput[llmarker])
llmarker += 1
}
continue loop
default: fatalError("internal lexer error")
}
}
}
yyoutput.append(0)
return String(
cString: yyoutput,
encoding: .utf8
) ?? ""
}
}
/// Splits the SQL script into individual statements by semicolons.
///
/// This function preserves string literals (enclosed in single quotes),
/// and treats `BEGIN...END` blocks as single nested statements, preventing
/// splitting inside these blocks. Statements are split only at semicolons
/// outside string literals and `BEGIN...END` blocks.
///
/// The implementation of this function is generated using
/// [`re2swift`](https://re2c.org/manual/manual_swift.html)
/// from the following parsing template:
///
/// ```swift
/// @inline(__always)
/// func splitStatements() -> [String] {
/// withCString {
/// let yyinput = $0
/// let yylimit = strlen($0)
/// var yyranges = [Range<Int>]()
/// var yycursor = 0
/// var yymarker = 0
/// var yynesting = 0
/// var yystart = 0
/// var yyend = 0
/// loop: while yycursor < yylimit {/*!re2c
/// re2c:define:YYCTYPE = "CChar";
/// re2c:yyfill:enable = 0;
///
/// "'" ( [^'] | "''" )* "'" {
/// yyend = yycursor
/// continue loop }
///
/// 'BEGIN' {
/// yynesting += 1
/// yyend = yycursor
/// continue loop }
///
/// 'END' {
/// if yynesting > 0 {
/// yynesting -= 1
/// }
/// yyend = yycursor
/// continue loop }
///
/// ";" [ \t]* "\n"* {
/// if yynesting == 0 {
/// if yystart < yyend {
/// yyranges.append(yystart..<yyend)
/// }
/// yystart = yycursor
/// continue loop
/// } else {
/// continue loop
/// }}
///
/// [^] {
/// yyend = yycursor
/// continue loop }
/// */}
/// if yystart < yyend {
/// yyranges.append(yystart..<yyend)
/// }
/// return yyranges.map { range in
/// let buffer = UnsafeBufferPointer<CChar>(
/// start: yyinput.advanced(by: range.lowerBound),
/// count: range.count
/// )
/// let array = Array(buffer) + [0]
/// return String(cString: array, encoding: .utf8) ?? ""
/// }
/// }
/// }
/// ```
@inline(__always)
func splitStatements() -> [String] {
withCString {
let yyinput = $0
let yylimit = strlen($0)
var yyranges = [Range<Int>]()
var yycursor = 0
var yymarker = 0
var yynesting = 0
var yystart = 0
var yyend = 0
loop: while yycursor < yylimit {
var yych: CChar = 0
var yystate: UInt = 0
yyl: while true {
switch yystate {
case 0:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 3
continue yyl
case 0x3B:
yystate = 4
continue yyl
case 0x42:
fallthrough
case 0x62:
yystate = 6
continue yyl
case 0x45:
fallthrough
case 0x65:
yystate = 7
continue yyl
default:
yystate = 1
continue yyl
}
case 1:
yystate = 2
continue yyl
case 2:
yyend = yycursor
continue loop
case 3:
yych = yyinput[yycursor]
yycursor += 1
switch yych {
case 0x27:
yystate = 8
continue yyl
default:
yystate = 3
continue yyl
}
case 4:
yych = yyinput[yycursor]
switch yych {
case 0x09:
fallthrough
case 0x20:
yycursor += 1
yystate = 4
continue yyl
case 0x0A:
yycursor += 1
yystate = 10
continue yyl
default:
yystate = 5
continue yyl
}
case 5:
if yynesting == 0 {
if yystart < yyend {
yyranges.append(yystart..<yyend)
}
yystart = yycursor
continue loop
} else {
continue loop
}
case 6:
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x45:
fallthrough
case 0x65:
yycursor += 1
yystate = 11
continue yyl
default:
yystate = 2
continue yyl
}
case 7:
yymarker = yycursor
yych = yyinput[yycursor]
switch yych {
case 0x4E:
fallthrough
case 0x6E:
yycursor += 1
yystate = 13
continue yyl
default:
yystate = 2
continue yyl
}
case 8:
yych = yyinput[yycursor]
switch yych {
case 0x27:
yycursor += 1
yystate = 3
continue yyl
default:
yystate = 9
continue yyl
}
case 9:
yyend = yycursor
continue loop
case 10:
yych = yyinput[yycursor]
switch yych {
case 0x0A:
yycursor += 1
yystate = 10
continue yyl
default:
yystate = 5
continue yyl
}
case 11:
yych = yyinput[yycursor]
switch yych {
case 0x47:
fallthrough
case 0x67:
yycursor += 1
yystate = 14
continue yyl
default:
yystate = 12
continue yyl
}
case 12:
yycursor = yymarker
yystate = 2
continue yyl
case 13:
yych = yyinput[yycursor]
switch yych {
case 0x44:
fallthrough
case 0x64:
yycursor += 1
yystate = 15
continue yyl
default:
yystate = 12
continue yyl
}
case 14:
yych = yyinput[yycursor]
switch yych {
case 0x49:
fallthrough
case 0x69:
yycursor += 1
yystate = 16
continue yyl
default:
yystate = 12
continue yyl
}
case 15:
if yynesting > 0 {
yynesting -= 1
}
yyend = yycursor
continue loop
case 16:
yych = yyinput[yycursor]
switch yych {
case 0x4E:
fallthrough
case 0x6E:
yycursor += 1
yystate = 17
continue yyl
default:
yystate = 12
continue yyl
}
case 17:
yynesting += 1
yyend = yycursor
continue loop
default: fatalError("internal lexer error")
}
}
}
if yystart < yyend {
yyranges.append(yystart..<yyend)
}
return yyranges.map { range in
let buffer = UnsafeBufferPointer<CChar>(
start: yyinput.advanced(by: range.lowerBound),
count: range.count
)
let array = Array(buffer) + [0]
return String(cString: array, encoding: .utf8) ?? ""
}
}
}
}

View File

@@ -0,0 +1,26 @@
import Foundation
extension String: SQLiteRawRepresentable {
/// Provides the `SQLiteRawValue` representation for `String` type.
///
/// This implementation converts the `String` value to an `SQLiteRawValue` of type `.text`.
///
/// - Returns: An `SQLiteRawValue` of type `.text`, containing the string value.
public var sqliteRawValue: SQLiteRawValue {
.text(self)
}
/// Initializes an instance of `String` from an `SQLiteRawValue`.
///
/// This initializer handles `SQLiteRawValue` of type `.text`, converting it to a `String` value.
///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance.
public init?(_ sqliteRawValue: SQLiteRawValue) {
switch sqliteRawValue {
case .text(let value):
self = value
default:
return nil
}
}
}

View File

@@ -0,0 +1,26 @@
import Foundation
extension UUID: SQLiteRawRepresentable {
/// Provides the `SQLiteRawValue` representation for `UUID`.
///
/// This implementation converts the `UUID` value to an `SQLiteRawValue` of type `.text`.
///
/// - Returns: An `SQLiteRawValue` of type `.text`, containing the UUID string.
public var sqliteRawValue: SQLiteRawValue {
.text(self.uuidString)
}
/// Initializes an instance of `UUID` from an `SQLiteRawValue`.
///
/// This initializer handles `SQLiteRawValue` of type `.text`, converting it to a `UUID`.
///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance.
public init?(_ sqliteRawValue: SQLiteRawValue) {
switch sqliteRawValue {
case .text(let value):
self.init(uuidString: value)
default:
return nil
}
}
}

View File

@@ -0,0 +1,170 @@
import Foundation
/// A protocol defining methods that can be implemented by delegates of a `Connection` object.
///
/// The `ConnectionDelegate` protocol allows a delegate to receive notifications about various
/// events that occur within a ``Connection``, including SQL statement tracing, database update
/// actions, and transaction commits or rollbacks. Implementing this protocol provides a way
/// to monitor and respond to database interactions in a structured manner.
///
/// ### Default Implementations
///
/// The protocol provides default implementations for all methods, which do nothing. This allows
/// conforming types to only implement the methods they are interested in without the need to
/// provide an implementation for each method.
///
/// ## Topics
///
/// ### Instance Methods
///
/// - ``ConnectionDelegate/connection(_:trace:)``
/// - ``ConnectionDelegate/connection(_:didUpdate:)``
/// - ``ConnectionDelegate/connectionDidCommit(_:)``
/// - ``ConnectionDelegate/connectionDidRollback(_:)``
public protocol ConnectionDelegate: AnyObject {
/// Informs the delegate that a SQL statement is being traced.
///
/// This method is called right before a SQL statement is executed, allowing the delegate
/// to monitor the queries being sent to SQLite. This can be particularly useful for debugging
/// purposes or for performance analysis, as it provides insights into the exact SQL being
/// executed against the database.
///
/// - Parameters:
/// - connection: The ``Connection`` instance that is executing the SQL statement.
/// - sql: A tuple containing the unexpanded and expanded forms of the SQL statement being traced.
/// - `unexpandedSQL`: The original SQL statement as it was written by the developer.
/// - `expandedSQL`: The SQL statement with all parameters substituted in, which shows
/// exactly what is being sent to SQLite.
///
/// ### Example
///
/// You can implement this method to log or analyze SQL statements:
///
/// ```swift
/// func connection(
/// _ connection: Connection,
/// trace sql: (unexpandedSQL: String, expandedSQL: String)
/// ) {
/// print("Tracing SQL: \(sql.unexpandedSQL)")
/// }
/// ```
///
/// - Important: If the implementation of this method performs any heavy operations, it could
/// potentially slow down the execution of the SQL statement. It is recommended to keep the
/// implementation lightweight to avoid impacting performance.
func connection(_ connection: Connection, trace sql: (unexpandedSQL: String, expandedSQL: String))
/// Informs the delegate that an update action has occurred.
///
/// This method is called whenever an update action, such as insertion, modification,
/// or deletion, is performed on the database. It provides details about the action taken,
/// allowing the delegate to respond appropriately to changes in the database.
///
/// - Parameters:
/// - connection: The `Connection` instance where the update action occurred.
/// - action: The type of update action that occurred, represented by the ``SQLiteAction`` enum.
///
/// ### Example
///
/// You can implement this method to respond to specific update actions:
///
/// ```swift
/// func connection(_ connection: Connection, didUpdate action: SQLiteAction) {
/// switch action {
/// case .insert(let db, let table, let rowID):
/// print("Inserted row \(rowID) into \(table) in database \(db).")
/// case .update(let db, let table, let rowID):
/// print("Updated row \(rowID) in \(table) in database \(db).")
/// case .delete(let db, let table, let rowID):
/// print("Deleted row \(rowID) from \(table) in database \(db).")
/// }
/// }
/// ```
///
/// - Note: Implementing this method can help you maintain consistency and perform any
/// necessary actions (such as UI updates or logging) in response to database changes.
func connection(_ connection: Connection, didUpdate action: SQLiteAction)
/// Informs the delegate that a transaction has been successfully committed.
///
/// This method is called when a transaction has been successfully committed. It provides an
/// opportunity for the delegate to perform any necessary actions after the commit. If this
/// method throws an error, the COMMIT operation will be converted into a ROLLBACK, ensuring
/// data integrity in the database.
///
/// - Parameter connection: The `Connection` instance where the transaction was committed.
///
/// - Throws: May throw an error to abort the commit process, which will cause the transaction
/// to be rolled back.
///
/// ### Example
/// You can implement this method to perform actions after a successful commit:
///
/// ```swift
/// func connectionDidCommit(_ connection: Connection) throws {
/// print("Transaction committed successfully.")
/// }
/// ```
///
/// - Important: Be cautious when implementing this method. If it performs heavy operations,
/// it could delay the commit process. It is advisable to keep the implementation lightweight
/// to maintain optimal performance and responsiveness.
func connectionDidCommit(_ connection: Connection) throws
/// Informs the delegate that a transaction has been rolled back.
///
/// This method is called when a transaction is rolled back, allowing the delegate to handle
/// any necessary cleanup or logging related to the rollback. This can be useful for maintaining
/// consistency in the application state or for debugging purposes.
///
/// - Parameter connection: The `Connection` instance where the rollback occurred.
///
/// ### Example
/// You can implement this method to respond to rollback events:
///
/// ```swift
/// func connectionDidRollback(_ connection: Connection) {
/// print("Transaction has been rolled back.")
/// }
/// ```
///
/// - Note: It's a good practice to keep any logic within this method lightweight, as it may
/// be called frequently during database operations, especially in scenarios involving errors
/// that trigger rollbacks.
func connectionDidRollback(_ connection: Connection)
}
public extension ConnectionDelegate {
/// Default implementation of the `connection(_:trace:)` method.
///
/// This default implementation does nothing.
///
/// - Parameters:
/// - connection: The `Connection` instance that is executing the SQL statement.
/// - sql: A tuple containing the unexpanded and expanded forms of the SQL statement being traced.
func connection(_ connection: Connection, trace sql: (unexpandedSQL: String, expandedSQL: String)) {}
/// Default implementation of the `connection(_:didUpdate:)` method.
///
/// This default implementation does nothing.
///
/// - Parameters:
/// - connection: The `Connection` instance where the update action occurred.
/// - action: The type of update action that occurred.
func connection(_ connection: Connection, didUpdate action: SQLiteAction) {}
/// Default implementation of the `connectionDidCommit(_:)` method.
///
/// This default implementation does nothing.
///
/// - Parameter connection: The `Connection` instance where the transaction was committed.
/// - Throws: May throw an error to abort the commit process.
func connectionDidCommit(_ connection: Connection) throws {}
/// Default implementation of the `connectionDidRollback(_:)` method.
///
/// This default implementation does nothing.
///
/// - Parameter connection: The `Connection` instance where the rollback occurred.
func connectionDidRollback(_ connection: Connection) {}
}

View File

@@ -0,0 +1,390 @@
import Foundation
import DataLiteC
/// A protocol that defines the interface for a database connection.
///
/// This protocol specifies the requirements for managing a connection
/// to an SQLite database, including connection state, configuration via PRAGMA,
/// executing SQL statements and scripts, transaction control, and encryption support.
///
/// It also includes support for delegation to handle connection-related events.
///
/// ## See Also
///
/// - ``Connection``
///
/// ## Topics
///
/// ### Delegation
///
/// - ``ConnectionDelegate``
/// - ``delegate``
///
/// ### Connection State
///
/// - ``isAutocommit``
/// - ``isReadonly``
/// - ``busyTimeout``
///
/// ### PRAGMA Accessors
///
/// - ``applicationID``
/// - ``foreignKeys``
/// - ``journalMode``
/// - ``synchronous``
/// - ``userVersion``
///
/// ### SQLite Lifecycle
///
/// - ``initialize()``
/// - ``shutdown()``
///
/// ### Custom SQL Functions
///
/// - ``add(function:)``
/// - ``remove(function:)``
///
/// ### Statement Preparation
///
/// - ``prepare(sql:options:)``
///
/// ### Script Execution
///
/// - ``execute(sql:)``
/// - ``execute(raw:)``
///
/// ### PRAGMA Execution
///
/// - ``get(pragma:)``
/// - ``set(pragma:value:)``
///
/// ### Transactions
///
/// - ``beginTransaction(_:)``
/// - ``commitTransaction()``
/// - ``rollbackTransaction()``
///
/// ### Encryption Keys
///
/// - ``Connection/Key``
/// - ``apply(_:name:)``
/// - ``rekey(_:name:)``
public protocol ConnectionProtocol: AnyObject {
// MARK: - Delegation
/// An optional delegate to receive connection-related events and callbacks.
///
/// The delegate allows external objects to monitor or respond to events
/// occurring during the lifetime of the connection, such as errors,
/// transaction commits, or other significant state changes.
var delegate: ConnectionDelegate? { get set }
// MARK: - Connection State
/// Indicates whether the database connection is in autocommit mode.
///
/// Autocommit mode is enabled by default. It remains enabled as long as no
/// explicit transactions are active. Executing `BEGIN` disables autocommit mode,
/// and executing `COMMIT` or `ROLLBACK` re-enables it.
///
/// - Returns: `true` if the connection is in autocommit mode; otherwise, `false`.
/// - SeeAlso: [sqlite3_get_autocommit()](https://sqlite.org/c3ref/get_autocommit.html)
var isAutocommit: Bool { get }
/// Indicates whether the database connection is read-only.
///
/// This property reflects the access mode of the main database for the connection.
/// It returns `true` if the database was opened with read-only access,
/// and `false` if it allows read-write access.
///
/// - Returns: `true` if the main database is read-only; otherwise, `false`.
/// - SeeAlso: [sqlite3_db_readonly()](https://www.sqlite.org/c3ref/db_readonly.html)
var isReadonly: Bool { get }
/// The busy timeout duration in milliseconds for the database connection.
///
/// This value determines how long SQLite will wait for a locked database to become available
/// before returning a `SQLITE_BUSY` error. A value of zero disables the timeout and causes
/// operations to fail immediately if the database is locked.
///
/// - SeeAlso: [sqlite3_busy_timeout()](https://www.sqlite.org/c3ref/busy_timeout.html)
var busyTimeout: Int32 { get set }
// MARK: - PRAGMA Accessors
/// The application ID stored in the database header.
///
/// This 32-bit integer is used to identify the application that created or manages the database.
/// It is stored at a fixed offset within the database file header and can be read or modified
/// using the `application_id` pragma.
///
/// - SeeAlso: [PRAGMA application_id](https://www.sqlite.org/pragma.html#pragma_application_id)
var applicationID: Int32 { get set }
/// Indicates whether foreign key constraints are enforced.
///
/// This property enables or disables enforcement of foreign key constraints
/// by the database connection. When set to `true`, constraints are enforced;
/// when `false`, they are ignored.
///
/// - SeeAlso: [PRAGMA foreign_keys](https://www.sqlite.org/pragma.html#pragma_foreign_keys)
var foreignKeys: Bool { get set }
/// The journal mode used by the database connection.
///
/// The journal mode determines how SQLite manages rollback journals,
/// impacting durability, concurrency, and performance.
///
/// Setting this property updates the journal mode using the corresponding SQLite PRAGMA.
///
/// - SeeAlso: [PRAGMA journal_mode](https://www.sqlite.org/pragma.html#pragma_journal_mode)
var journalMode: JournalMode { get set }
/// The synchronous mode used by the database connection.
///
/// This property controls how rigorously SQLite waits for data to be
/// physically written to disk, influencing durability and performance.
///
/// Setting this property updates the synchronous mode using the
/// corresponding SQLite PRAGMA.
///
/// - SeeAlso: [PRAGMA synchronous](https://www.sqlite.org/pragma.html#pragma_synchronous)
var synchronous: Synchronous { get set }
/// The user version number stored in the database.
///
/// This 32-bit integer is stored as the `user_version` pragma and
/// is typically used by applications to track the schema version
/// or migration state of the database.
///
/// Setting this property updates the corresponding SQLite PRAGMA.
///
/// - SeeAlso: [PRAGMA user_version](https://www.sqlite.org/pragma.html#pragma_user_version)
var userVersion: Int32 { get set }
// MARK: - SQLite Lifecycle
/// Initializes the SQLite library.
///
/// This method sets up the global state required by SQLite. It must be called before using
/// any other SQLite interface, unless SQLite is initialized automatically.
///
/// A successful call has an effect only the first time it is invoked during the lifetime of
/// the process, or the first time after a call to ``shutdown()``. All other calls are no-ops.
///
/// - Throws: ``Connection/Error`` if the initialization fails.
/// - SeeAlso: [sqlite3_initialize()](https://www.sqlite.org/c3ref/initialize.html)
static func initialize() throws(Connection.Error)
/// Shuts down the SQLite library.
///
/// This method releases global resources used by SQLite and reverses the effects of a successful
/// call to ``initialize()``. It must be called exactly once for each successful call to
/// ``initialize()``, and only after all database connections are closed.
///
/// - Throws: ``Connection/Error`` if the shutdown process fails.
/// - SeeAlso: [sqlite3_shutdown()](https://www.sqlite.org/c3ref/initialize.html)
static func shutdown() throws(Connection.Error)
// MARK: - Custom SQL Functions
/// Registers a custom SQL function with the connection.
///
/// This allows adding user-defined functions callable from SQL queries.
///
/// - Parameter function: The type of the custom SQL function to add.
/// - Throws: ``Connection/Error`` if the function registration fails.
func add(function: Function.Type) throws(Connection.Error)
/// Removes a previously registered custom SQL function from the connection.
///
/// - Parameter function: The type of the custom SQL function to remove.
/// - Throws: ``Connection/Error`` if the function removal fails.
func remove(function: Function.Type) throws(Connection.Error)
// MARK: - Statement Preparation
/// Prepares an SQL statement for execution.
///
/// Compiles the provided SQL query into a ``Statement`` object that can be executed or stepped through.
///
/// - Parameters:
/// - query: The SQL query string to prepare.
/// - options: Options that affect statement preparation.
/// - Returns: A prepared ``Statement`` ready for execution.
/// - Throws: ``Connection/Error`` if statement preparation fails.
/// - SeeAlso: [sqlite3_prepare_v3()](https://www.sqlite.org/c3ref/prepare.html)
func prepare(sql query: String, options: Statement.Options) throws(Connection.Error) -> Statement
// MARK: - Script Execution
/// Executes a sequence of SQL statements.
///
/// Processes the given SQL script by executing each individual statement in order.
///
/// - Parameter script: A collection of SQL statements to execute.
/// - Throws: ``Connection/Error`` if any statement execution fails.
func execute(sql script: SQLScript) throws(Connection.Error)
/// Executes a raw SQL string.
///
/// Executes the provided raw SQL string as a single operation.
///
/// - Parameter sql: The raw SQL string to execute.
/// - Throws: ``Connection/Error`` if the execution fails.
func execute(raw sql: String) throws(Connection.Error)
// MARK: - PRAGMA Execution
/// Retrieves the value of a PRAGMA setting from the database.
///
/// - Parameter pragma: The PRAGMA setting to retrieve.
/// - Returns: The current value of the PRAGMA, or `nil` if the value is not available.
/// - Throws: ``Connection/Error`` if the operation fails.
func get<T: SQLiteRawRepresentable>(pragma: Pragma) throws(Connection.Error) -> T?
/// Sets the value of a PRAGMA setting in the database.
///
/// - Parameters:
/// - pragma: The PRAGMA setting to modify.
/// - value: The new value to assign to the PRAGMA.
/// - Returns: The resulting value after the assignment, or `nil` if unavailable.
/// - Throws: ``Connection/Error`` if the operation fails.
@discardableResult
func set<T: SQLiteRawRepresentable>(pragma: Pragma, value: T) throws(Connection.Error) -> T?
// MARK: - Transactions
/// Begins a database transaction of the specified type.
///
/// - Parameter type: The type of transaction to begin (e.g., deferred, immediate, exclusive).
/// - Throws: ``Connection/Error`` if starting the transaction fails.
/// - SeeAlso: [BEGIN TRANSACTION](https://www.sqlite.org/lang_transaction.html)
func beginTransaction(_ type: TransactionType) throws(Connection.Error)
/// Commits the current database transaction.
///
/// - Throws: ``Connection/Error`` if committing the transaction fails.
/// - SeeAlso: [COMMIT](https://www.sqlite.org/lang_transaction.html)
func commitTransaction() throws(Connection.Error)
/// Rolls back the current database transaction.
///
/// - Throws: ``Connection/Error`` if rolling back the transaction fails.
/// - SeeAlso: [ROLLBACK](https://www.sqlite.org/lang_transaction.html)
func rollbackTransaction() throws(Connection.Error)
// MARK: - Encryption Keys
/// Applies an encryption key to the database connection.
///
/// - Parameters:
/// - key: The encryption key to apply.
/// - name: An optional name identifying the database to apply the key to.
/// - Throws: ``Connection/Error`` if applying the key fails.
func apply(_ key: Connection.Key, name: String?) throws(Connection.Error)
/// Changes the encryption key for the database connection.
///
/// - Parameters:
/// - key: The new encryption key to set.
/// - name: An optional name identifying the database to rekey.
/// - Throws: ``Connection/Error`` if rekeying fails.
func rekey(_ key: Connection.Key, name: String?) throws(Connection.Error)
}
// MARK: - PRAGMA Accessors
public extension ConnectionProtocol {
var applicationID: Int32 {
get { try! get(pragma: .applicationID) ?? 0 }
set { try! set(pragma: .applicationID, value: newValue) }
}
var foreignKeys: Bool {
get { try! get(pragma: .foreignKeys) ?? false }
set { try! set(pragma: .foreignKeys, value: newValue) }
}
var journalMode: JournalMode {
get { try! get(pragma: .journalMode) ?? .off }
set { try! set(pragma: .journalMode, value: newValue) }
}
var synchronous: Synchronous {
get { try! get(pragma: .synchronous) ?? .off }
set { try! set(pragma: .synchronous, value: newValue) }
}
var userVersion: Int32 {
get { try! get(pragma: .userVersion) ?? 0 }
set { try! set(pragma: .userVersion, value: newValue) }
}
}
// MARK: - SQLite Lifecycle
public extension ConnectionProtocol {
static func initialize() throws(Connection.Error) {
let status = sqlite3_initialize()
if status != SQLITE_OK {
throw Connection.Error(code: status, message: "")
}
}
static func shutdown() throws(Connection.Error) {
let status = sqlite3_shutdown()
if status != SQLITE_OK {
throw Connection.Error(code: status, message: "")
}
}
}
// MARK: - Script Execution
public extension ConnectionProtocol {
func execute(sql script: SQLScript) throws(Connection.Error) {
for query in script {
let stmt = try prepare(sql: query, options: [])
while try stmt.step() {}
}
}
}
// MARK: - PRAGMA Execution
public extension ConnectionProtocol {
func get<T: SQLiteRawRepresentable>(pragma: Pragma) throws(Connection.Error) -> T? {
let stmt = try prepare(sql: "PRAGMA \(pragma)", options: [])
switch try stmt.step() {
case true: return stmt.columnValue(at: 0)
case false: return nil
}
}
@discardableResult
func set<T: SQLiteRawRepresentable>(pragma: Pragma, value: T) throws(Connection.Error) -> T? {
let query = "PRAGMA \(pragma) = \(value.sqliteLiteral)"
let stmt = try prepare(sql: query, options: [])
switch try stmt.step() {
case true: return stmt.columnValue(at: 0)
case false: return nil
}
}
}
// MARK: - Transactions
public extension ConnectionProtocol {
func beginTransaction(_ type: TransactionType = .deferred) throws(Connection.Error) {
try prepare(sql: "BEGIN \(type) TRANSACTION", options: []).step()
}
func commitTransaction() throws(Connection.Error) {
try prepare(sql: "COMMIT TRANSACTION", options: []).step()
}
func rollbackTransaction() throws(Connection.Error) {
try prepare(sql: "ROLLBACK TRANSACTION", options: []).step()
}
}

View File

@@ -0,0 +1,33 @@
import Foundation
/// A type that can be represented as literals in an SQL query.
///
/// This protocol ensures that types conforming to it provide a string representation
/// that can be used directly in SQL queries. Each conforming type must implement
/// a way to return its corresponding SQLite literal representation.
///
/// **Example implementation:**
///
/// ```swift
/// struct Device: SQLiteLiteralable {
/// var model: String
///
/// var sqliteLiteral: String {
/// return "'\(model)'"
/// }
/// }
/// ```
public protocol SQLiteLiteralable {
/// Returns the string representation of the object, formatted as an SQLite literal.
///
/// This property should return a string that adheres to SQL query syntax and is compatible
/// with SQLite's rules for literals.
///
/// For example:
/// - **Integers:** `42` -> `"42"`
/// - **Strings:** `"Hello"` -> `"'Hello'"` (with single quotes)
/// - **Booleans:** `true` -> `"1"`, `false` -> `"0"`
/// - **Data:** `Data([0x01, 0x02])` -> `"X'0102'"`
/// - **Null:** `NSNull()` -> `"NULL"`
var sqliteLiteral: String { get }
}

View File

@@ -0,0 +1,36 @@
import Foundation
/// A type that can be used as a parameter in an SQL statement.
///
/// Conforming types provide a raw SQLite-compatible representation of their values,
/// enabling them to be directly bound to SQL statements.
///
/// **Example implementation:**
///
/// ```swift
/// struct Device: SQLiteRawBindable {
/// var model: String
///
/// var sqliteRawValue: SQLiteRawValue {
/// return .text(model)
/// }
/// }
/// ```
public protocol SQLiteRawBindable: SQLiteLiteralable {
/// The raw SQLite representation of the value.
///
/// This property provides a value that is compatible with SQLite's internal representation,
/// such as text, integer, real, blob, or null. It is used when binding the conforming
/// type to SQL statements.
var sqliteRawValue: SQLiteRawValue { get }
}
public extension SQLiteRawBindable {
/// The string representation of the value as an SQLite literal.
///
/// This property leverages the `sqliteRawValue` to produce a valid SQLite-compatible literal,
/// formatted appropriately for use in SQL queries.
var sqliteLiteral: String {
sqliteRawValue.sqliteLiteral
}
}

View File

@@ -0,0 +1,35 @@
import Foundation
/// A type that can be initialized from a raw SQLite value.
///
/// This protocol extends `SQLiteRawBindable` and requires conforming types to implement
/// an initializer that can convert a raw SQLite value into the corresponding type.
///
/// **Example implementation:**
///
/// ```swift
/// struct Device: SQLiteRawRepresentable {
/// var model: String
///
/// var sqliteRawValue: SQLiteRawValue {
/// return .text(model)
/// }
///
/// init?(_ sqliteRawValue: SQLiteRawValue) {
/// guard
/// case let .text(value) = sqliteRawValue
/// else { return nil }
/// self.model = value
/// }
/// }
/// ```
public protocol SQLiteRawRepresentable: SQLiteRawBindable {
/// Initializes an instance from a raw SQLite value.
///
/// This initializer should map the provided SQLite raw value to the appropriate type.
/// If the conversion is not possible (e.g., if the raw value is of an incompatible type),
/// the initializer should return `nil`.
///
/// - Parameter sqliteRawValue: A raw SQLite value to be converted.
init?(_ sqliteRawValue: SQLiteRawValue)
}

View File

@@ -0,0 +1,105 @@
import Foundation
/// A type representing SQLite pragmas.
///
/// The `Pragma` structure provides a convenient way to work with
/// SQLite pragmas, which are special commands used to control various aspects
/// of the SQLite database engine. For more information on SQLite pragmas,
/// visit the [SQLite Pragma Documentation](https://www.sqlite.org/pragma.html).
///
/// ## Topics
///
/// ### Initializers
///
/// - ``init(rawValue:)``
/// - ``init(stringLiteral:)``
///
/// ### Instances
///
/// - ``applicationID``
/// - ``foreignKeys``
/// - ``journalMode``
/// - ``synchronous``
/// - ``userVersion``
public struct Pragma: RawRepresentable, CustomStringConvertible, ExpressibleByStringLiteral, Sendable {
// MARK: - Properties
/// The raw string value of the pragma.
///
/// This is the underlying string that represents the pragma, which can be used
/// directly in SQL queries.
public var rawValue: String
/// A textual representation of the pragma.
///
/// This provides a description of the pragma as a string.
public var description: String {
rawValue
}
// MARK: - Instances
/// Represents the `application_id` pragma.
///
/// This pragma allows you to query or set the application ID associated with the
/// SQLite database file. The application ID is a 32-bit integer that can be used for
/// versioning or identification purposes. For more details, see
/// [application_id](https://www.sqlite.org/pragma.html#pragma_application_id).
public static let applicationID: Pragma = "application_id"
/// Represents the `foreign_keys` pragma.
///
/// This pragma controls the enforcement of foreign key constraints in SQLite.
/// Foreign key constraints are disabled by default, but you can enable them
/// by using this pragma. For more details, see
/// [foreign_keys](https://www.sqlite.org/pragma.html#pragma_foreign_keys).
public static let foreignKeys: Pragma = "foreign_keys"
/// Represents the `journal_mode` pragma.
///
/// This pragma is used to query or configure the journal mode for the database connection.
/// The journal mode determines how transactions are logged, influencing both the
/// performance and recovery behavior of the database. For more details, see
/// [journal_mode](https://www.sqlite.org/pragma.html#pragma_journal_mode).
public static let journalMode: Pragma = "journal_mode"
/// Represents the `synchronous` pragma.
///
/// This pragma is used to query or configure the synchronous mode for the database connection.
/// The synchronous mode controls how the database synchronizes with the disk during write operations,
/// affecting both performance and durability. For more details, see
/// [synchronous](https://www.sqlite.org/pragma.html#pragma_synchronous).
public static let synchronous: Pragma = "synchronous"
/// Represents the `user_version` pragma.
///
/// This pragma is commonly used to query or set the user version number associated
/// with the database file. It is useful for schema versioning or implementing custom
/// database management strategies. For more details, see
/// [user_version](https://www.sqlite.org/pragma.html#pragma_user_version).
public static let userVersion: Pragma = "user_version"
public static let busyTimeout: Pragma = "busy_timeout"
// MARK: - Inits
/// Initializes a `Pragma` instance with the provided raw value.
///
/// - Parameter rawValue: The raw string value of the pragma.
///
/// This initializer allows you to create a `Pragma` instance with any raw string
/// that represents a valid SQLite pragma.
public init(rawValue: String) {
self.rawValue = rawValue
}
/// Initializes a `Pragma` instance with the provided string literal.
///
/// - Parameter value: The string literal representing the pragma.
///
/// This initializer allows you to create a `Pragma` instance using a string literal,
/// providing a convenient syntax for common pragmas.
public init(stringLiteral value: String) {
self.rawValue = value
}
}

View File

@@ -0,0 +1,273 @@
import Foundation
/// A structure representing a collection of SQL queries.
///
/// ## Overview
///
/// `SQLScript` is a structure for loading and processing SQL scripts, representing a collection
/// where each element is an individual SQL query. It allows loading scripts from a file via URL,
/// from the app bundle, or from a string, and provides convenient access to individual SQL queries
/// and the ability to iterate over them.
///
/// ## Usage Examples
///
/// ### Loading from a File
///
/// To load a SQL script from a file in your project, use the following code. In this example,
/// we load a file named `sample_script.sql` from the main app bundle.
///
/// ```swift
/// do {
/// guard let sqlScript = try SQLScript(
/// byResource: "sample_script",
/// extension: "sql"
/// ) else {
/// throw NSError(
/// domain: "SomeDomain",
/// code: -1,
/// userInfo: [:]
/// )
/// }
///
/// for (index, statement) in sqlScript.enumerated() {
/// print("Query \(index + 1):")
/// print(statement)
/// print("--------------------")
/// }
/// } catch {
/// print("Error: \(error.localizedDescription)")
/// }
/// ```
///
/// ### Loading from a String
///
/// If the SQL queries are already contained in a string, you can create an instance of `SQLScript`
/// by passing the string to the initializer. Below is an example where we create a SQL script
/// with two queries: creating a table and inserting data.
///
/// ```swift
/// do {
/// let sqlString = """
/// CREATE TABLE users (
/// id INTEGER PRIMARY KEY,
/// username TEXT NOT NULL,
/// email TEXT NOT NULL
/// );
/// INSERT INTO users (id, username, email)
/// VALUES (1, 'john_doe', 'john@example.com');
/// """
///
/// let sqlScript = try SQLScript(string: sqlString)
///
/// for (index, statement) in sqlScript.enumerated() {
/// print("Query \(index + 1):")
/// print(statement)
/// print("--------------------")
/// }
/// } catch {
/// print("Error: \(error.localizedDescription)")
/// }
/// ```
///
/// ## SQL Format and Syntax
///
/// `SQLScript` is designed to handle SQL scripts that contain one or more SQL queries. Each query
/// must end with a semicolon (`;`), which indicates the end of the statement.
///
/// **Supported Features:**
///
/// - **Command Separation:** Each query ends with a semicolon (`;`), marking the end of the command.
/// - **Formatting:** SQLScript removes lines containing only whitespace or comments, keeping
/// only the valid SQL queries in the collection.
/// - **Comment Support:** Single-line (`--`) and multi-line (`/* */`) comments are supported.
///
/// **Example of a correctly formatted SQL script:**
///
/// ```sql
/// -- Create users table
/// CREATE TABLE users (
/// id INTEGER PRIMARY KEY,
/// username TEXT NOT NULL,
/// email TEXT NOT NULL
/// );
///
/// -- Insert data into users table
/// INSERT INTO users (id, username, email)
/// VALUES (1, 'john_doe', 'john@example.com');
///
/// /* Update user data */
/// UPDATE users
/// SET email = 'john.doe@example.com'
/// WHERE id = 1;
/// ```
///
/// - Important: Nested comments are not supported, so avoid placing multi-line comments inside
/// other multi-line comments.
///
/// - Important: `SQLScript` does not support SQL scripts containing transactions.
/// To execute an `SQLScript`, use the method ``Connection/execute(sql:)``, which executes
/// each statement individually in autocommit mode.
///
/// If you need to execute the entire `SQLScript` within a single transaction, use the methods
/// ``Connection/beginTransaction(_:)``, ``Connection/commitTransaction()``, and
/// ``Connection/rollbackTransaction()`` to manage the transaction explicitly.
///
/// If your SQL script includes transaction statements (e.g., BEGIN, COMMIT, ROLLBACK),
/// execute the entire script using ``Connection/execute(raw:)``.
///
/// - Important: This class is not designed to work with untrusted user data. Never insert
/// user-provided data directly into SQL queries without proper sanitization or parameterization.
/// Unfiltered data can lead to SQL injection attacks, which pose a security risk to your data and
/// database. For more information about SQL injection risks, see the OWASP documentation:
/// [SQL Injection](https://owasp.org/www-community/attacks/SQL_Injection).
public struct SQLScript: Collection, ExpressibleByStringLiteral {
/// The type representing the index in the collection of SQL queries.
///
/// This type is used to access elements in the `SQLScript` collection. The index is an
/// integer (`Int`) indicating the position of the SQL query in the collection.
public typealias Index = Int
/// The type representing an element in the collection of SQL queries.
///
/// This type defines that each element in the `SQLScript` collection is a string (`String`).
/// Each element represents a separate SQL query that can be loaded and processed.
public typealias Element = String
// MARK: - Properties
/// An array containing SQL queries.
private var elements: [Element]
/// The starting index of the collection of SQL queries.
///
/// This property returns the index of the first element in the collection. The starting
/// index is used to initialize iterators and access the elements of the collection. If
/// the collection is empty, this value will equal `endIndex`.
public var startIndex: Index {
elements.startIndex
}
/// The end index of the collection of SQL queries.
///
/// This property returns the index that indicates the position following the last element
/// of the collection. The end index is used for iterating over the collection and marks
/// the point where the collection ends. If the collection is empty, this value will equal
/// `startIndex`.
public var endIndex: Index {
elements.endIndex
}
/// The number of SQL queries in the collection.
///
/// This property returns the total number of SQL queries stored in the collection. The
/// value will be 0 if the collection is empty. Use this property to know how many queries
/// can be processed or iterated over.
public var count: Int {
elements.count
}
/// A Boolean value indicating whether the collection is empty.
///
/// This property returns `true` if there are no SQL queries in the collection, and `false`
/// otherwise. Use this property to check for the presence of SQL queries before performing
/// operations that require elements in the collection.
public var isEmpty: Bool {
elements.isEmpty
}
// MARK: - Inits
/// Initializes an instance of `SQLScript`, loading SQL queries from a resource file.
///
/// This initializer looks for a file with the specified name and extension in the given
/// bundle and loads its contents as SQL queries.
///
/// - If `name` is `nil`, the first found resource file with the specified extension will
/// be loaded.
/// - If `extension` is an empty string or `nil`, it is assumed that the extension does
/// not exist, and the first found file that exactly matches the name will be loaded.
/// - Returns `nil` if the specified file is not found.
///
/// - Parameters:
/// - name: The name of the resource file containing SQL queries.
/// - extension: The extension of the resource file. Defaults to `nil`.
/// - bundle: The bundle from which to load the resource file. Defaults to `.main`.
///
/// - Throws: An error if the file cannot be loaded or processed.
public init?(
byResource name: String?,
extension: String? = nil,
in bundle: Bundle = .main
) throws {
guard let url = bundle.url(
forResource: name,
withExtension: `extension`
) else { return nil }
try self.init(contentsOf: url)
}
/// Initializes an instance of `SQLScript`, loading SQL queries from the specified file.
///
/// This initializer takes a URL to a file and loads its contents as SQL queries.
///
/// - Parameter url: The URL of the file containing SQL queries.
///
/// - Throws: An error if the file cannot be loaded or processed.
public init(contentsOf url: URL) throws {
try self.init(string: .init(contentsOf: url, encoding: .utf8))
}
/// Initializes an instance of `SQLScript` from a string literal.
///
/// This initializer allows you to create a `SQLScript` instance directly from a string literal.
/// The string is parsed into individual SQL queries, removing comments and empty lines.
///
/// - Parameter value: The string literal containing SQL queries. Each query should be separated
/// by semicolons (`;`), and the string can include comments and empty lines, which will
/// be ignored during initialization.
///
/// - Warning: The string literal should represent valid SQL queries. Invalid syntax or
/// improperly formatted SQL may lead to an error at runtime.
public init(stringLiteral value: StringLiteralType) {
self.init(string: value)
}
/// Initializes an instance of `SQLScript` by parsing SQL queries from the specified string.
///
/// This initializer takes a string containing SQL queries and extracts individual queries,
/// removing comments and empty lines.
///
/// - Parameter string: The string containing SQL queries.
public init(string: String) {
elements = string
.removingComments()
.trimmingLines()
.splitStatements()
}
// MARK: - Collection Methods
/// Accesses the element at the specified position in the collection.
///
/// - Parameter index: The index of the SQL query in the collection. The index must be within the
/// bounds of the collection. If an out-of-bounds index is provided, a runtime error will occur.
///
/// - Returns: The SQL query as a string at the specified index.
public subscript(index: Index) -> Element {
elements[index]
}
/// Returns the index after the given index.
///
/// This method is used for iterating through the collection. It provides the next valid
/// index following the specified index.
///
/// - Parameter i: The index of the current element.
///
/// - Returns: The index of the next element in the collection. If `i` is the last
/// index in the collection, this method returns `endIndex`, which is one past the
/// last valid index.
public func index(after i: Index) -> Index {
elements.index(after: i)
}
}

View File

@@ -0,0 +1,225 @@
import Foundation
import OrderedCollections
/// A structure representing a single row in an SQLite database, providing ordered access to columns and their values.
///
/// The `SQLiteRow` structure allows for convenient access to the data stored in a row of an SQLite
/// database, using an ordered dictionary to maintain the insertion order of columns. This makes it
/// easy to retrieve, update, and manage the values associated with each column in the row.
///
/// ```swift
/// let row = SQLiteRow()
/// row["name"] = "John Doe"
/// row["age"] = 30
/// print(row.description)
/// // Outputs: ["name": 'John Doe', "age": 30]
/// ```
///
/// ## Topics
///
/// ### Type Aliases
///
/// - ``Elements``
/// - ``Column``
/// - ``Value``
/// - ``Index``
/// - ``Element``
public struct SQLiteRow: Collection, CustomStringConvertible, Equatable {
// MARK: - Type Aliases
/// A type for the internal storage of column names and their associated values in a database row.
///
/// This ordered dictionary is used to store column data for a row, retaining the insertion order
/// of columns as they appear in the SQLite database. Each key-value pair corresponds to a column name
/// and its associated value, represented by `SQLiteRawValue`.
///
/// - Key: `String` representing the name of the column.
/// - Value: `SQLiteRawValue` representing the value of the column in the row.
public typealias Elements = OrderedDictionary<String, SQLiteRawValue>
/// A type representing the name of a column in a database row.
///
/// This type alias provides a convenient way to refer to column names within a row.
/// Each `Column` is a `String` key that corresponds to a specific column in the SQLite row,
/// matching the key type of the `Elements` dictionary.
public typealias Column = Elements.Key
/// A type representing the value of a column in a database row.
///
/// This type alias provides a convenient way to refer to the data stored in a column.
/// Each `Value` is of type `SQLiteRawValue`, which corresponds to the value associated
/// with a specific column in the SQLite row, matching the value type of the `Elements` dictionary.
public typealias Value = Elements.Value
/// A type representing the index of a column in a database row.
///
/// This type alias provides a convenient way to refer to the position of a column
/// within the ordered collection of columns. Each `Index` is an integer that corresponds
/// to the index of a specific column in the SQLite row, matching the index type of the `Elements` dictionary.
public typealias Index = Elements.Index
/// A type representing a column-value pair in a database row.
///
/// This type alias defines an element as a tuple consisting of a `Column` and its associated
/// `Value`. Each `Element` encapsulates a single column name and its corresponding value,
/// providing a clear structure for accessing and managing data within the SQLite row.
public typealias Element = (column: Column, value: Value)
// MARK: - Properties
/// An ordered dictionary that stores the columns and their associated values in the row.
///
/// This private property holds the internal representation of the row's data as an
/// `OrderedDictionary`, maintaining the insertion order of columns. It is used to
/// facilitate access to the row's columns and values, ensuring that the original
/// order from the SQLite database is preserved.
private var elements: Elements
/// The starting index of the row, which is always zero.
///
/// This property indicates the initial position of the row's elements. Since the
/// elements in the row are indexed starting from zero, this property consistently
/// returns zero, allowing for predictable iteration through the row's data.
///
/// - Complexity: `O(1)`
public var startIndex: Index {
0
}
/// The ending index of the row, which is equal to the number of columns.
///
/// This property indicates the position one past the last element in the row.
/// It returns the count of columns in the row, allowing for proper iteration
/// through the row's data in a collection context. The `endIndex` is useful
/// for determining the bounds of the row's elements when traversing or accessing them.
///
/// - Complexity: `O(1)`
public var endIndex: Index {
elements.count
}
/// A Boolean value indicating whether the row is empty.
///
/// This property returns `true` if the row contains no columns; otherwise, it returns `false`.
/// It provides a quick way to check if there are any data present in the row, which can be
/// useful for validation or conditional logic when working with database rows.
///
/// - Complexity: `O(1)`
public var isEmpty: Bool {
elements.isEmpty
}
/// The number of columns in the row.
///
/// This property returns the total count of columns stored in the row. It reflects
/// the number of column-value pairs in the `elements` dictionary, providing a convenient
/// way to determine how much data is present in the row. This is useful for iteration
/// and conditional checks when working with database rows.
///
/// - Complexity: `O(1)`
public var count: Int {
elements.count
}
/// A textual description of the row, showing the columns and values.
///
/// This property returns a string representation of the row, including all column names
/// and their associated values. The description is generated from the `elements` dictionary,
/// providing a clear and concise overview of the row's data, which can be helpful for debugging
/// and logging purposes.
public var description: String {
elements.description
}
/// A list of column names in the row, preserving their insertion order.
///
/// Useful for dynamically generating SQL queries (e.g. `INSERT INTO ... (columns)`).
///
/// - Complexity: `O(1)`
public var columns: [String] {
elements.keys.elements
}
/// A list of SQL named parameters in the form `:column`, preserving column order.
///
/// Useful for generating placeholders in SQL queries (e.g. `VALUES (:column1, :column2, ...)`)
/// to match the row's structure.
///
/// - Complexity: `O(n)`
public var namedParameters: [String] {
elements.keys.map { ":\($0)" }
}
// MARK: - Inits
/// Initializes an empty row.
///
/// This initializer creates a new instance of `SQLiteRow` with no columns or values.
public init() {
elements = [:]
}
// MARK: - Subscripts
/// Accesses the element at the specified index.
///
/// This subscript allows you to retrieve a column-value pair from the row by its index.
/// It returns an `Element`, which is a tuple containing the column name and its associated
/// value. The index must be valid; otherwise, it will trigger a runtime error.
///
/// - Parameter index: The index of the element to access.
/// - Returns: A tuple containing the column name and its associated value.
///
/// - Complexity: `O(1)`
public subscript(index: Index) -> Element {
let element = elements.elements[index]
return (element.key, element.value)
}
/// Accesses the value for the specified column.
///
/// This subscript allows you to retrieve or set the value associated with a given column name.
/// It returns an optional `Value`, which is the value stored in the row for the specified column.
/// If the column does not exist, it returns `nil`. When setting a value, the column will be created
/// if it does not already exist.
///
/// - Parameter column: The name of the column to access.
/// - Returns: The value associated with the specified column, or `nil` if the column does not exist.
///
/// - Complexity: On average, the complexity is O(1) for lookups and amortized O(1) for updates.
public subscript(column: Column) -> Value? {
get { elements[column] }
set { elements[column] = newValue }
}
// MARK: - Methods
/// Returns the index immediately after the given index.
///
/// This method provides the next valid index in the row's collection after the specified index.
/// It increments the given index by one, allowing for iteration through the row's elements
/// in a collection context. If the provided index is the last valid index, this method
/// will return an index that may not be valid for the collection, so it should be used
/// in conjunction with bounds checking.
///
/// - Parameter i: A valid index of the row.
/// - Returns: The index immediately after `i`.
///
/// - Complexity: `O(1)`
public func index(after i: Index) -> Index {
i + 1
}
/// Checks if the row contains a value for the specified column.
///
/// This method determines whether a column with the given name exists in the row. It is
/// useful for validating the presence of data before attempting to access it.
///
/// - Parameter column: The name of the column to check for.
/// - Returns: `true` if the column exists; otherwise, `false`.
///
/// - Complexity: On average, the complexity is `O(1)`.
public func contains(_ column: Column) -> Bool {
elements.keys.contains(column)
}
}