Refactor entire codebase and rewrite documentation

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

View File

@@ -15,8 +15,8 @@ DataLiteCore provides an object-oriented API over the C interface, allowing deve
## Requirements ## Requirements
- **Swift**: 5.10+ - **Swift**: 6.0+
- **Platforms**: macOS 10.14+, iOS 12.0+, Linux - **Platforms**: macOS 10.14+, iOS 12.0+
## Installation ## Installation
@@ -37,7 +37,7 @@ To add DataLiteCore to your project, use Swift Package Manager (SPM).
If you are using Swift Package Manager with a `Package.swift` file, add the dependency like this: If you are using Swift Package Manager with a `Package.swift` file, add the dependency like this:
```swift ```swift
// swift-tools-version: 5.10 // swift-tools-version: 6.0
import PackageDescription import PackageDescription
let package = Package( let package = Package(

View File

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

View File

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

View File

@@ -1,140 +1,43 @@
import Foundation import Foundation
extension Connection { extension Connection {
/// The `Location` enum represents different locations for a SQLite database. /// A location specifying where the SQLite database is stored or created.
/// ///
/// This enum allows you to specify how and where a SQLite database will be stored or accessed. /// Three locations are supported:
/// You can choose from three options: /// - ``file(path:)``: A database at a specific file path or URI (persistent).
/// /// - ``inMemory``: An in-memory database that exists only in RAM.
/// - **File**: A database located at a specified file path or URI. This option is suitable /// - ``temporary``: A temporary on-disk database deleted when the connection closes.
/// 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 { public enum Location {
/// A database located at a given file path or URI. /// A database stored at a given file path or URI.
/// ///
/// This case allows you to specify the exact location of a SQLite database using a file /// Use this for persistent databases located on disk or referenced via SQLite URI.
/// path or a URI. The provided path should point to a valid SQLite database file. If the /// The file is created if it does not exist (subject to open options).
/// 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 /// - Parameter path: Absolute/relative file path or URI.
/// relative path, or a URI scheme supported by SQLite. /// - SeeAlso: [Uniform Resource Identifiers](https://sqlite.org/uri.html)
///
/// ### 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) case file(path: String)
/// An in-memory database. /// A transient in-memory database.
/// ///
/// In-memory databases are temporary and exist only in RAM. They are not persisted to disk, /// The database exists only in RAM and is discarded once the connection closes.
/// which makes them suitable for scenarios where you need fast access to data without the /// Suitable for testing, caching, or temporary data processing.
/// overhead of disk I/O.
/// ///
/// When you create an in-memory database, it is stored entirely in memory, meaning that /// - SeeAlso: [In-Memory Databases](https://sqlite.org/inmemorydb.html)
/// 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 case inMemory
/// A temporary database on disk. /// A temporary on-disk database.
/// ///
/// Temporary databases are created on disk but are not intended for persistent storage. They /// Created on disk and removed automatically when the connection closes or the
/// are automatically deleted when the connection is closed or when the process ends. This /// process terminates. Useful for ephemeral data that should not persist.
/// 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: /// - SeeAlso: [Temporary Databases](https://sqlite.org/inmemorydb.html)
/// - 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 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 { var path: String {
switch self { switch self {
case .file(let path): return path case .file(let path): path
case .inMemory: return ":memory:" case .inMemory: ":memory:"
case .temporary: return "" case .temporary: ""
} }
} }
} }

View File

@@ -2,437 +2,98 @@ import Foundation
import DataLiteC import DataLiteC
extension Connection { extension Connection {
/// Options for controlling the connection to a SQLite database. /// Options that control how the SQLite database connection is opened.
/// ///
/// This type represents a set of options that can be used when opening a connection to a /// Each option corresponds to a flag from the SQLite C API. Multiple options can be combined
/// SQLite database. Each option corresponds to one of the flags defined in the SQLite /// using the `OptionSet` syntax.
/// library. For more details, read [Opening A New Database Connection](https://www.sqlite.org/c3ref/open.html).
/// ///
/// ### Usage /// - SeeAlso: [Opening A New Database Connection](https://sqlite.org/c3ref/open.html)
///
/// ```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 { public struct Options: OptionSet, Sendable {
// MARK: - Properties // MARK: - Properties
/// An integer value representing a combination of option flags. /// The raw integer value representing the 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 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 // MARK: - Inits
/// Initializes a set of options for connecting to a SQLite database. /// Creates a new set of options from a raw integer value.
/// ///
/// This initializer allows you to create a combination of option flags that dictate how the /// Combine multiple flags using bitwise OR (`|`).
/// 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 /// ```swift
/// let options = Connection.Options( /// let opts = Connection.Options(
/// rawValue: SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE /// 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) { public init(rawValue: Int32) {
self.rawValue = rawValue self.rawValue = rawValue
} }
// MARK: - Instances
/// Opens the database in read-only mode.
///
/// Fails if the database file does not exist.
public static let readonly = Self(rawValue: SQLITE_OPEN_READONLY)
/// Opens the database for reading and writing.
///
/// Fails if the file does not exist or is write-protected.
public static let readwrite = Self(rawValue: SQLITE_OPEN_READWRITE)
/// Creates the database file if it does not exist.
///
/// Commonly combined with `.readwrite`.
public static let create = Self(rawValue: SQLITE_OPEN_CREATE)
/// Interprets the filename as a URI.
///
/// Enables SQLites URI parameters and schemes.
/// - SeeAlso: [Uniform Resource Identifiers](https://sqlite.org/uri.html)
public static let uri = Self(rawValue: SQLITE_OPEN_URI)
/// Opens an in-memory database.
///
/// Data is stored in RAM and discarded when closed.
/// - SeeAlso: [In-Memory Databases](https://sqlite.org/inmemorydb.html)
public static let memory = Self(rawValue: SQLITE_OPEN_MEMORY)
/// Disables mutexes for higher concurrency.
///
/// Each thread must use a separate connection.
/// - SeeAlso: [Using SQLite In Multi-Threaded Applications](
/// https://sqlite.org/threadsafe.html)
public static let nomutex = Self(rawValue: SQLITE_OPEN_NOMUTEX)
/// Enables serialized access using full mutexes.
///
/// Safe for concurrent access from multiple threads.
/// - SeeAlso: [Using SQLite In Multi-Threaded Applications](
/// https://sqlite.org/threadsafe.html)
public static let fullmutex = Self(rawValue: SQLITE_OPEN_FULLMUTEX)
/// Enables shared cache mode.
///
/// Allows multiple connections to share cached data.
/// - SeeAlso: [SQLite Shared-Cache Mode](https://sqlite.org/sharedcache.html)
/// - Warning: Shared cache mode is discouraged by SQLite.
public static let sharedcache = Self(rawValue: SQLITE_OPEN_SHAREDCACHE)
/// Disables shared cache mode.
///
/// Each connection uses a private cache.
/// - SeeAlso: [SQLite Shared-Cache Mode](https://sqlite.org/sharedcache.html)
public static let privatecache = Self(rawValue: SQLITE_OPEN_PRIVATECACHE)
/// Enables extended result codes.
///
/// Provides more detailed SQLite error codes.
/// - SeeAlso: [Result and Error Codes](https://sqlite.org/rescode.html)
public static let exrescode = Self(rawValue: SQLITE_OPEN_EXRESCODE)
/// Disallows following symbolic links.
///
/// Improves security by preventing indirect file access.
public static let nofollow = Self(rawValue: SQLITE_OPEN_NOFOLLOW)
} }
} }

View File

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

View File

@@ -2,24 +2,23 @@ import Foundation
import DataLiteC import DataLiteC
extension Function { extension Function {
/// Base class for creating custom SQLite aggregate functions. /// Base class for defining custom SQLite aggregate functions.
/// ///
/// This class provides a basic implementation for creating aggregate functions in SQLite. /// The `Aggregate` class provides a foundation for creating aggregate
/// Aggregate functions process a set of input values and return a single value. /// functions in SQLite. Aggregate functions operate on multiple rows of
/// To create a custom aggregate function, subclass `Function.Aggregate` and override the /// input and return a single result value.
/// following properties and methods:
/// ///
/// - ``name``: The name of the function used in SQL queries. /// To define a custom aggregate function, subclass `Function.Aggregate` and
/// - ``argc``: The number of arguments the function accepts. /// override the following:
/// - ``options``: Options for the function, such as `deterministic` and `innocuous`. ///
/// - ``step(args:)``: Method called for each input value. /// - ``name`` The SQL name of the function.
/// - ``finalize()``: Method called after processing all input values. /// - ``argc`` The number of arguments accepted by the function.
/// - ``options`` Function options, such as `.deterministic` or `.innocuous`.
/// - ``step(args:)`` Called for each row's argument values.
/// - ``finalize()`` Called once to compute and return the final result.
/// ///
/// ### Example /// ### Example
/// ///
/// This example shows how to create a custom aggregate function to calculate the sum
/// of integers.
///
/// ```swift /// ```swift
/// final class SumAggregate: Function.Aggregate { /// final class SumAggregate: Function.Aggregate {
/// enum Error: Swift.Error { /// enum Error: Swift.Error {
@@ -34,23 +33,20 @@ extension Function {
/// ///
/// private var sum: Int = 0 /// private var sum: Int = 0
/// ///
/// override func step(args: Arguments) throws { /// override func step(args: ArgumentsProtocol) throws {
/// guard let value = args[0] as Int? else { /// guard let value = args[0] as Int? else {
/// throw Error.argumentsWrong /// throw Error.argumentsWrong
/// } /// }
/// sum += value /// sum += value
/// } /// }
/// ///
/// override func finalize() throws -> SQLiteRawRepresentable? { /// override func finalize() throws -> SQLiteRepresentable? {
/// return sum /// return sum
/// } /// }
/// } /// }
/// ``` /// ```
/// ///
/// ### Usage /// ### Registration
///
/// To use a custom aggregate function, first establish a database connection and
/// register the function.
/// ///
/// ```swift /// ```swift
/// let connection = try Connection( /// let connection = try Connection(
@@ -62,16 +58,13 @@ extension Function {
/// ///
/// ### SQL Example /// ### 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 /// ```sql
/// SELECT sum_aggregate(value) FROM my_table /// SELECT sum_aggregate(value) FROM my_table
/// ``` /// ```
/// ///
/// ## Topics /// ## Topics
/// ///
/// ### Initializers /// ### Initialization
/// ///
/// - ``init()`` /// - ``init()``
/// ///
@@ -80,139 +73,23 @@ extension Function {
/// - ``step(args:)`` /// - ``step(args:)``
/// - ``finalize()`` /// - ``finalize()``
open class Aggregate: Function { 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 // MARK: - Properties
/// Flag indicating whether an error occurred during execution.
fileprivate var hasErrored = false fileprivate var hasErrored = false
// MARK: - Inits // MARK: - Inits
/// Initializes a new instance of the `Aggregate` class. /// Initializes a new aggregate function instance.
/// ///
/// This initializer is required for subclasses of ``Aggregate``. /// Subclasses may override this initializer to perform custom setup.
/// In the current implementation, it performs no additional actions but provides /// The base implementation performs no additional actions.
/// a basic structure for creating instances.
/// ///
/// Subclasses may override this initializer to implement their own initialization /// - Important: Always call `super.init()` when overriding.
/// 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() {} required public override init() {}
// MARK: - Methods // MARK: - Methods
/// Installs the custom SQLite aggregate function into the specified database connection. override class func install(db connection: OpaquePointer) throws(SQLiteError) {
///
/// 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 context = Context(function: self)
let ctx = Unmanaged.passRetained(context).toOpaque() let ctx = Unmanaged.passRetained(context).toOpaque()
let status = sqlite3_create_function_v2( let status = sqlite3_create_function_v2(
@@ -220,105 +97,91 @@ extension Function {
nil, xStep(_:_:_:), xFinal(_:), xDestroy(_:) nil, xStep(_:_:_:), xFinal(_:), xDestroy(_:)
) )
if status != SQLITE_OK { if status != SQLITE_OK {
throw Connection.Error(connection) throw SQLiteError(connection)
} }
} }
/// Called for each input value during aggregate computation. /// Processes one step of the aggregate computation.
/// ///
/// This method should be overridden by subclasses to implement the specific logic /// This method is called once for each row of input data. Subclasses must override it to
/// for processing each input value. Your implementation should handle the input /// accumulate intermediate results.
/// arguments and accumulate results for later finalization.
/// ///
/// ```swift /// - Parameter args: The set of arguments passed to the function.
/// class MyCustomAggregate: Function.Aggregate { /// - Throws: An error if the input arguments are invalid or the computation fails.
/// // ...
/// ///
/// private var sum: Int = 0 /// - Note: The default implementation triggers a runtime error.
/// open func step(args: any ArgumentsProtocol) throws {
/// override func step(args: Arguments) throws { fatalError("Subclasses must override `step(args:)`.")
/// 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. /// Finalizes the aggregate computation and returns the result.
/// ///
/// This method should be overridden by subclasses to return the final result /// SQLite calls this method once after all input rows have been processed.
/// of the aggregate computation. Your implementation should return a value that will /// Subclasses must override it to produce the final result of the aggregate.
/// be used in SQL queries. If the aggregate should not return a value, you can
/// return `nil`.
/// ///
/// ```swift /// - Returns: The final computed value, or `nil` if the function produces no result.
/// class MyCustomAggregate: Function.Aggregate { /// - Throws: An error if the computation cannot be finalized.
/// // ... /// - Note: The default implementation triggers a runtime error.
/// open func finalize() throws -> SQLiteRepresentable? {
/// private var sum: Int = 0 fatalError("Subclasses must override `finalize()`.")
/// }
/// override func finalize() throws -> SQLiteRawRepresentable? { }
/// return sum }
/// }
/// } extension Function.Aggregate {
/// ``` fileprivate final class Context {
/// // MARK: - Properties
/// - Returns: An optional ``SQLiteRawRepresentable`` representing the result of the
/// aggregate function. The return value may be `nil` if a result is not required. private let function: Aggregate.Type
///
/// - Throws: An error if the function execution fails. Subclasses may throw errors // MARK: - Inits
/// if the aggregate cannot be computed correctly or if other issues arise.
/// init(function: Aggregate.Type) {
/// - Note: It is important to override this method in subclasses; otherwise, a self.function = function
/// runtime error will occur due to calling `fatalError()`. }
open func finalize() throws -> SQLiteRawRepresentable? {
fatalError("The 'finalize' method should be overridden.") // MARK: - Methods
func function(
for ctx: OpaquePointer?, isFinal: Bool = false
) -> Unmanaged<Aggregate>? {
typealias U = Unmanaged<Aggregate>
let bytes = isFinal ? 0 : MemoryLayout<U>.stride
let raw = sqlite3_aggregate_context(ctx, Int32(bytes))
guard let raw else { return nil }
let pointer = raw.assumingMemoryBound(to: U?.self)
if let pointer = pointer.pointee {
return pointer
} else {
let function = self.function.init()
pointer.pointee = Unmanaged.passRetained(function)
return pointer.pointee
}
} }
} }
} }
// MARK: - Functions // 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( private func xStep(
_ ctx: OpaquePointer?, _ ctx: OpaquePointer?,
_ argc: Int32, _ argc: Int32,
_ argv: UnsafeMutablePointer<OpaquePointer?>? _ argv: UnsafeMutablePointer<OpaquePointer?>?
) { ) {
let context = Unmanaged<Function.Aggregate.Context> let function = Unmanaged<Function.Aggregate.Context>
.fromOpaque(sqlite3_user_data(ctx)) .fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue() .takeUnretainedValue()
.function(for: ctx)?
let function = context
.function(ctx: ctx)
.takeUnretainedValue() .takeUnretainedValue()
guard let function else {
sqlite3_result_error_nomem(ctx)
return
}
assert(!function.hasErrored) assert(!function.hasErrored)
do { do {
@@ -330,31 +193,28 @@ private func xStep(
let message = "Error executing function '\(name)': \(description)" let message = "Error executing function '\(name)': \(description)"
function.hasErrored = true function.hasErrored = true
sqlite3_result_error(ctx, message, -1) sqlite3_result_error(ctx, message, -1)
sqlite3_result_error_code(ctx, SQLITE_ERROR)
} }
} }
/// C callback function to finalize the result of the custom SQLite aggregate function.
///
/// This function is called by SQLite when the aggregate computation is complete.
/// It retrieves the function implementation from the context, calls the `finalize()`
/// method, and sets the query result based on the returned value.
///
/// - Parameter ctx: Pointer to the SQLite context associated with the current query.
private func xFinal(_ ctx: OpaquePointer?) { private func xFinal(_ ctx: OpaquePointer?) {
let context = Unmanaged<Function.Aggregate.Context> let pointer = Unmanaged<Function.Aggregate.Context>
.fromOpaque(sqlite3_user_data(ctx)) .fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue() .takeUnretainedValue()
.function(for: ctx, isFinal: true)
let unmanagedFunction = context.function(ctx: ctx) defer { pointer?.release() }
let function = unmanagedFunction.takeUnretainedValue()
defer { unmanagedFunction.release() } guard let function = pointer?.takeUnretainedValue() else {
sqlite3_result_null(ctx)
return
}
guard !function.hasErrored else { return } guard !function.hasErrored else { return }
do { do {
let result = try function.finalize() let result = try function.finalize()
sqlite3_result_value(ctx, result?.sqliteRawValue) sqlite3_result_value(ctx, result?.sqliteValue)
} catch { } catch {
let name = type(of: function).name let name = type(of: function).name
let description = error.localizedDescription let description = error.localizedDescription
@@ -364,12 +224,6 @@ private func xFinal(_ ctx: OpaquePointer?) {
} }
} }
/// C callback function to destroy the context associated with the custom SQLite aggregate function.
///
/// This function is called by SQLite when the function is uninstalled. It frees the memory
/// allocated for the `Context` object associated with the function to avoid memory leaks.
///
/// - Parameter ctx: Pointer to the SQLite query context.
private func xDestroy(_ ctx: UnsafeMutableRawPointer?) { private func xDestroy(_ ctx: UnsafeMutableRawPointer?) {
guard let ctx else { return } guard let ctx else { return }
Unmanaged<AnyObject>.fromOpaque(ctx).release() Unmanaged<AnyObject>.fromOpaque(ctx).release()

View File

@@ -4,29 +4,14 @@ import DataLiteC
extension Function { extension Function {
/// A collection representing the arguments passed to an SQLite 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 /// The `Arguments` structure provides a type-safe interface for accessing the arguments
/// function. Each argument is represented by an instance of `SQLiteValue`, which can hold /// received by a user-defined SQLite function. Each element of the collection is represented
/// various types of SQLite values such as integers, floats, text, blobs, or nulls. /// by a ``SQLiteValue`` instance that can store integers, floating-point numbers, text, blobs,
/// /// or nulls.
/// - Important: This collection does not perform bounds checking when accessing arguments via public struct Arguments: ArgumentsProtocol {
/// 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 // MARK: - Properties
/// The number of arguments passed to the SQLite function.
private let argc: Int32 private let argc: Int32
/// A pointer to an array of `OpaquePointer?` representing SQLite values.
private let argv: UnsafeMutablePointer<OpaquePointer?>? private let argv: UnsafeMutablePointer<OpaquePointer?>?
/// The number of arguments passed to the SQLite function. /// The number of arguments passed to the SQLite function.
@@ -34,28 +19,23 @@ extension Function {
Int(argc) Int(argc)
} }
/// A Boolean value indicating whether there are no arguments passed to the SQLite function. /// A Boolean value indicating whether there are no arguments.
public var isEmpty: Bool { public var isEmpty: Bool {
count == 0 count == 0
} }
/// The starting index of the arguments passed to the SQLite function. /// The index of the first argument.
public var startIndex: Index { public var startIndex: Index {
0 0
} }
/// The ending index of the arguments passed to the SQLite function. /// The index immediately after the last valid argument.
public var endIndex: Index { public var endIndex: Index {
count count
} }
// MARK: - Inits // 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?>?) { init(argc: Int32, argv: UnsafeMutablePointer<OpaquePointer?>?) {
self.argc = argc self.argc = argc
self.argv = argv self.argv = argv
@@ -63,18 +43,17 @@ extension Function {
// MARK: - Subscripts // MARK: - Subscripts
/// Accesses the SQLite value at the specified index. /// Returns the SQLite value at the specified index.
/// ///
/// - Parameter index: The index of the SQLite value to access. /// Retrieves the raw value from the SQLite function arguments and returns it as an
/// instance of ``SQLiteValue``.
///
/// - Parameter index: The index of the argument to retrieve.
/// - Returns: The SQLite value at the specified index. /// - 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) /// - Complexity: O(1)
public subscript(index: Index) -> Element { public subscript(index: Index) -> Element {
guard count > index else { guard index < count else {
fatalError("\(index) out of bounds") fatalError("Index \(index) out of bounds")
} }
let arg = argv.unsafelyUnwrapped[index] let arg = argv.unsafelyUnwrapped[index]
switch sqlite3_value_type(arg) { switch sqlite3_value_type(arg) {
@@ -86,31 +65,12 @@ extension Function {
} }
} }
/// Accesses the SQLite value at the specified index and converts it to a type conforming to
/// `SQLiteConvertible`.
///
/// - Parameter index: The index of the SQLite value to access.
/// - Returns: The SQLite value at the specified index, converted to the specified type,
/// or `nil` if conversion fails.
///
/// This subscript allows accessing the SQLite value at a specific index within the argument
/// list and converting it to a type conforming to `SQLiteConvertible`.
///
/// - Complexity: O(1)
public subscript<T: SQLiteRawRepresentable>(index: Index) -> T? {
T(self[index])
}
// MARK: - Methods // MARK: - Methods
/// Returns the index after the specified index. /// Returns the index that follows 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.
/// ///
/// - Parameter i: The current index.
/// - Returns: The index immediately after the specified one.
/// - Complexity: O(1) /// - Complexity: O(1)
public func index(after i: Index) -> Index { public func index(after i: Index) -> Index {
i + 1 i + 1
@@ -120,39 +80,10 @@ extension Function {
// MARK: - Functions // 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 { private func sqlite3_value_text(_ value: OpaquePointer!) -> String {
String(cString: DataLiteC.sqlite3_value_text(value)) 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 { private func sqlite3_value_blob(_ value: OpaquePointer!) -> Data {
Data( Data(
bytes: sqlite3_value_blob(value), bytes: sqlite3_value_blob(value),

View File

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

View File

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

View File

@@ -2,19 +2,21 @@ import Foundation
import DataLiteC import DataLiteC
extension Function { extension Function {
/// A base class for creating custom scalar SQLite functions. /// A base class for defining custom scalar SQLite functions.
/// ///
/// This class provides a base implementation for creating scalar functions in SQLite. /// The `Scalar` class provides a foundation for defining scalar functions in SQLite. Scalar
/// Scalar functions take one or more input arguments and return a single value. To /// functions take one or more input arguments and return a single value for each function call.
/// create a custom scalar function, subclass `Function.Scalar` and override the ///
/// ``name``, ``argc``, ``options``, and ``invoke(args:)`` methods. /// To define a custom scalar function, subclass `Function.Scalar` and override the following
/// members:
///
/// - ``name`` The SQL name of the function.
/// - ``argc`` The number of arguments the function accepts.
/// - ``options`` Function options, such as `.deterministic` or `.innocuous`.
/// - ``invoke(args:)`` The method implementing the functions logic.
/// ///
/// ### Example /// ### 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 /// ```swift
/// @available(macOS 13.0, *) /// @available(macOS 13.0, *)
/// final class Regexp: Function.Scalar { /// final class Regexp: Function.Scalar {
@@ -23,19 +25,15 @@ extension Function {
/// case regexError(Swift.Error) /// case regexError(Swift.Error)
/// } /// }
/// ///
/// // MARK: - Properties
///
/// override class var argc: Int32 { 2 } /// override class var argc: Int32 { 2 }
/// override class var name: String { "REGEXP" } /// override class var name: String { "REGEXP" }
/// override class var options: Function.Options { /// override class var options: Function.Options {
/// [.deterministic, .innocuous] /// [.deterministic, .innocuous]
/// } /// }
/// ///
/// // MARK: - Methods
///
/// override class func invoke( /// override class func invoke(
/// args: Function.Arguments /// args: ArgumentsProtocol
/// ) throws -> SQLiteRawRepresentable? { /// ) throws -> SQLiteRepresentable? {
/// guard let regex = args[0] as String?, /// guard let regex = args[0] as String?,
/// let value = args[1] as String? /// let value = args[1] as String?
/// else { throw Error.argumentsWrong } /// else { throw Error.argumentsWrong }
@@ -50,8 +48,7 @@ extension Function {
/// ///
/// ### Usage /// ### Usage
/// ///
/// Once you've created your custom function, you need to install it into the SQLite database /// To use a custom function, register it with an SQLite connection:
/// connection. Here's how you can add the `Regexp` function to a ``Connection`` instance:
/// ///
/// ```swift /// ```swift
/// let connection = try Connection( /// let connection = try Connection(
@@ -63,71 +60,15 @@ extension Function {
/// ///
/// ### SQL Example /// ### SQL Example
/// ///
/// With the `Regexp` function installed, you can use it in your SQL queries. For /// After registration, the function becomes available in SQL expressions:
/// example, to find rows where the `name` column matches the regular expression
/// `John.*`, you would write:
/// ///
/// ```sql /// ```sql
/// -- Find rows where 'name' column matches the regular expression 'John.*'
/// SELECT * FROM users WHERE REGEXP('John.*', name); /// SELECT * FROM users WHERE REGEXP('John.*', name);
/// ``` /// ```
open class Scalar: Function { 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 // MARK: - Methods
/// Installs a custom scalar SQLite function into the specified database connection. override class func install(db connection: OpaquePointer) throws(SQLiteError) {
///
/// 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 context = Context(function: self)
let ctx = Unmanaged.passRetained(context).toOpaque() let ctx = Unmanaged.passRetained(context).toOpaque()
let status = sqlite3_create_function_v2( let status = sqlite3_create_function_v2(
@@ -135,69 +76,58 @@ extension Function {
xFunc(_:_:_:), nil, nil, xDestroy(_:) xFunc(_:_:_:), nil, nil, xDestroy(_:)
) )
if status != SQLITE_OK { if status != SQLITE_OK {
throw Connection.Error(connection) throw SQLiteError(connection)
} }
} }
/// Implementation of the custom scalar function. /// Implements the logic of the custom scalar function.
/// ///
/// This method must be overridden by subclasses to implement the specific logic /// Subclasses must override this method to process the provided arguments and return a
/// of the function. Your implementation should handle the input arguments and return /// result value for the scalar function call.
/// the result as ``SQLiteRawRepresentable``.
/// ///
/// - Parameter args: An ``Arguments`` object containing the input arguments of the /// - Parameter args: The set of arguments passed to the function.
/// function. /// - Returns: The result of the function call, represented as ``SQLiteRepresentable``.
/// - Throws: An error if the arguments are invalid or the computation fails.
/// ///
/// - Returns: The result of the function execution, represented as /// - Note: The default implementation triggers a runtime error.
/// ``SQLiteRawRepresentable``. open class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
///
/// - 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.") fatalError("Subclasses must override this method to implement function logic.")
} }
} }
} }
extension Function.Scalar {
fileprivate final class Context {
// MARK: - Properties
let function: Scalar.Type
// MARK: - Inits
init(function: Scalar.Type) {
self.function = function
}
}
}
// MARK: - Functions // 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( private func xFunc(
_ ctx: OpaquePointer?, _ ctx: OpaquePointer?,
_ argc: Int32, _ argc: Int32,
_ argv: UnsafeMutablePointer<OpaquePointer?>? _ argv: UnsafeMutablePointer<OpaquePointer?>?
) { ) {
let context = Unmanaged<Function.Scalar.Context> let function = Unmanaged<Function.Scalar.Context>
.fromOpaque(sqlite3_user_data(ctx)) .fromOpaque(sqlite3_user_data(ctx))
.takeUnretainedValue() .takeUnretainedValue()
.function
do { do {
let args = Function.Arguments(argc: argc, argv: argv) let args = Function.Arguments(argc: argc, argv: argv)
let result = try context.function.invoke(args: args) let result = try function.invoke(args: args)
sqlite3_result_value(ctx, result?.sqliteRawValue) sqlite3_result_value(ctx, result?.sqliteValue)
} catch { } catch {
let name = context.function.name let name = function.name
let description = error.localizedDescription let description = error.localizedDescription
let message = "Error executing function '\(name)': \(description)" let message = "Error executing function '\(name)': \(description)"
sqlite3_result_error(ctx, message, -1) sqlite3_result_error(ctx, message, -1)
@@ -205,17 +135,6 @@ private func xFunc(
} }
} }
/// The C function callback for destroying the context associated with a custom SQLite scalar function.
///
/// This function is called by SQLite when the function is uninstalled. It releases the memory
/// allocated for the `Context` object associated with the function to avoid memory leaks.
///
/// - Parameter ctx: A pointer to the context of the SQLite query. This context contains the
/// `Context` object that should be released.
///
/// - Note: The `xDestroy` function should only release the memory allocated for the `Context`
/// object. It should not perform any other operations or access the context beyond freeing
/// the memory.
private func xDestroy(_ ctx: UnsafeMutableRawPointer?) { private func xDestroy(_ ctx: UnsafeMutableRawPointer?) {
guard let ctx else { return } guard let ctx else { return }
Unmanaged<AnyObject>.fromOpaque(ctx).release() Unmanaged<AnyObject>.fromOpaque(ctx).release()

View File

@@ -1,70 +1,66 @@
import Foundation import Foundation
import DataLiteC import DataLiteC
/// A base class representing a custom SQLite function. /// A base class representing a user-defined SQLite function.
/// ///
/// This class provides a framework for defining custom functions in SQLite. Subclasses must /// The `Function` class defines the common interface and structure for implementing custom SQLite
/// override specific properties and methods to define the function's behavior, including /// functions. Subclasses are responsible for specifying the function name, argument count, and
/// its name, argument count, and options. /// behavior. This class should not be used directly instead, use one of its specialized
/// subclasses, such as ``Scalar`` or ``Aggregate``.
/// ///
/// To create a custom SQLite function, you should subclass either ``Scalar`` or /// To define a new SQLite function, subclass either ``Scalar`` or ``Aggregate`` depending on
/// ``Aggregate`` depending on whether your function is a scalar function (returns /// whether the function computes a value from a single row or aggregates results across multiple
/// a single result) or an aggregate function (returns a result accumulated from multiple /// rows. Override the required properties and implement the necessary logic to define the
/// rows). The subclass will then override the necessary properties and methods to implement /// functions behavior.
/// the function's behavior.
/// ///
/// ## Topics /// ## Topics
/// ///
/// ### Base Function Classes /// ### Base Function Classes
/// ///
/// - ``Aggregate``
/// - ``Scalar`` /// - ``Scalar``
/// - ``Aggregate``
/// ///
/// ### Custom Function Classes /// ### Custom Function Classes
/// ///
/// - ``Regexp`` /// - ``Regexp``
///
/// ### Configuration
///
/// - ``argc``
/// - ``name``
/// - ``options``
/// - ``Options``
open class Function { open class Function {
// MARK: - Properties // MARK: - Properties
/// The number of arguments that the custom SQLite function accepts. /// The number of arguments that the function accepts.
/// ///
/// This property must be overridden by subclasses to specify how many arguments /// Subclasses must override this property to specify the expected number of arguments. The
/// the function expects. The value should be a positive integer representing the /// value should be a positive integer, or zero if the function does not accept any arguments.
/// number of arguments, or zero if the function does not accept arguments.
open class var argc: Int32 { open class var argc: Int32 {
fatalError("Subclasses must override this property to specify the number of arguments.") fatalError("Subclasses must override this property to specify the number of arguments.")
} }
/// The name of the custom SQLite function. /// The name of the function.
/// ///
/// This property must be overridden by subclasses to provide the name that the SQLite /// Subclasses must override this property to provide the name by which the SQLite engine
/// engine will use to identify the function. The name should be a valid SQLite function /// identifies the function. The name must comply with SQLite function naming rules.
/// name according to SQLite naming conventions.
open class var name: String { open class var name: String {
fatalError("Subclasses must override this property to provide the function name.") fatalError("Subclasses must override this property to provide the function name.")
} }
/// The options for the custom SQLite function. /// The configuration options for the function.
/// ///
/// This property must be overridden by subclasses to specify options such as whether the /// Subclasses must override this property to specify the functions behavioral flags, such as
/// function is deterministic or not. Options are represented as a bitmask of `Function.Options`. /// whether it is deterministic, direct-only, or innocuous.
open class var options: Options { open class var options: Options {
fatalError("Subclasses must override this property to specify function 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 { class var encoding: Function.Options {
Function.Options(rawValue: SQLITE_UTF8) 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 { class var opts: Int32 {
var options = options var options = options
options.insert(encoding) options.insert(encoding)
@@ -73,91 +69,35 @@ open class Function {
// MARK: - Methods // MARK: - Methods
/// Installs the custom SQLite function into the specified database connection. class func install(db connection: OpaquePointer) throws(SQLiteError) {
///
/// 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.") fatalError("Subclasses must override this method to implement function installation.")
} }
/// Uninstalls the custom SQLite function from the specified database connection. class func uninstall(db connection: OpaquePointer) throws(SQLiteError) {
///
/// 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( let status = sqlite3_create_function_v2(
connection, connection,
name, argc, opts, name, argc, opts,
nil, nil, nil, nil, nil nil, nil, nil, nil, nil
) )
if status != SQLITE_OK { if status != SQLITE_OK {
throw Connection.Error(connection) throw SQLiteError(connection)
} }
} }
} }
// MARK: - Functions // 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) { func sqlite3_result_text(_ ctx: OpaquePointer!, _ string: String) {
sqlite3_result_text(ctx, string, -1, SQLITE_TRANSIENT) 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) { func sqlite3_result_blob(_ ctx: OpaquePointer!, _ data: Data) {
data.withUnsafeBytes { data.withUnsafeBytes {
sqlite3_result_blob(ctx, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT) sqlite3_result_blob(ctx, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT)
} }
} }
/// Sets the result of an SQLite query based on the `SQLiteRawValue` type. func sqlite3_result_value(_ ctx: OpaquePointer!, _ value: SQLiteValue?) {
///
/// 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 { switch value ?? .null {
case .int(let value): sqlite3_result_int64(ctx, value) case .int(let value): sqlite3_result_int64(ctx, value)
case .real(let value): sqlite3_result_double(ctx, value) case .real(let value): sqlite3_result_double(ctx, value)

View File

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

View File

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

View File

@@ -1,687 +1,139 @@
import Foundation import Foundation
import DataLiteC import DataLiteC
/// A value representing a static destructor for SQLite. /// A prepared SQLite statement used to execute SQL commands.
/// ///
/// `SQLITE_STATIC` is used to indicate that the SQLite library should not free the associated /// `Statement` encapsulates the lifecycle of a compiled SQL statement, including parameter binding,
/// memory when the statement is finalized. /// execution, and result retrieval. The statement is finalized automatically when the instance is
let SQLITE_STATIC = unsafeBitCast( /// deallocated.
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 /// This class serves as a thin, type-safe wrapper over the SQLite C API, providing a Swift
/// associated memory and free the original memory when the statement is finalized. /// interface for managing prepared statements.
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 /// ## Topics
/// ///
/// ### Subtypes /// ### Statement Options
/// ///
/// - ``Options`` /// - ``Options``
/// - ``Arguments`` public final class Statement {
///
/// ### 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 // MARK: - Private Properties
/// The SQLite statement pointer associated with this `Statement` instance.
private let statement: OpaquePointer private let statement: OpaquePointer
/// The SQLite database connection pointer used to create this statement.
private let connection: OpaquePointer private let connection: OpaquePointer
// MARK: - Inits // MARK: - Inits
/// Initializes a new `Statement` instance with a given SQL query and options. init(
/// db connection: OpaquePointer,
/// This initializer prepares the SQL statement for execution and sets up any necessary sql query: String,
/// options. It throws an ``Connection/Error`` if the SQL preparation fails. options: Options
/// ) throws(SQLiteError) {
/// - 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 var statement: OpaquePointer! = nil
let status = sqlite3_prepare_v3(connection, query, -1, options.rawValue, &statement, nil) let status = sqlite3_prepare_v3(
connection, query, -1,
options.rawValue, &statement, nil
)
if status == SQLITE_OK, let statement { if status == SQLITE_OK, let statement {
self.statement = statement self.statement = statement
self.connection = connection self.connection = connection
} else { } else {
sqlite3_finalize(statement) sqlite3_finalize(statement)
throw Connection.Error(connection) throw SQLiteError(connection)
} }
} }
/// Finalizes the SQL statement, releasing any associated resources.
deinit { deinit {
sqlite3_finalize(statement) sqlite3_finalize(statement)
} }
}
// MARK: - Binding Parameters // MARK: - StatementProtocol
/// Returns the count of parameters that can be bound to this statement. extension Statement: StatementProtocol {
/// public func parameterCount() -> Int32 {
/// 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) sqlite3_bind_parameter_count(statement)
} }
/// Returns the index of a parameter by its name. public func parameterIndexBy(_ name: String) -> Int32 {
///
/// 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) sqlite3_bind_parameter_index(statement, name)
} }
/// Returns the name of a parameter by its index. public func parameterNameBy(_ index: Int32) -> String? {
/// sqlite3_bind_parameter_name(statement, 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. public func bind(_ value: SQLiteValue, at index: Int32) throws(SQLiteError) {
/// let status = switch value {
/// This method allows you to bind various types of values (integer, real, text, or blob) to a case .int(let value): sqlite3_bind_int64(statement, index, value)
/// parameter in the SQL statement. The appropriate SQLite function is called based on the type case .real(let value): sqlite3_bind_double(statement, index, value)
/// of value being bound. case .text(let value): sqlite3_bind_text(statement, index, value)
/// case .blob(let value): sqlite3_bind_blob(statement, index, value)
/// - Parameters: case .null: sqlite3_bind_null(statement, index)
/// - 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 { if status != SQLITE_OK {
throw Connection.Error(connection) throw SQLiteError(connection)
} }
} }
/// Binds a value conforming to `RawBindable` to a parameter at a specified index. public func clearBindings() throws(SQLiteError) {
///
/// 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 { if sqlite3_clear_bindings(statement) != SQLITE_OK {
throw Connection.Error(connection) throw SQLiteError(connection)
} }
} }
// MARK: - Retrieving Results @discardableResult
public func step() throws(SQLiteError) -> Bool {
switch sqlite3_step(statement) {
case SQLITE_ROW: true
case SQLITE_DONE: false
default: throw SQLiteError(connection)
}
}
public func reset() throws(SQLiteError) {
if sqlite3_reset(statement) != SQLITE_OK {
throw SQLiteError(connection)
}
}
/// Returns the number of columns in the result set.
///
/// This method provides the count of columns returned by the SQL statement result, which is
/// useful for iterating over query results and processing data.
///
/// - Returns: The number of columns in the result set.
public func columnCount() -> Int32 { public func columnCount() -> Int32 {
sqlite3_column_count(statement) sqlite3_column_count(statement)
} }
/// Returns the type of data stored in a column at a specified index. public func columnName(at index: Int32) -> String? {
/// sqlite3_column_name(statement, 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. public func columnValue(at index: Int32) -> SQLiteValue {
/// switch sqlite3_column_type(statement, index) {
/// This method retrieves the name of a column, which is useful for debugging or when you need case SQLITE_INTEGER: .int(sqlite3_column_int64(statement, index))
/// to work with column names directly. case SQLITE_FLOAT: .real(sqlite3_column_double(statement, index))
/// case SQLITE_TEXT: .text(sqlite3_column_text(statement, index))
/// - Parameters: case SQLITE_BLOB: .blob(sqlite3_column_blob(statement, index))
/// - index: The index of the column (0-based). default: .null
/// - 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 // MARK: - Constants
let SQLITE_STATIC = unsafeBitCast(
OpaquePointer(bitPattern: 0),
to: sqlite3_destructor_type.self
)
let SQLITE_TRANSIENT = unsafeBitCast(
OpaquePointer(bitPattern: -1),
to: sqlite3_destructor_type.self
)
// MARK: - Private Sunctions
private func sqlite3_bind_parameter_name(_ stmt: OpaquePointer!, _ index: Int32) -> String? {
guard let cString = DataLiteC.sqlite3_bind_parameter_name(stmt, index) else { return nil }
return String(cString: cString)
}
private func sqlite3_bind_text(_ stmt: OpaquePointer!, _ index: Int32, _ string: String) -> Int32 { private func sqlite3_bind_text(_ stmt: OpaquePointer!, _ index: Int32, _ string: String) -> Int32 {
sqlite3_bind_text(stmt, index, string, -1, SQLITE_TRANSIENT) sqlite3_bind_text(stmt, index, string, -1, SQLITE_TRANSIENT)
@@ -693,6 +145,13 @@ private func sqlite3_bind_blob(_ stmt: OpaquePointer!, _ index: Int32, _ data: D
} }
} }
private func sqlite3_column_name(_ stmt: OpaquePointer!, _ iCol: Int32) -> String? {
guard let cString = DataLiteC.sqlite3_column_name(stmt, iCol) else {
return nil
}
return String(cString: cString)
}
private func sqlite3_column_text(_ stmt: OpaquePointer!, _ iCol: Int32) -> String { private func sqlite3_column_text(_ stmt: OpaquePointer!, _ iCol: Int32) -> String {
String(cString: DataLiteC.sqlite3_column_text(stmt, iCol)) String(cString: DataLiteC.sqlite3_column_text(stmt, iCol))
} }

View File

@@ -0,0 +1,144 @@
# Extending SQLite with Custom Functions
Build custom scalar and aggregate SQL functions that run inside SQLite.
DataLiteCore lets you register custom SQL functions that participate in expressions, queries, and
aggregations just like built-in ones. Each function is registered on a specific connection and
becomes available to all statements executed through that connection.
## Registering Functions
Use ``ConnectionProtocol/add(function:)`` to register a function type on a connection. Pass the
functions type, not an instance. DataLiteCore automatically manages function creation and
lifecycle — scalar functions are executed via their type, while aggregate functions are instantiated
per SQL invocation.
```swift
try connection.add(function: Function.Regexp.self) // Built-in helper
try connection.add(function: Slugify.self) // Custom scalar function
```
To remove a registered function, call ``ConnectionProtocol/remove(function:)``. This is useful for
dynamic plug-ins or test environments that require a clean registration state.
```swift
try connection.remove(function: Slugify.self)
```
## Implementing Scalar Functions
Subclass ``Function/Scalar`` to define a function that returns a single value for each call.
Override the static metadata properties — ``Function/name``, ``Function/argc``, and
``Function/options`` — to declare the functions signature, and implement its logic in
``Function/Scalar/invoke(args:)``. Return any type conforming to ``SQLiteRepresentable``.
```swift
final class Slugify: Function.Scalar {
override class var name: String { "slugify" }
override class var argc: Int32 { 1 }
override class var options: Function.Options { [.deterministic, .innocuous] }
override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
guard let value = args[0] as String?, !value.isEmpty else { return nil }
return value.lowercased()
.replacingOccurrences(of: "\\W+", with: "-", options: .regularExpression)
.trimmingCharacters(in: .init(charactersIn: "-"))
}
}
try connection.add(function: Slugify.self)
let rows = try connection.prepare(sql: "SELECT slugify(title) FROM articles")
```
## Implementing Aggregate Functions
Aggregate functions maintain internal state across multiple rows. Subclass ``Function/Aggregate``
and override ``Function/Aggregate/step(args:)`` to process each row and
``Function/Aggregate/finalize()`` to produce the final result.
```swift
final class Median: Function.Aggregate {
private var values: [Double] = []
override class var name: String { "median" }
override class var argc: Int32 { 1 }
override class var options: Function.Options { [.deterministic] }
override func step(args: any ArgumentsProtocol) throws {
if let value = args[0] as Double? {
values.append(value)
}
}
override func finalize() throws -> SQLiteRepresentable? {
guard !values.isEmpty else { return nil }
let sorted = values.sorted()
let mid = sorted.count / 2
return sorted.count.isMultiple(of: 2)
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid]
}
}
try connection.add(function: Median.self)
```
SQLite creates a new instance of an aggregate function for each aggregate expression in a query and
reuses it for all rows contributing to that result. Its safe to store mutable state in instance
properties.
## Handling Arguments and Results
Custom functions receive input through an ``ArgumentsProtocol`` instance. Use subscripts to access
arguments by index and automatically convert them to Swift types.
Two access forms are available:
- `subscript(index: Index) -> SQLiteValue` — returns the raw SQLite value without conversion.
- `subscript<T: SQLiteRepresentable>(index: Index) -> T?`— converts the value to a Swift type
conforming to ``SQLiteRepresentable``. Returns `nil` if the argument is `NULL` or cannot be
converted.
Use ``Function/Arguments/count`` to verify argument count before accessing elements. For
fine-grained decoding control, prefer the raw ``SQLiteValue`` form and handle conversion manually.
```swift
override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
guard args.count == 2 else {
throw SQLiteError(code: SQLITE_MISUSE, message: "expected two arguments")
}
guard let lhs = args[0] as Double?, let rhs = args[1] as Double? else {
return nil // returns SQL NULL if either argument is NULL
}
return lhs * rhs
}
```
Any type conforming to ``SQLiteRepresentable`` can be used both to read arguments and to return
results. Returning `nil` produces an SQL `NULL`.
## Choosing Function Options
Customize function characteristics via the ``Function/Options`` bitset:
- ``Function/Options/deterministic`` — identical arguments always yield the same result, enabling
SQLite to cache calls and optimize query plans.
- ``Function/Options/directonly`` — restricts usage to trusted contexts (for example, disallows
calls from triggers or CHECK constraints).
- ``Function/Options/innocuous`` — marks the function as side-effect-free and safe for untrusted
SQL.
Each scalar or aggregate subclass may return a different option set, depending on its behavior.
## Error Handling
Throwing from ``Function/Scalar/invoke(args:)``, ``Function/Aggregate/step(args:)``, or
``Function/Aggregate/finalize()`` propagates an error back to SQLite. DataLiteCore converts the
thrown error into a generic `SQLITE_ERROR` result code and uses its `localizedDescription` as the
message text.
You can use this mechanism to signal both validation failures and runtime exceptions during function
execution. Throwing an error stops evaluation immediately and returns control to SQLite.
- SeeAlso: ``Function``
- SeeAlso: [Application-Defined SQL Functions](https://sqlite.org/appfunc.html)

View File

@@ -0,0 +1,61 @@
# Database Encryption
Secure SQLite databases with SQLCipher encryption using DataLiteCore.
DataLiteCore provides a clean API for applying and rotating SQLCipher encryption keys through the
connection interface. You can use it to unlock existing encrypted databases or to initialize new
ones securely before executing SQL statements.
## Applying an Encryption Key
Use ``ConnectionProtocol/apply(_:name:)`` to unlock an encrypted database file or to initialize
encryption on a new one. Supported key formats include:
- ``Connection/Key/passphrase(_:)`` — a textual passphrase processed by SQLCiphers key derivation.
- ``Connection/Key/rawKey(_:)`` — a 256-bit (`32`-byte) key supplied as `Data`.
```swift
let connection = try Connection(
location: .file(path: "/path/to/sqlite.db"),
options: [.readwrite, .create, .fullmutex]
)
try connection.apply(.passphrase("vault-password"), name: nil)
```
The first call on a new database establishes encryption. If the database already exists and is
encrypted, the same call unlocks it for the current session. Plaintext files cannot be encrypted in
place. Always call ``ConnectionProtocol/apply(_:name:)`` immediately after opening the connection
and before executing any statements to avoid `SQLITE_NOTADB` errors.
## Rotating Keys
Use ``ConnectionProtocol/rekey(_:name:)`` to rewrite the database with a new key. The connection
must already be unlocked with the current key via ``ConnectionProtocol/apply(_:name:)``.
```swift
let newKey = Data((0..<32).map { _ in UInt8.random(in: 0...UInt8.max) })
try connection.rekey(.rawKey(newKey), name: nil)
```
Rekeying touches every page in the database and can take noticeable time on large files. Schedule
it during maintenance windows and be prepared for `SQLITE_BUSY` if other connections keep the file
locked. Adjust ``ConnectionProtocol/busyTimeout`` or coordinate access with application-level
locking.
## Attached Databases
When attaching additional databases, pass the attachment alias through the `name` parameter.
Use `nil` or `"main"` for the primary database, `"temp"` for the temporary one, and the alias for
others.
```swift
try connection.execute(raw: "ATTACH DATABASE 'analytics.db' AS analytics")
try connection.apply(.passphrase("aux-password"), name: "analytics")
```
All databases attached to the same connection must follow a consistent encryption policy. If an
attached database must remain unencrypted, attach it using a separate connection instead.
- SeeAlso: ``ConnectionProtocol/apply(_:name:)``
- SeeAlso: ``ConnectionProtocol/rekey(_:name:)``
- SeeAlso: [SQLCipher Documentation](https://www.zetetic.net/sqlcipher/documentation/)

View File

@@ -0,0 +1,66 @@
# Handling SQLite Errors
Handle SQLite errors predictably with DataLiteCore.
DataLiteCore converts all SQLite failures into an ``SQLiteError`` structure that contains both the
extended result code and a descriptive message. This unified error model lets you accurately
distinguish between constraint violations, locking issues, and other failure categories — while
preserving full diagnostic information for recovery and logging.
## SQLiteError Breakdown
``SQLiteError`` exposes fields that help with diagnostics and recovery:
- ``SQLiteError/code`` — the extended SQLite result code (for example,
`SQLITE_CONSTRAINT_FOREIGNKEY` or `SQLITE_BUSY_TIMEOUT`). Use it for programmatic
branching — e.g., retry logic, rollbacks, or user-facing messages.
- ``SQLiteError/message`` — a textual description of the underlying SQLite failure.
Since ``SQLiteError`` conforms to `CustomStringConvertible`, you can log it directly. For
user-facing alerts, derive your own localized messages from the error code instead of exposing
SQLite messages verbatim.
## Typed Throws
Most DataLiteCore APIs are annotated as `throws(SQLiteError)`, meaning they only throw SQLiteError
instances.
Only APIs that touch the file system or execute arbitrary user code may throw other error
types — for example, ``Connection/init(location:options:)`` when creating directories for on-disk
databases.
```swift
do {
try connection.execute(raw: """
INSERT INTO users(email) VALUES ('ada@example.com')
""")
} catch {
switch error.code {
case SQLITE_CONSTRAINT:
showAlert("A user with this email already exists.")
case SQLITE_BUSY, SQLITE_LOCKED:
retryLater()
default:
print("Unexpected error: \(error.message)")
}
}
```
## Multi-Statement Scenarios
- ``ConnectionProtocol/execute(sql:)`` and ``ConnectionProtocol/execute(raw:)`` stop at the first
failing statement and propagate its ``SQLiteError``.
- ``StatementProtocol/execute(_:)`` reuses prepared statements; inside `catch` blocks, remember to
call ``StatementProtocol/reset()`` and (if needed) ``StatementProtocol/clearBindings()`` before
retrying.
- When executing multiple statements, add your own logging if you need to know which one
failed — the propagated ``SQLiteError`` reflects SQLites diagnostics only.
## Custom Functions
Errors thrown from ``Function/Scalar`` or ``Function/Aggregate`` implementations are reported back
to SQLite as `SQLITE_ERROR`, with the errors `localizedDescription` as the message text.
Define clear, domain-specific error types to make SQL traces and logs more meaningful.
- SeeAlso: ``SQLiteError``
- SeeAlso: [SQLite Result Codes](https://sqlite.org/rescode.html)

View File

@@ -0,0 +1,94 @@
# Multithreading Strategies
Coordinate SQLite safely across queues, actors, and Swift concurrency using DataLiteCore.
SQLite remains fundamentally serialized, so deliberate connection ownership and scheduling are
essential for correctness and performance. DataLiteCore does not include a built-in connection
pool, but its deterministic behavior and configuration options allow you to design synchronization
strategies that match your workload.
## Core Guidelines
- **One connection per queue or actor**. Keep each ``Connection`` confined to a dedicated serial
`DispatchQueue` or an `actor` to ensure ordered execution and predictable statement lifecycles.
- **Do not share statements across threads**. ``Statement`` instances are bound to their parent
``Connection`` and are not thread-safe.
- **Scale with multiple connections**. For concurrent workloads, use a dedicated writer connection
alongside a pool of readers so long-running transactions dont block unrelated operations.
```swift
actor Database {
private let connection: Connection
init(path: String) throws {
connection = try Connection(
path: path,
options: [.readwrite, .create, .fullmutex]
)
connection.busyTimeout = 5_000 // wait up to 5 seconds for locks
}
func insertUser(name: String) throws {
let statement = try connection.prepare(
sql: "INSERT INTO users(name) VALUES (?)"
)
try statement.bind(name, at: 1)
try statement.step()
}
}
```
Encapsulating database work in an `actor` or serial queue aligns naturally with Swift Concurrency
while maintaining safe access to SQLites synchronous API.
## Synchronization Options
- ``Connection/Options/nomutex`` — disables SQLites internal mutexes (multi-thread mode). Each
connection must be accessed by only one thread at a time.
```swift
let connection = try Connection(
location: .file(path: "/path/to/sqlite.db"),
options: [.readwrite, .nomutex]
)
```
- ``Connection/Options/fullmutex`` — enables serialized mode with full internal locking. A single
``Connection`` may be shared across threads, but global locks reduce throughput.
```swift
let connection = try Connection(
location: .file(path: "/path/to/sqlite.db"),
options: [.readwrite, .fullmutex]
)
```
SQLite defaults to serialized mode, but concurrent writers still contend for locks. Plan long
transactions carefully and adjust ``ConnectionProtocol/busyTimeout`` to handle `SQLITE_BUSY`
conditions gracefully.
- SeeAlso: [Using SQLite In Multi-Threaded Applications](https://sqlite.org/threadsafe.html)
## Delegates and Side Effects
``ConnectionDelegate`` and ``ConnectionTraceDelegate`` callbacks execute synchronously on SQLites
internal thread. Keep them lightweight and non-blocking. Offload work to another queue when
necessary to prevent deadlocks or extended lock holds.
```swift
final class Logger: ConnectionTraceDelegate {
private let queue = DispatchQueue(label: "logging")
func connection(
_ connection: ConnectionProtocol,
trace sql: ConnectionTraceDelegate.Trace
) {
queue.async {
print("SQL:", sql.expandedSQL)
}
}
}
```
This pattern keeps tracing responsive and prevents SQLites internal thread from being blocked by
slow I/O or external operations.

View File

@@ -0,0 +1,192 @@
# Mastering Prepared Statements
Execute SQL efficiently and safely with reusable prepared statements in DataLiteCore.
Prepared statements allow you to compile SQL once, bind parameters efficiently, and execute it
multiple times without re-parsing or re-planning. The ``Statement`` type in DataLiteCore is a thin,
type-safe wrapper around SQLites C API that manages the entire lifecycle of a compiled SQL
statement — from preparation and parameter binding to execution and result retrieval. Statements are
automatically finalized when no longer referenced, ensuring predictable resource cleanup.
## Preparing Statements
Use ``ConnectionProtocol/prepare(sql:)`` or ``ConnectionProtocol/prepare(sql:options:)`` to create
a compiled ``StatementProtocol`` instance ready for parameter binding and execution.
The optional ``Statement/Options`` control how the database engine optimizes the compilation and
reuse of a prepared statement. For example, ``Statement/Options/persistent`` marks the statement as
suitable for long-term reuse, allowing the engine to optimize memory allocation for statements
expected to remain active across multiple executions. ``Statement/Options/noVtab`` restricts the use
of virtual tables during preparation, preventing them from being referenced at compile time.
```swift
let statement = try connection.prepare(
sql: """
SELECT id, email
FROM users
WHERE status = :status
AND updated_at >= ?
""",
options: [.persistent]
)
```
Implementations of ``StatementProtocol`` are responsible for managing the lifetime of their
underlying database resources. The default ``Statement`` type provided by DataLiteCore automatically
finalizes statements when their last strong reference is released, ensuring deterministic cleanup
through Swifts memory management.
## Managing the Lifecycle
Use ``StatementProtocol/reset()`` to return a statement to its initial state so that it can be
executed again. Call ``StatementProtocol/clearBindings()`` to remove all previously bound parameter
values, allowing the same prepared statement to be reused with completely new input data. Always
use these methods before reusing a prepared statement.
```swift
try statement.reset()
try statement.clearBindings()
```
## Binding Parameters
You can bind either raw ``SQLiteValue`` values or any Swift type that conforms to ``SQLiteBindable``
or ``SQLiteRepresentable``. Parameter placeholders in the SQL statement are assigned numeric indexes
in the order they appear, starting from `1`.
To inspect or debug parameter mappings, use ``StatementProtocol/parameterCount()`` to check the
total number of parameters, or ``StatementProtocol/parameterNameBy(_:)`` to retrieve the name of a
specific placeholder by its index.
### Binding by Index
Use positional placeholders to bind parameters by numeric index. A simple `?` placeholder is
automatically assigned the next available index in the order it appears, starting from `1`.
A numbered placeholder (`?NNN`) explicitly defines its own index within the statement, letting you
bind parameters out of order if needed.
```swift
let insertLog = try connection.prepare(sql: """
INSERT INTO logs(level, message, created_at)
VALUES (?, ?, ?)
""")
try insertLog.bind("info", at: 1)
try insertLog.bind("Cache warmed", at: 2)
try insertLog.bind(Date(), at: 3)
try insertLog.step() // executes the INSERT
try insertLog.reset()
```
### Binding by Name
Named placeholders (`:name`, `@name`, `$name`) improve readability and allow the same parameter to
appear multiple times within a statement. When binding, pass the full placeholder token — including
its prefix — to the ``StatementProtocol/bind(_:by:)-(SQLiteValue,_)`` method.
```swift
let usersByStatus = try connection.prepare(sql: """
SELECT id, email
FROM users
WHERE status = :status
AND email LIKE :pattern
""")
try usersByStatus.bind("active", by: ":status")
try usersByStatus.bind("%@example.com", by: ":pattern")
```
If you need to inspect the numeric index associated with a named parameter, use
``StatementProtocol/parameterIndexBy(_:)``. This can be useful for diagnostics, logging, or
integrating with utility layers that operate by index.
### Reusing Parameters
When the same named placeholder appears multiple times in a statement, SQLite internally assigns all
of them to a single binding slot. This means you only need to set the value once, and it will be
applied everywhere that placeholder occurs.
```swift
let sales = try connection.prepare(sql: """
SELECT id
FROM orders
WHERE customer_id = :client
OR created_by = :client
LIMIT :limit
""")
try sales.bind(42, by: ":client") // used for both conditions
try sales.bind(50, by: ":limit")
```
### Mixing Placeholders
You can freely combine named and positional placeholders within the same statement. SQLite assigns
numeric indexes to all placeholders in the order they appear, regardless of whether they are named
or positional. To keep bindings predictable, its best to follow a consistent style within each
statement.
```swift
let search = try connection.prepare(sql: """
SELECT id, title
FROM articles
WHERE category_id IN (?, ?, ?)
AND published_at >= :since
""")
try search.bind(3, at: 1)
try search.bind(5, at: 2)
try search.bind(8, at: 3)
try search.bind(Date(timeIntervalSinceNow: -7 * 24 * 60 * 60), by: ":since")
```
## Executing Statements
Advance execution with ``StatementProtocol/step()``. This method returns `true` while rows are
available, and `false` when the statement is fully consumed — or immediately, for statements that
do not produce results.
Always reset a statement before re-executing it; otherwise, the database engine will report a misuse
error.
```swift
var rows: [SQLiteRow] = []
while try usersByStatus.step() {
if let row = usersByStatus.currentRow() {
rows.append(row)
}
}
try usersByStatus.reset()
try usersByStatus.clearBindings()
```
For bulk operations, use ``StatementProtocol/execute(_:)``. It accepts an array of ``SQLiteRow``
values and automatically performs binding, stepping, clearing, and resetting in a loop — making it
convenient for batch inserts or updates.
## Fetching Result Data
Use ``StatementProtocol/columnCount()`` and ``StatementProtocol/columnName(at:)`` to inspect the
structure of the result set. Retrieve individual column values with
``StatementProtocol/columnValue(at:)->SQLiteValue`` — either as a raw ``SQLiteValue`` or as a typed
value conforming to ``SQLiteRepresentable``. Alternatively, use ``StatementProtocol/currentRow()``
to obtain the full set of column values for the current result row.
```swift
while try statement.step() {
guard let identifier: Int64 = statement.columnValue(at: 0),
let email: String = statement.columnValue(at: 1)
else { continue }
print("User \(identifier): \(email)")
}
try statement.reset()
```
Each row returned by `currentRow()` is an independent copy of the current result data. You can
safely store it, transform it into a domain model, or reuse its values as parameters in subsequent
statements through ``StatementProtocol/bind(_:)``.
- SeeAlso: ``StatementProtocol``
- SeeAlso: ``Statement``
- SeeAlso: [SQLite Prepared Statements](https://sqlite.org/c3ref/stmt.html)

View File

@@ -0,0 +1,98 @@
# Running SQL Scripts
Execute and automate SQL migrations, seed data, and test fixtures with DataLiteCore.
``SQLScript`` and ``ConnectionProtocol/execute(sql:)`` let you run sequences of prepared statements
as a single script. The loader splits content by semicolons, removes comments and whitespace, and
compiles each statement individually. Scripts run in autocommit mode by default — execution stops at
the first failure and throws the corresponding ``SQLiteError``.
## Building Scripts
Create a script inline or load it from a bundled resource. ``SQLScript`` automatically strips
comments and normalizes whitespace.
```swift
let script = SQLScript(string: """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE
);
INSERT INTO users (username) VALUES ('ada'), ('grace');
""")
```
Load a script from your module or app bundle using ``SQLScript/init(byResource:extension:in:)``:
```swift
let bootstrap = try? SQLScript(
byResource: "bootstrap",
extension: "sql",
in: .module
)
```
## Executing Scripts
Run the script through ``ConnectionProtocol/execute(sql:)``. Statements execute sequentially in the
order they appear.
```swift
let connection = try Connection(
location: .file(path: dbPath),
options: [.readwrite, .create]
)
try connection.execute(sql: script)
```
In autocommit mode, each statement commits as soon as it succeeds. If any statement fails, execution
stops and previously executed statements remain committed. To ensure all-or-nothing execution, wrap
the script in an explicit transaction:
```swift
try connection.beginTransaction(.immediate)
do {
try connection.execute(sql: script)
try connection.commitTransaction()
} catch {
try? connection.rollbackTransaction()
throw error
}
```
- Important: SQLScript must not include BEGIN, COMMIT, or ROLLBACK. Always manage transactions at
the connection level.
## Executing Raw SQL
Use ``ConnectionProtocol/execute(raw:)`` to run multi-statement SQL directly, without parsing or
preprocessing. This method executes the script exactly as provided, allowing you to manage
transactions explicitly within the SQL itself.
```swift
let migrations = """
BEGIN;
CREATE TABLE categories (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE
);
INSERT INTO categories (name) VALUES ('Swift'), ('SQLite');
COMMIT;
"""
try connection.execute(raw: migrations)
```
Each statement runs in sequence until completion or until the first error occurs. If a statement
fails, execution stops and remaining statements are skipped. Open transactions are not rolled back
automatically — they must be handled explicitly inside the script or by the caller.
## Handling Errors
Inspect the thrown ``SQLiteError`` to identify the failing statements result code and message. For
longer scripts, wrap execution in logging to trace progress and isolate the exact statement that
triggered the exception.
- SeeAlso: ``SQLScript``
- SeeAlso: ``ConnectionProtocol/execute(sql:)``
- SeeAlso: ``ConnectionProtocol/execute(raw:)``

View File

@@ -0,0 +1,142 @@
# Working with SQLiteRow
Represent SQL rows and parameters with SQLiteRow.
``SQLiteRow`` is an ordered container for column/value pairs. It preserves insertion order—matching
the schema when representing result sets—and provides helpers for column names, named parameters,
and literal rendering.
## Creating Rows
Initialize a row with a dictionary literal or assign values incrementally through subscripting.
Values can be ``SQLiteValue`` instances or any type convertible via ``SQLiteRepresentable``.
```swift
var payload: SQLiteRow = [
"username": .text("ada"),
"email": "ada@example.com".sqliteValue,
"is_admin": false.sqliteValue
]
payload["last_login_at"] = Int64(Date().timeIntervalSince1970).sqliteValue
```
``SQLiteRow/columns`` returns the ordered column names, and ``SQLiteRow/namedParameters`` provides
matching tokens (prefixed with `:`) suitable for parameterized SQL.
```swift
print(payload.columns) // ["username", "email", "is_admin", "last_login_at"]
print(payload.namedParameters) // [":username", ":email", ":is_admin", ":last_login_at"]
```
## Generating SQL Fragments
Use row metadata to build SQL snippets without manual string concatenation:
```swift
let columns = payload.columns.joined(separator: ", ")
let placeholders = payload.namedParameters.joined(separator: ", ")
let assignments = zip(payload.columns, payload.namedParameters)
.map { "\($0) = \($1)" }
.joined(separator: ", ")
// columns -> "username, email, is_admin, last_login_at"
// placeholders -> ":username, :email, :is_admin, :last_login_at"
// assignments -> "username = :username, ..."
```
When generating migrations or inserting literal values, ``SQLiteValue/sqliteLiteral`` renders safe
SQL fragments for numeric and text values. Always escape identifiers manually if column names come
from untrusted input.
## Inserting Rows
Bind an entire row to a statement using ``StatementProtocol/bind(_:)``. The method matches column
names to identically named placeholders.
```swift
var user: SQLiteRow = [
"username": .text("ada"),
"email": .text("ada@example.com"),
"created_at": Int64(Date().timeIntervalSince1970).sqliteValue
]
let insertSQL = """
INSERT INTO users (\(user.columns.joined(separator: ", ")))
VALUES (\(user.namedParameters.joined(separator: ", ")))
"""
let insert = try connection.prepare(sql: insertSQL)
try insert.bind(user)
try insert.step()
try insert.reset()
```
To insert multiple rows, prepare an array of ``SQLiteRow`` values and call
``StatementProtocol/execute(_:)``. The helper performs binding, stepping, and clearing for each row:
```swift
let batch: [SQLiteRow] = [
["username": .text("ada"), "email": .text("ada@example.com")],
["username": .text("grace"), "email": .text("grace@example.com")]
]
try insert.execute(batch)
```
## Updating Rows
Because ``SQLiteRow`` is a value type, you can duplicate and extend it for related operations such
as building `SET` clauses or constructing `WHERE` conditions.
```swift
var changes: SQLiteRow = [
"email": .text("ada@new.example"),
"last_login_at": Int64(Date().timeIntervalSince1970).sqliteValue
]
let setClause = zip(changes.columns, changes.namedParameters)
.map { "\($0) = \($1)" }
.joined(separator: ", ")
var parameters = changes
parameters["id"] = .int(1)
let update = try connection.prepare(sql: """
UPDATE users
SET \(setClause)
WHERE id = :id
""")
try update.bind(parameters)
try update.step()
```
## Reading Rows
``StatementProtocol/currentRow()`` returns an ``SQLiteRow`` snapshot of the current result. Use it
to pass data through mapping layers or transform results lazily without immediate conversion:
```swift
let statement = try connection.prepare(sql: "SELECT id, email FROM users LIMIT 10")
var rows: [SQLiteRow] = []
while try statement.step() {
if let row = statement.currentRow() {
rows.append(row)
}
}
```
You can iterate over a rows columns via `columns`, and subscript by name to retrieve stored values.
For typed access, cast through ``SQLiteValue`` or adopt ``SQLiteRepresentable`` in your custom
types.
## Diagnostics
Use ``SQLiteRow/description`` to log payloads during development. For security-sensitive logs,
redact or whitelist keys before printing. Because rows preserve order, logs mirror the schema
defined in your SQL, making comparisons straightforward.
- SeeAlso: ``SQLiteRow``
- SeeAlso: ``StatementProtocol``

View File

@@ -0,0 +1,188 @@
# Working with Connections
Open, configure, monitor, and transact with SQLite connections using DataLiteCore.
Establishing and configuring a ``Connection`` is the first step before executing SQL statements
with **DataLiteCore**. A connection wraps the underlying SQLite handle, exposes ergonomic Swift
APIs, and provides hooks for observing database activity.
## Opening a Connection
Create a connection with ``Connection/init(location:options:)``. The initializer opens (or creates)
the target database file and registers lifecycle hooks that enable tracing, update notifications,
and transaction callbacks.
Call ``ConnectionProtocol/initialize()`` once during application start-up when you need to ensure
the SQLite core has been initialized manually—for example, when linking SQLite dynamically or when
the surrounding framework does not do it on your behalf. Pair it with
``ConnectionProtocol/shutdown()`` during application tear-down if you require full control of
SQLite's global state.
```swift
import DataLiteCore
do {
try Connection.initialize()
let connection = try Connection(
location: .file(path: "/path/to/sqlite.db"),
options: [.readwrite, .create]
)
// Execute SQL or configure PRAGMAs
} catch {
print("Failed to open database: \(error)")
}
```
### Choosing a Location
Pick the database storage strategy from the ``Connection/Location`` enumeration:
- ``Connection/Location/file(path:)`` — a persistent on-disk file or URI backed database.
- ``Connection/Location/inMemory`` — a pure in-memory database that disappears once the connection
closes.
- ``Connection/Location/temporary`` — a transient on-disk file that SQLite removes when the session
ends.
### Selecting Options
Control how the connection is opened with ``Connection/Options``. Combine flags to describe the
required access mode, locking policy, and URI behavior.
```swift
let connection = try Connection(
location: .inMemory,
options: [.readwrite, .nomutex]
)
```
Common combinations include:
- ``Connection/Options/readwrite`` + ``Connection/Options/create`` — read/write access that creates
the file if missing.
- ``Connection/Options/readonly`` — read-only access preventing accidental writes.
- ``Connection/Options/fullmutex`` — enables serialized mode for multi-threaded access.
- ``Connection/Options/uri`` — allows SQLite URI parameters, such as query string pragmas.
- SeeAlso: [Opening A New Database Connection](https://sqlite.org/c3ref/open.html)
- SeeAlso: [In-Memory Databases](https://sqlite.org/inmemorydb.html)
## Closing a Connection
``Connection`` automatically closes the underlying SQLite handle when the instance is deallocated.
This ensures resources are released even when the object leaves scope unexpectedly. For long-lived
applications, prefer explicit lifecycle management—store the connection in a dedicated component
and release it deterministically when you are done to avoid keeping file locks or WAL checkpoints
around unnecessarily.
If your application called ``ConnectionProtocol/shutdown()`` to clean up global state, make sure all
connections have been released before invoking it.
## Managing Transactions
Manage transactional work with ``ConnectionProtocol/beginTransaction(_:)``,
``ConnectionProtocol/commitTransaction()``, and ``ConnectionProtocol/rollbackTransaction()``. When
you do not start a transaction explicitly, SQLite runs in autocommit mode and executes each
statement in its own transaction.
```swift
do {
try connection.beginTransaction(.immediate)
try connection.execute(raw: "INSERT INTO users (name) VALUES ('Ada')")
try connection.execute(raw: "INSERT INTO users (name) VALUES ('Grace')")
try connection.commitTransaction()
} catch {
try? connection.rollbackTransaction()
throw error
}
```
``TransactionType`` controls when SQLite acquires locks:
- ``TransactionType/deferred`` — defers locking until the first read or write; this is the default.
- ``TransactionType/immediate`` — immediately takes a RESERVED lock to prevent other writers.
- ``TransactionType/exclusive`` — escalates to an EXCLUSIVE lock and, in `DELETE` journal mode,
blocks readers.
``ConnectionProtocol/beginTransaction(_:)`` uses `.deferred` by default. When
``ConnectionProtocol/isAutocommit`` returns `false`, a transaction is already active. Calling
`beginTransaction` again raises an error, so guard composite operations accordingly.
- SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html)
## PRAGMA Parameters
Most frequently used PRAGMA directives are modeled as direct properties on ``ConnectionProtocol``:
``ConnectionProtocol/busyTimeout``, ``ConnectionProtocol/applicationID``,
``ConnectionProtocol/foreignKeys``, ``ConnectionProtocol/journalMode``,
``ConnectionProtocol/synchronous``, and ``ConnectionProtocol/userVersion``. Update them directly on
an active connection:
```swift
connection.userVersion = 2024
connection.foreignKeys = true
connection.journalMode = .wal
```
### Custom PRAGMAs
Use ``ConnectionProtocol/get(pragma:)`` and ``ConnectionProtocol/set(pragma:value:)`` for PRAGMAs
that do not have a dedicated API. They accept ``Pragma`` values (string literal expressible) and
any type that conforms to ``SQLiteRepresentable``. `set` composes a `PRAGMA <name> = <value>`
statement, while `get` issues `PRAGMA <name>`.
```swift
// Read the current cache_size value
let cacheSize: Int32? = try connection.get(pragma: "cache_size")
// Enable WAL journaling and adjust the sync mode
try connection.set(pragma: .journalMode, value: JournalMode.wal)
try connection.set(pragma: .synchronous, value: Synchronous.normal)
```
The `value` parameter automatically converts to ``SQLiteValue`` through ``SQLiteRepresentable``,
so you can pass `Bool`, `Int`, `String`, `Synchronous`, `JournalMode`, or a custom type that
supports the protocol.
- SeeAlso: [PRAGMA Statements](https://sqlite.org/pragma.html)
## Observing Connection Events
``ConnectionDelegate`` lets you observe connection-level events such as row updates, commits, and
rollbacks. Register a delegate with ``ConnectionProtocol/add(delegate:)``. Delegates are stored
weakly, so you are responsible for managing their lifetime. Remove a delegate with
``ConnectionProtocol/remove(delegate:)`` when it is no longer required.
Use ``ConnectionTraceDelegate`` to receive SQL statement traces and register it with
``ConnectionProtocol/add(trace:)``. Trace delegates are also held weakly.
```swift
final class QueryLogger: ConnectionDelegate, ConnectionTraceDelegate {
func connection(_ connection: ConnectionProtocol, trace sql: ConnectionTraceDelegate.Trace) {
print("SQL:", sql.expandedSQL)
}
func connection(_ connection: ConnectionProtocol, didUpdate action: SQLiteAction) {
print("Change:", action)
}
func connectionWillCommit(_ connection: ConnectionProtocol) throws {
try validatePendingOperations()
}
func connectionDidRollback(_ connection: ConnectionProtocol) {
resetInMemoryCache()
}
}
let logger = QueryLogger()
connection.add(delegate: logger)
connection.add(trace: logger)
// ...
connection.remove(trace: logger)
connection.remove(delegate: logger)
```
All callbacks execute synchronously on SQLite's internal thread. Keep delegate logic lightweight,
avoid blocking I/O, and hand heavy work off to other queues when necessary to preserve responsiveness.

View File

@@ -1,30 +0,0 @@
# ``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

@@ -1,31 +0,0 @@
# ``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

@@ -1,298 +0,0 @@
# ``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:)``
### Connection State
- ``isAutocommit``
- ``isReadonly``
- ``busyTimeout``
### PRAGMA Accessors
- ``applicationID``
- ``foreignKeys``
- ``journalMode``
- ``synchronous``
- ``userVersion``
### Delegation
- ``addDelegate(_:)``
- ``removeDelegate(_:)``
### 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

@@ -2,6 +2,19 @@
**DataLiteCore** is an intuitive library for working with SQLite in Swift applications. **DataLiteCore** is an intuitive library for working with SQLite in Swift applications.
## Overview **DataLiteCore** provides an object-oriented API on top of the C interface, making it simple to
integrate SQLite capabilities into your projects. The library combines powerful database
management and SQL execution features with the ergonomics and flexibility of native Swift code.
**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. ## Topics
### Articles
- <doc:WorkingWithConnections>
- <doc:CustomFunctions>
- <doc:PreparedStatements>
- <doc:SQLiteRows>
- <doc:SQLScripts>
- <doc:ErrorHandling>
- <doc:Multithreading>
- <doc:DatabaseEncryption>

View File

@@ -2,33 +2,36 @@ import Foundation
/// Represents the journal modes available for an SQLite database. /// Represents the journal modes available for an SQLite database.
/// ///
/// The journal mode determines how the database handles transactions and how it /// The journal mode determines how the database handles transactions and how it maintains the
/// maintains the journal for rollback and recovery. For more details, refer to /// journal for rollback and recovery.
/// [Journal Mode Pragma](https://www.sqlite.org/pragma.html#pragma_journal_mode). ///
public enum JournalMode: String, SQLiteRawRepresentable { /// - SeeAlso: [journal_mode](https://sqlite.org/pragma.html#pragma_journal_mode)
public enum JournalMode: String, SQLiteRepresentable {
/// DELETE journal mode. /// DELETE journal mode.
/// ///
/// This is the default behavior. The rollback journal is deleted at the conclusion /// This is the default behavior. The rollback journal is deleted at the conclusion of each
/// of each transaction. The delete operation itself causes the transaction to commit. /// 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). /// - SeeAlso: [Atomic Commit In SQLite](https://sqlite.org/atomiccommit.html)
case delete case delete
/// TRUNCATE journal mode. /// TRUNCATE journal mode.
/// ///
/// In this mode, the rollback journal is truncated to zero length at the end of each transaction /// In this mode, the rollback journal is truncated to zero length at the end of each
/// instead of being deleted. On many systems, truncating a file is much faster than deleting it /// transaction instead of being deleted. On many systems, truncating a file is much faster than
/// because truncating does not require modifying the containing directory. /// deleting it because truncating does not require modifying the containing directory.
case truncate case truncate
/// PERSIST journal mode. /// PERSIST journal mode.
/// ///
/// In this mode, the rollback journal is not deleted at the end of each transaction. Instead, /// 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 /// the header of the journal is overwritten with zeros. This prevents other database
/// from rolling the journal back. The PERSIST mode is useful as an optimization on platforms /// connections from rolling the journal back. The PERSIST mode is useful as an optimization on
/// where deleting or truncating a file is more expensive than overwriting the first block of a file /// platforms where deleting or truncating a file is more expensive than overwriting the first
/// with zeros. For additional configuration, refer to /// block of a file with zeros.
/// [journal_size_limit](https://www.sqlite.org/pragma.html#pragma_journal_size_limit). ///
/// - SeeAlso: [journal_size_limit](
/// https://sqlite.org/pragma.html#pragma_journal_size_limit)
case persist case persist
/// MEMORY journal mode. /// MEMORY journal mode.
@@ -42,21 +45,32 @@ public enum JournalMode: String, SQLiteRawRepresentable {
/// ///
/// This mode uses a write-ahead log instead of a rollback journal to implement transactions. /// 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 /// 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 /// and persists even after closing and reopening the database.
/// [Write-Ahead Logging](https://www.sqlite.org/wal.html). ///
/// - SeeAlso: [Write-Ahead Logging](https://sqlite.org/wal.html)
case wal case wal
/// OFF journal mode. /// OFF journal mode.
/// ///
/// In this mode, the rollback journal is completely disabled, meaning no rollback journal is ever created. /// In this mode, the rollback journal is completely disabled, meaning no rollback journal is
/// This disables SQLite's atomic commit and rollback capabilities. The `ROLLBACK` command will no longer work /// ever created. This disables SQLite's atomic commit and rollback capabilities. The `ROLLBACK`
/// and behaves in an undefined way. Applications must avoid using the `ROLLBACK` command when the journal mode is OFF. /// command will no longer work and behaves in an undefined way. Applications must avoid using
/// If the application crashes in the middle of a transaction, the database file will likely become corrupt, /// the `ROLLBACK` command when the journal mode is OFF. If the application crashes in the
/// as there is no way to unwind partially completed operations. For example, if a duplicate entry causes a /// middle of a transaction, the database file will likely become corrupt, as there is no way to
/// `CREATE UNIQUE INDEX` statement to fail halfway through, it will leave behind a partially created index, /// unwind partially completed operations. For example, if a duplicate entry causes a
/// resulting in a corrupted database state. /// `CREATE UNIQUE INDEX` statement to fail halfway through, it will leave behind a partially
/// created index, resulting in a corrupted database state.
case off case off
/// The string representation of the journal mode recognized by SQLite.
///
/// Each case maps to its corresponding uppercase string value expected by SQLite. For example,
/// `.wal` maps to `"WAL"`. This value is typically used when reading or setting the journal mode
/// through the `PRAGMA journal_mode` command.
///
/// - Returns: The uppercase string identifier of the journal mode as understood by SQLite.
///
/// - SeeAlso: [journal_mode](https://sqlite.org/pragma.html#pragma_journal_mode)
public var rawValue: String { public var rawValue: String {
switch self { switch self {
case .delete: "DELETE" case .delete: "DELETE"
@@ -68,6 +82,17 @@ public enum JournalMode: String, SQLiteRawRepresentable {
} }
} }
/// Creates a `JournalMode` instance from a string representation.
///
/// The initializer performs a case-insensitive match between the provided string and the known
/// SQLite journal mode names. If the input does not correspond to any valid journal mode, the
/// initializer returns `nil`.
///
/// - Parameter rawValue: The string name of the journal mode, as defined by SQLite.
/// - Returns: A `JournalMode` value if the input string matches a supported mode; otherwise,
/// `nil`.
///
/// - SeeAlso: [journal_mode](https://sqlite.org/pragma.html#pragma_journal_mode)
public init?(rawValue: String) { public init?(rawValue: String) {
switch rawValue.uppercased() { switch rawValue.uppercased() {
case "DELETE": self = .delete case "DELETE": self = .delete

View File

@@ -1,39 +1,33 @@
import Foundation import Foundation
/// Represents different types of database update actions. /// Represents a type of database change operation.
/// ///
/// The `SQLiteAction` enum is used to identify the type of action /// The `SQLiteAction` enumeration describes an action that modifies a database table. It
/// performed on a database, such as insertion, updating, or deletion. /// distinguishes between row insertions, updates, and deletions, providing context such
/// as the database name, table, and affected row ID.
///
/// - SeeAlso: [Data Change Notification Callbacks](https://sqlite.org/c3ref/update_hook.html)
public enum SQLiteAction { public enum SQLiteAction {
/// Indicates the insertion of a new row into a table. /// A new row was inserted into a table.
///
/// This case is used to represent the action of adding a new
/// row to a specific table in a database.
/// ///
/// - Parameters: /// - Parameters:
/// - db: The name of the database where the insertion occurred. /// - db: The name of the database where the insertion occurred.
/// - table: The name of the table where the insertion occurred. /// - table: The name of the table into which the row was inserted.
/// - rowID: The row ID of the newly inserted row. /// - rowID: The row ID of the newly inserted row.
case insert(db: String, table: String, rowID: Int64) case insert(db: String, table: String, rowID: Int64)
/// Indicates the modification of an existing row in a table. /// An existing row was modified in a table.
///
/// This case is used to represent the action of updating an
/// existing row within a specific table in a database.
/// ///
/// - Parameters: /// - Parameters:
/// - db: The name of the database where the update occurred. /// - db: The name of the database where the update occurred.
/// - table: The name of the table where the update occurred. /// - table: The name of the table containing the updated row.
/// - rowID: The row ID of the updated row. /// - rowID: The row ID of the modified row.
case update(db: String, table: String, rowID: Int64) case update(db: String, table: String, rowID: Int64)
/// Indicates the removal of a row from a table. /// A row was deleted from a table.
///
/// This case is used to represent the action of deleting a
/// row from a specific table in a database.
/// ///
/// - Parameters: /// - Parameters:
/// - db: The name of the database from which the row was deleted. /// - db: The name of the database where the deletion occurred.
/// - table: The name of the table 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. /// - rowID: The row ID of the deleted row.
case delete(db: String, table: String, rowID: Int64) case delete(db: String, table: String, rowID: Int64)

View File

@@ -1,75 +0,0 @@
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

@@ -1,99 +0,0 @@
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,65 @@
import Foundation
/// An enumeration that represents raw SQLite values.
///
/// `SQLiteValue` encapsulates all fundamental SQLite storage classes. It is used to
/// store values retrieved from or written to a SQLite database, providing a type-safe
/// Swift representation for each supported data type.
///
/// - SeeAlso: [Datatypes In SQLite](https://sqlite.org/datatype3.html)
///
/// ## Topics
///
/// ### Enumeration Cases
///
/// - ``int(_:)``
/// - ``real(_:)``
/// - ``text(_:)``
/// - ``blob(_:)``
/// - ``null``
public enum SQLiteValue: Equatable, Hashable, Sendable {
/// A 64-bit integer value.
case int(Int64)
/// A double-precision floating-point value.
case real(Double)
/// A text string encoded in UTF-8.
case text(String)
/// Binary data (BLOB).
case blob(Data)
/// A `NULL` value.
case null
}
public extension SQLiteValue {
/// A SQL literal representation of the value.
///
/// Converts the current value into a string suitable for embedding directly in an SQL
/// statement. Strings are quoted and escaped, binary data is encoded in hexadecimal form, and
/// `NULL` is represented by the literal `NULL`.
var sqliteLiteral: String {
switch self {
case .int(let int): "\(int)"
case .real(let real): "\(real)"
case .text(let text): "'\(text.replacingOccurrences(of: "'", with: "''"))'"
case .blob(let data): "X'\(data.hex)'"
case .null: "NULL"
}
}
}
extension SQLiteValue: CustomStringConvertible {
/// A textual representation of the value, identical to `sqliteLiteral`.
public var description: String {
sqliteLiteral
}
}
private extension Data {
var hex: String {
map { String(format: "%02hhX", $0) }.joined()
}
}

View File

@@ -1,45 +1,41 @@
import Foundation import Foundation
/// Represents different synchronous modes available for an SQLite database. /// Represents the available synchronous modes for an SQLite database.
/// ///
/// The synchronous mode determines how SQLite handles data synchronization with the database. /// The synchronous mode controls how thoroughly SQLite ensures that data is physically written to
/// For more details, refer to [Synchronous Pragma](https://www.sqlite.org/pragma.html#pragma_synchronous). /// disk. It defines the balance between durability, consistency, and performance during commits.
public enum Synchronous: UInt8, SQLiteRawRepresentable { ///
/// Synchronous mode off. Disables synchronization for maximum performance. /// - SeeAlso: [PRAGMA synchronous](https://sqlite.org/pragma.html#pragma_synchronous)
public enum Synchronous: UInt8, SQLiteRepresentable {
/// Disables synchronization for maximum performance.
/// ///
/// With synchronous OFF, SQLite continues without syncing as soon as it has handed data off /// With `synchronous=OFF`, SQLite does not wait for data to reach non-volatile storage before
/// to the operating system. If the application running SQLite crashes, the data will be safe, /// continuing. The database may become inconsistent if the operating system crashes or power is
/// but the database might become corrupted if the operating system crashes or the computer loses /// lost, although application-level crashes do not cause corruption.
/// power before the data is written to the disk surface. On the other hand, commits can be orders /// Best suited for temporary databases or rebuildable data.
/// of magnitude faster with synchronous OFF.
case off = 0 case off = 0
/// Normal synchronous mode. /// Enables normal synchronization.
/// ///
/// The SQLite database engine syncs at the most critical moments, but less frequently /// SQLite performs syncs only at critical points. In WAL mode, this guarantees consistency but
/// than in FULL mode. While there is a very small chance of corruption in /// not full durability: the most recent transactions might be lost after a power failure. In
/// `journal_mode=DELETE` on older filesystems during a power failure, WAL /// rollback journal mode, there is a very small chance of corruption on older filesystems.
/// mode is safe from corruption with synchronous=NORMAL. Modern filesystems /// Recommended for most use cases where performance is preferred over strict durability.
/// 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 case normal = 1
/// Full synchronous mode. /// Enables full synchronization.
/// ///
/// Uses the xSync method of the VFS to ensure that all content is safely written /// SQLite calls the VFS `xSync` method to ensure that all data is written to disk before
/// to the disk surface prior to continuing. This ensures that an operating system /// continuing. Prevents corruption even after a system crash or power loss. Default mode for
/// crash or power failure will not corrupt the database. FULL synchronous is very /// rollback journals and fully ACID-compliant in WAL mode. Provides strong consistency and
/// safe but also slower. It is the most commonly used synchronous setting when /// isolation; durability may depend on filesystem behavior.
/// not in WAL mode.
case full = 2 case full = 2
/// Extra synchronous mode. /// Enables extra synchronization for maximum durability.
/// ///
/// Similar to FULL mode, but ensures the directory containing the rollback journal /// Extends `FULL` by also syncing the directory that contained the rollback journal after it
/// is synced after the journal is unlinked, providing additional durability in case of /// is removed, ensuring durability even if power is lost immediately after a commit. Guarantees
/// power loss shortly after a commit. /// full ACID compliance in both rollback and WAL modes. Recommended for systems where
/// durability is more important than performance.
case extra = 3 case extra = 3
} }

View File

@@ -1,39 +1,35 @@
import Foundation import Foundation
/// An enumeration representing different types of SQLite transactions. /// Represents the transaction modes supported by SQLite.
/// ///
/// SQLite transactions determine how the database engine handles concurrency and locking /// A transaction defines how the database manages concurrency and locking. The transaction type
/// during a transaction. The default transaction behavior is DEFERRED. For more detailed information /// determines when a write lock is acquired and how other connections can access the database
/// about SQLite transactions, refer to the [SQLite documentation](https://www.sqlite.org/lang_transaction.html). /// during the transaction.
///
/// - SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html)
public enum TransactionType: String, CustomStringConvertible { public enum TransactionType: String, CustomStringConvertible {
/// A deferred transaction. /// Defers the start of the transaction until the first database access.
/// ///
/// A deferred transaction does not start until the database is first accessed. Internally, /// With `BEGIN DEFERRED`, no locks are acquired immediately. If the first statement is a read
/// the `BEGIN DEFERRED` statement merely sets a flag on the database connection to prevent /// (`SELECT`), a read transaction begins. If it is a write statement, a write transaction
/// the automatic commit that normally occurs when the last statement finishes. If the first /// begins instead. Deferred transactions allow greater concurrency and are the default mode.
/// 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" case deferred = "DEFERRED"
/// An immediate transaction. /// Starts a write transaction immediately.
/// ///
/// An immediate transaction starts a new write immediately, without waiting for the first /// With `BEGIN IMMEDIATE`, a reserved lock is acquired right away to ensure that no other
/// write statement. The `BEGIN IMMEDIATE` statement may fail with `SQLITE_BUSY` if another /// connection can start a conflicting write. The statement may fail with `SQLITE_BUSY` if
/// write transaction is active on a different database connection. /// another write transaction is already active.
case immediate = "IMMEDIATE" case immediate = "IMMEDIATE"
/// An exclusive transaction. /// Starts an exclusive write transaction.
/// ///
/// Similar to `IMMEDIATE`, an exclusive transaction starts a write immediately. However, /// With `BEGIN EXCLUSIVE`, a write lock is acquired immediately. In rollback journal mode, it
/// in non-WAL modes, `EXCLUSIVE` prevents other database connections from reading the database /// also prevents other connections from reading the database while the transaction is active.
/// while the transaction is in progress. In WAL mode, `EXCLUSIVE` behaves the same as `IMMEDIATE`. /// In WAL mode, it behaves the same as `.immediate`.
case exclusive = "EXCLUSIVE" case exclusive = "EXCLUSIVE"
/// A textual representation of the transaction type. /// A textual representation of the transaction type.
///
/// Returns the raw value of the transaction type (e.g., "DEFERRED", "IMMEDIATE", "EXCLUSIVE").
public var description: String { public var description: String {
rawValue rawValue
} }

View File

@@ -1,25 +1,27 @@
import Foundation import Foundation
public extension SQLiteRawBindable where Self: BinaryFloatingPoint { public extension SQLiteBindable where Self: BinaryFloatingPoint {
/// Provides the `SQLiteRawValue` representation for floating-point types. /// Converts a floating-point value to its SQLite representation.
/// ///
/// This implementation converts the floating-point value to a `real` SQLite raw value. /// Floating-point numbers are stored in SQLite as `REAL` values. This property wraps the
/// current value into an ``SQLiteValue/real(_:)`` case, suitable for parameter binding.
/// ///
/// - Returns: An `SQLiteRawValue` of type `.real`, containing the floating-point value. /// - Returns: An ``SQLiteValue`` of type `.real` containing the numeric value.
var sqliteRawValue: SQLiteRawValue { var sqliteValue: SQLiteValue {
.real(.init(self)) .real(.init(self))
} }
} }
public extension SQLiteRawRepresentable where Self: BinaryFloatingPoint { public extension SQLiteRepresentable where Self: BinaryFloatingPoint {
/// Initializes an instance of the conforming type from an `SQLiteRawValue`. /// Creates a floating-point value from an SQLite representation.
/// ///
/// This initializer handles `SQLiteRawValue` of type `.real`, converting it to the floating-point value. /// This initializer supports both ``SQLiteValue/real(_:)`` and ``SQLiteValue/int(_:)`` cases,
/// It also handles `SQLiteRawValue` of type `.int`, converting it to the floating-point value. /// converting the stored number to the corresponding floating-point type.
/// ///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. /// - Parameter value: The SQLite value to convert from.
init?(_ sqliteRawValue: SQLiteRawValue) { /// - Returns: A new instance if the conversion succeeds, or `nil` if the value is incompatible.
switch sqliteRawValue { init?(_ value: SQLiteValue) {
switch value {
case .int(let value): case .int(let value):
self.init(Double(value)) self.init(Double(value))
case .real(let value): case .real(let value):
@@ -30,5 +32,5 @@ public extension SQLiteRawRepresentable where Self: BinaryFloatingPoint {
} }
} }
extension Float: SQLiteRawRepresentable {} extension Float: SQLiteRepresentable {}
extension Double: SQLiteRawRepresentable {} extension Double: SQLiteRepresentable {}

View File

@@ -1,27 +1,28 @@
import Foundation import Foundation
public extension SQLiteRawBindable where Self: BinaryInteger { public extension SQLiteBindable where Self: BinaryInteger {
/// Provides the `SQLiteRawValue` representation for integer types. /// Converts an integer value to its SQLite representation.
/// ///
/// This implementation converts the integer value to an `SQLiteRawValue` of type `.int`. /// Integer values are stored in SQLite as `INTEGER` values. This property wraps the current
/// value into an ``SQLiteValue/int(_:)`` case, suitable for use in parameter binding.
/// ///
/// - Returns: An `SQLiteRawValue` of type `.int`, containing the integer value. /// - Returns: An ``SQLiteValue`` of type `.int` containing the integer value.
var sqliteRawValue: SQLiteRawValue { var sqliteValue: SQLiteValue {
.int(Int64(self)) .int(Int64(self))
} }
} }
public extension SQLiteRawRepresentable where Self: BinaryInteger { public extension SQLiteRepresentable where Self: BinaryInteger {
/// Initializes an instance of the conforming type from an `SQLiteRawValue`. /// Creates an integer value from an SQLite representation.
/// ///
/// This initializer handles `SQLiteRawValue` of type `.int`, converting it to the integer value. /// This initializer supports the ``SQLiteValue/int(_:)`` case and uses `init(exactly:)` to
/// It uses the `init(exactly:)` initializer to ensure that the value fits within the range of the /// ensure that the value fits within the bounds of the integer type. If the value cannot be
/// integer type. If the value cannot be exactly represented by the integer type, the initializer /// exactly represented, the initializer returns `nil`.
/// will return `nil`.
/// ///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. /// - Parameter value: The SQLite value to convert from.
init?(_ sqliteRawValue: SQLiteRawValue) { /// - Returns: A new instance if the conversion succeeds, or `nil` otherwise.
switch sqliteRawValue { init?(_ value: SQLiteValue) {
switch value {
case .int(let value): case .int(let value):
self.init(exactly: value) self.init(exactly: value)
default: default:
@@ -30,14 +31,14 @@ public extension SQLiteRawRepresentable where Self: BinaryInteger {
} }
} }
extension Int: SQLiteRawRepresentable {} extension Int: SQLiteRepresentable {}
extension Int8: SQLiteRawRepresentable {} extension Int8: SQLiteRepresentable {}
extension Int16: SQLiteRawRepresentable {} extension Int16: SQLiteRepresentable {}
extension Int32: SQLiteRawRepresentable {} extension Int32: SQLiteRepresentable {}
extension Int64: SQLiteRawRepresentable {} extension Int64: SQLiteRepresentable {}
extension UInt: SQLiteRawRepresentable {} extension UInt: SQLiteRepresentable {}
extension UInt8: SQLiteRawRepresentable {} extension UInt8: SQLiteRepresentable {}
extension UInt16: SQLiteRawRepresentable {} extension UInt16: SQLiteRepresentable {}
extension UInt32: SQLiteRawRepresentable {} extension UInt32: SQLiteRepresentable {}
extension UInt64: SQLiteRawRepresentable {} extension UInt64: SQLiteRepresentable {}

View File

@@ -1,28 +1,27 @@
import Foundation import Foundation
extension Bool: SQLiteRawRepresentable { extension Bool: SQLiteRepresentable {
/// Provides the `SQLiteRawValue` representation for boolean types. /// Converts a Boolean value to its SQLite representation.
/// ///
/// This implementation converts the boolean value to an `SQLiteRawValue` of type `.int`. /// Boolean values are stored in SQLite as integers (`INTEGER` type). The value `true` is
/// - `true` is represented as `1`. /// represented as `1`, and `false` as `0`.
/// - `false` is represented as `0`.
/// ///
/// - Returns: An `SQLiteRawValue` of type `.int`, containing `1` for `true` and `0` for `false`. /// - Returns: An ``SQLiteValue`` of type `.int`, containing `1` for `true`
public var sqliteRawValue: SQLiteRawValue { /// and `0` for `false`.
public var sqliteValue: SQLiteValue {
.int(self ? 1 : 0) .int(self ? 1 : 0)
} }
/// Initializes an instance of the conforming type from an `SQLiteRawValue`. /// Creates a Boolean value from an SQLite representation.
/// ///
/// This initializer handles `SQLiteRawValue` of type `.int`, converting it to a boolean value. /// This initializer supports the ``SQLiteValue/int(_:)`` case and converts the integer value to
/// - `1` is converted to `true`. /// a Boolean. `1` is interpreted as `true`, `0` as `false`. If the integer is not `0` or `1`,
/// - `0` is converted to `false`. /// the initializer returns `nil`.
/// ///
/// If the integer value is not `0` or `1`, the initializer returns `nil`. /// - Parameter value: The SQLite value to convert from.
/// /// - Returns: A Boolean value if the conversion succeeds, or `nil` otherwise.
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. public init?(_ value: SQLiteValue) {
public init?(_ sqliteRawValue: SQLiteRawValue) { switch value {
switch sqliteRawValue {
case .int(let value) where value == 0 || value == 1: case .int(let value) where value == 0 || value == 1:
self = value == 1 self = value == 1
default: default:

View File

@@ -1,22 +1,25 @@
import Foundation import Foundation
extension Data: SQLiteRawRepresentable { extension Data: SQLiteRepresentable {
/// Provides the `SQLiteRawValue` representation for `Data` types. /// Converts a `Data` value to its SQLite representation.
/// ///
/// This implementation converts the `Data` value to an `SQLiteRawValue` of type `.blob`. /// Binary data is stored in SQLite as a BLOB (`BLOB` type). This property wraps the current
/// value into an ``SQLiteValue/blob(_:)`` case, suitable for parameter binding.
/// ///
/// - Returns: An `SQLiteRawValue` of type `.blob`, containing the data. /// - Returns: An ``SQLiteValue`` of type `.blob` containing the binary data.
public var sqliteRawValue: SQLiteRawValue { public var sqliteValue: SQLiteValue {
.blob(self) .blob(self)
} }
/// Initializes an instance of the conforming type from an `SQLiteRawValue`. /// Creates a `Data` value from an SQLite representation.
/// ///
/// This initializer handles `SQLiteRawValue` of type `.blob`, converting it to `Data`. /// This initializer supports the ``SQLiteValue/blob(_:)`` case and converts the binary content
/// to a `Data` instance.
/// ///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. /// - Parameter value: The SQLite value to convert from.
public init?(_ sqliteRawValue: SQLiteRawValue) { /// - Returns: A `Data` instance if the conversion succeeds, or `nil` otherwise.
switch sqliteRawValue { public init?(_ value: SQLiteValue) {
switch value {
case .blob(let data): case .blob(let data):
self = data self = data
default: default:

View File

@@ -1,37 +1,38 @@
import Foundation import Foundation
extension Date: SQLiteRawRepresentable { extension Date: SQLiteRepresentable {
/// Provides the `SQLiteRawValue` representation for `Date` types. /// Converts a `Date` value to its SQLite representation.
/// ///
/// This implementation converts the `Date` value to an `SQLiteRawValue` of type `.text`. /// Dates are stored in SQLite as text using the ISO 8601 format. This property converts the
/// The date is formatted as an ISO 8601 string. /// current date into an ISO 8601 string and wraps it in an ``SQLiteValue/text(_:)`` case,
/// suitable for parameter binding.
/// ///
/// - Returns: An `SQLiteRawValue` of type `.text`, containing the ISO 8601 string representation of the date. /// - Returns: An ``SQLiteValue`` of type `.text`, containing the ISO 8601 string.
public var sqliteRawValue: SQLiteRawValue { public var sqliteValue: SQLiteValue {
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
let dateString = formatter.string(from: self) let dateString = formatter.string(from: self)
return .text(dateString) return .text(dateString)
} }
/// Initializes an instance of `Date` from an `SQLiteRawValue`. /// Creates a `Date` value from an SQLite representation.
/// ///
/// This initializer handles `SQLiteRawValue` of type `.text`, converting it from an ISO 8601 string. /// This initializer supports the following ``SQLiteValue`` cases:
/// It also supports `.int` and `.real` types representing time intervals since 1970. /// - ``SQLiteValue/text(_:)`` parses an ISO 8601 date string.
/// - ``SQLiteValue/int(_:)`` or ``SQLiteValue/real(_:)`` interprets the number as a time
/// interval since 1970 (UNIX timestamp).
/// ///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. /// - Parameter value: The SQLite value to convert from.
public init?(_ sqliteRawValue: SQLiteRawValue) { /// - Returns: A `Date` instance if the conversion succeeds, or `nil` otherwise.
switch sqliteRawValue { public init?(_ value: SQLiteValue) {
switch value {
case .int(let value): case .int(let value):
self.init(timeIntervalSince1970: TimeInterval(value)) self.init(timeIntervalSince1970: TimeInterval(value))
case .real(let value): case .real(let value):
self.init(timeIntervalSince1970: value) self.init(timeIntervalSince1970: value)
case .text(let value): case .text(let value):
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
if let date = formatter.date(from: value) { guard let date = formatter.date(from: value) else { return nil }
self = date self = date
} else {
return nil
}
default: default:
return nil return nil
} }

View File

@@ -1,28 +1,28 @@
import Foundation import Foundation
public extension SQLiteRawBindable where Self: RawRepresentable, RawValue: SQLiteRawBindable { public extension SQLiteBindable where Self: RawRepresentable, RawValue: SQLiteBindable {
/// Provides the `SQLiteRawValue` representation for `RawRepresentable` types. /// Converts a `RawRepresentable` value to its SQLite representation.
/// ///
/// This implementation converts the `RawRepresentable` type's `rawValue` to its corresponding /// The `rawValue` of the conforming type must itself conform to ``SQLiteBindable``. This
/// `SQLiteRawValue` representation. The `rawValue` itself must conform to `SQLiteRawBindable`. /// property delegates the conversion to the underlying ``rawValue``.
/// ///
/// - Returns: An `SQLiteRawValue` representation of the `RawRepresentable` type. /// - Returns: The ``SQLiteValue`` representation of the underlying ``rawValue``.
var sqliteRawValue: SQLiteRawValue { var sqliteValue: SQLiteValue {
rawValue.sqliteRawValue rawValue.sqliteValue
} }
} }
public extension SQLiteRawRepresentable where Self: RawRepresentable, RawValue: SQLiteRawRepresentable { public extension SQLiteRepresentable where Self: RawRepresentable, RawValue: SQLiteRepresentable {
/// Initializes an instance of the conforming type from an `SQLiteRawValue`. /// Creates a `RawRepresentable` value from an SQLite representation.
/// ///
/// This initializer converts the `SQLiteRawValue` to the `RawRepresentable` type's `rawValue`. /// This initializer first attempts to create the underlying ``RawValue`` from the provided
/// It first attempts to create a `RawValue` from the `SQLiteRawValue`, then uses that to initialize /// ``SQLiteValue``. If successful, it uses that raw value to initialize the `RawRepresentable`
/// the `RawRepresentable` instance. If the `SQLiteRawValue` cannot be converted to the `RawValue`, the /// type. If the conversion fails, the initializer returns `nil`.
/// initializer returns `nil`.
/// ///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. /// - Parameter value: The SQLite value to convert from.
init?(_ sqliteRawValue: SQLiteRawValue) { /// - Returns: A new instance if the conversion succeeds, or `nil` otherwise.
if let value = RawValue(sqliteRawValue) { init?(_ value: SQLiteValue) {
if let value = RawValue(value) {
self.init(rawValue: value) self.init(rawValue: value)
} else { } else {
return nil return nil

View File

@@ -1,22 +1,25 @@
import Foundation import Foundation
extension String: SQLiteRawRepresentable { extension String: SQLiteRepresentable {
/// Provides the `SQLiteRawValue` representation for `String` type. /// Converts a `String` value to its SQLite representation.
/// ///
/// This implementation converts the `String` value to an `SQLiteRawValue` of type `.text`. /// Strings are stored in SQLite as text (`TEXT` type). This property wraps the current value
/// into an ``SQLiteValue/text(_:)`` case, suitable for parameter binding.
/// ///
/// - Returns: An `SQLiteRawValue` of type `.text`, containing the string value. /// - Returns: An ``SQLiteValue`` of type `.text` containing the string value.
public var sqliteRawValue: SQLiteRawValue { public var sqliteValue: SQLiteValue {
.text(self) .text(self)
} }
/// Initializes an instance of `String` from an `SQLiteRawValue`. /// Creates a `String` value from an SQLite representation.
/// ///
/// This initializer handles `SQLiteRawValue` of type `.text`, converting it to a `String` value. /// This initializer supports the ``SQLiteValue/text(_:)`` case and converts the text content
/// to a `String` instance.
/// ///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. /// - Parameter value: The SQLite value to convert from.
public init?(_ sqliteRawValue: SQLiteRawValue) { /// - Returns: A `String` instance if the conversion succeeds, or `nil` otherwise.
switch sqliteRawValue { public init?(_ value: SQLiteValue) {
switch value {
case .text(let value): case .text(let value):
self = value self = value
default: default:

View File

@@ -1,22 +1,26 @@
import Foundation import Foundation
extension UUID: SQLiteRawRepresentable { extension UUID: SQLiteRepresentable {
/// Provides the `SQLiteRawValue` representation for `UUID`. /// Converts a `UUID` value to its SQLite representation.
/// ///
/// This implementation converts the `UUID` value to an `SQLiteRawValue` of type `.text`. /// UUIDs are stored in SQLite as text (`TEXT` type) using their canonical string form
/// (e.g. `"550E8400-E29B-41D4-A716-446655440000"`). This property wraps the current value into
/// an ``SQLiteValue/text(_:)`` case.
/// ///
/// - Returns: An `SQLiteRawValue` of type `.text`, containing the UUID string. /// - Returns: An ``SQLiteValue`` of type `.text` containing the UUID string.
public var sqliteRawValue: SQLiteRawValue { public var sqliteValue: SQLiteValue {
.text(self.uuidString) .text(self.uuidString)
} }
/// Initializes an instance of `UUID` from an `SQLiteRawValue`. /// Creates a `UUID` value from an SQLite representation.
/// ///
/// This initializer handles `SQLiteRawValue` of type `.text`, converting it to a `UUID`. /// This initializer supports the ``SQLiteValue/text(_:)`` case and attempts to parse the stored
/// text as a valid UUID string.
/// ///
/// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. /// - Parameter value: The SQLite value to convert from.
public init?(_ sqliteRawValue: SQLiteRawValue) { /// - Returns: A `UUID` instance if the string is valid, or `nil` otherwise.
switch sqliteRawValue { public init?(_ value: SQLiteValue) {
switch value {
case .text(let value): case .text(let value):
self.init(uuidString: value) self.init(uuidString: value)
default: default:

View File

@@ -0,0 +1,24 @@
import Foundation
/// A protocol representing a collection of SQLite argument values.
///
/// Conforming types provide indexed access to a sequence of ``SQLiteValue`` elements. This protocol
/// extends `Collection` to allow convenient typed subscripting using types conforming to
/// ``SQLiteRepresentable``.
public protocol ArgumentsProtocol: Collection where Element == SQLiteValue, Index == Int {
/// Returns the element at the specified index, converted to the specified type.
///
/// This subscript retrieves the argument value at the given index and attempts to convert it to
/// a type conforming to ``SQLiteRepresentable``. If the conversion succeeds, the resulting
/// value of type `T` is returned. Otherwise, `nil` is returned.
///
/// - Parameter index: The index of the value to retrieve and convert.
/// - Returns: A value of type `T` if conversion succeeds, or `nil` if it fails.
subscript<T: SQLiteRepresentable>(index: Index) -> T? { get }
}
public extension ArgumentsProtocol {
subscript<T: SQLiteRepresentable>(index: Index) -> T? {
T.init(self[index])
}
}

View File

@@ -1,170 +1,51 @@
import Foundation import Foundation
/// A protocol defining methods that can be implemented by delegates of a `Connection` object. /// A delegate that observes connection-level database events.
/// ///
/// The `ConnectionDelegate` protocol allows a delegate to receive notifications about various /// Conforming types can monitor row-level updates and transaction lifecycle events. All methods are
/// events that occur within a ``Connection``, including SQL statement tracing, database update /// optional default implementations do nothing.
/// 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 /// This protocol is typically used for debugging, logging, or synchronizing application state with
/// database changes.
/// ///
/// The protocol provides default implementations for all methods, which do nothing. This allows /// - Important: Delegate methods are invoked synchronously on SQLites internal execution thread.
/// conforming types to only implement the methods they are interested in without the need to /// Implementations must be lightweight and non-blocking to avoid slowing down SQL operations.
/// provide an implementation for each method.
/// ///
/// ## Topics /// ## Topics
/// ///
/// ### Instance Methods /// ### Instance Methods
/// ///
/// - ``ConnectionDelegate/connection(_:trace:)``
/// - ``ConnectionDelegate/connection(_:didUpdate:)`` /// - ``ConnectionDelegate/connection(_:didUpdate:)``
/// - ``ConnectionDelegate/connectionWillCommit(_:)`` /// - ``ConnectionDelegate/connectionWillCommit(_:)``
/// - ``ConnectionDelegate/connectionDidRollback(_:)`` /// - ``ConnectionDelegate/connectionDidRollback(_:)``
public protocol ConnectionDelegate: AnyObject { public protocol ConnectionDelegate: AnyObject {
/// Informs the delegate that a SQL statement is being traced. /// Called when a row is inserted, updated, or deleted.
/// ///
/// This method is called right before a SQL statement is executed, allowing the delegate /// Enables reacting to data changes, for example to refresh caches or UI.
/// 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: /// - Parameters:
/// - connection: The ``Connection`` instance that is executing the SQL statement. /// - connection: The connection where the update occurred.
/// - sql: A tuple containing the unexpanded and expanded forms of the SQL statement being traced. /// - action: Describes the affected database, table, and row.
/// - `unexpandedSQL`: The original SQL statement as it was written by the developer. func connection(_ connection: ConnectionProtocol, didUpdate action: SQLiteAction)
/// - `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. /// Called right before a transaction is committed.
/// ///
/// This method is called whenever an update action, such as insertion, modification, /// Throwing an error aborts the commit and causes a rollback.
/// 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: /// - Parameter connection: The connection about to commit.
/// - connection: The `Connection` instance where the update action occurred. /// - Throws: An error to cancel and roll back the transaction.
/// - action: The type of update action that occurred, represented by the ``SQLiteAction`` enum. func connectionWillCommit(_ connection: ConnectionProtocol) throws
///
/// ### 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. /// Called after a transaction is rolled back.
/// ///
/// This method is called when a transaction has been successfully committed. It provides an /// Use to perform cleanup or maintain consistency after a failure.
/// 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. /// - Parameter connection: The connection that rolled back.
/// func connectionDidRollback(_ connection: ConnectionProtocol)
/// - 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 connectionWillCommit(_ 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 connectionWillCommit(_ 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 { public extension ConnectionDelegate {
/// Default implementation of the `connection(_:trace:)` method. func connection(_ connection: ConnectionProtocol, didUpdate action: SQLiteAction) {}
/// func connectionWillCommit(_ connection: ConnectionProtocol) throws {}
/// This default implementation does nothing. func connectionDidRollback(_ connection: ConnectionProtocol) {}
///
/// - 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 `connectionWillCommit(_:)` 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 connectionWillCommit(_ 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

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

View File

@@ -0,0 +1,22 @@
import Foundation
/// A delegate that receives SQL statement trace callbacks.
///
/// Conforming types can inspect SQL before and after parameter expansion for logging, diagnostics,
/// or profiling. Register a trace delegate with ``ConnectionProtocol/add(trace:)``.
///
/// - Important: Callbacks execute synchronously on SQLites internal thread. Keep implementations
/// lightweight to avoid slowing down query execution.
public protocol ConnectionTraceDelegate: AnyObject {
/// Represents traced SQL text before and after parameter substitution.
typealias Trace = (unexpandedSQL: String, expandedSQL: String)
/// Called before a SQL statement is executed.
///
/// Use to trace or log executed statements for debugging or profiling.
///
/// - Parameters:
/// - connection: The active database connection.
/// - sql: A tuple with the original and expanded SQL text.
func connection(_ connection: ConnectionProtocol, trace sql: Trace)
}

View File

@@ -0,0 +1,42 @@
import Foundation
/// A protocol whose conforming types can be used in SQLite statements and queries.
///
/// Conforming types provide a raw SQLite value for binding to prepared-statement parameters
/// and an SQL literal that can be inserted directly into SQL text.
///
/// ```swift
/// struct Device: SQLiteBindable {
/// var model: String
///
/// var sqliteValue: SQLiteValue {
/// return .text(model)
/// }
/// }
/// ```
///
/// ## Topics
///
/// ### Instance Properties
///
/// - ``sqliteValue``
/// - ``sqliteLiteral``
public protocol SQLiteBindable {
/// The raw SQLite value representation.
///
/// Supplies a value compatible with SQLite's internal representation. Used when binding
/// conforming types to parameters of a prepared SQLite statement.
var sqliteValue: SQLiteValue { get }
/// The SQL literal representation.
///
/// Provides a string that conforms to SQL syntax and is compatible with SQLite's rules
/// for literals. Defaults to ``SQLiteValue/sqliteLiteral``.
var sqliteLiteral: String { get }
}
public extension SQLiteBindable {
var sqliteLiteral: String {
sqliteValue.sqliteLiteral
}
}

View File

@@ -1,33 +0,0 @@
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

@@ -1,36 +0,0 @@
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

@@ -1,35 +0,0 @@
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,35 @@
import Foundation
/// A protocol whose conforming types can be initialized from raw SQLite values.
///
/// This protocol extends ``SQLiteBindable`` and adds an initializer for converting a raw SQLite
/// value into the corresponding type.
///
/// ```swift
/// struct Device: SQLiteRepresentable {
/// var model: String
///
/// var sqliteValue: SQLiteValue {
/// return .text(model)
/// }
///
/// init?(_ value: SQLiteValue) {
/// switch value {
/// case .text(let value):
/// self.model = value
/// default:
/// return nil
/// }
/// }
/// }
/// ```
public protocol SQLiteRepresentable: SQLiteBindable {
/// Initializes an instance from a raw SQLite value.
///
/// The initializer should map the provided raw SQLite value to the corresponding type.
/// If the conversion is not possible (for example, the value has an incompatible type),
/// the initializer should return `nil`.
///
/// - Parameter value: The raw SQLite value to convert.
init?(_ value: SQLiteValue)
}

View File

@@ -0,0 +1,311 @@
import Foundation
/// A protocol that defines a prepared SQLite statement.
///
/// Conforming types manage the statement's lifetime, including initialization and finalization.
/// The protocol exposes facilities for parameter discovery and binding, stepping, resetting, and
/// reading result columns.
///
/// ## Topics
///
/// ### Binding Parameters
///
/// - ``parameterCount()``
/// - ``parameterIndexBy(_:)``
/// - ``parameterNameBy(_:)``
/// - ``bind(_:at:)-(SQLiteValue,_)``
/// - ``bind(_:by:)-(SQLiteValue,_)``
/// - ``bind(_:at:)-(T?,_)``
/// - ``bind(_:by:)-(T?,_)``
/// - ``bind(_:)``
/// - ``clearBindings()``
///
/// ### Statement Execution
///
/// - ``step()``
/// - ``reset()``
/// - ``execute(_:)``
///
/// ### Result Set
///
/// - ``columnCount()``
/// - ``columnName(at:)``
/// - ``columnValue(at:)->SQLiteValue``
/// - ``columnValue(at:)->T?``
/// - ``currentRow()``
public protocol StatementProtocol {
// MARK: - Binding Parameters
/// Returns the number of parameters in the prepared SQLite statement.
///
/// This value corresponds to the highest parameter index in the compiled SQL statement.
/// Parameters may be specified using anonymous placeholders (`?`), numbered placeholders
/// (`?NNN`), or named placeholders (`:name`, `@name`, `$name`).
///
/// For statements using only `?` or named parameters, this value equals the number of parameters.
/// However, if numbered placeholders are used, the sequence may contain gaps for example,
/// a statement containing `?2` and `?5` will report a parameter count of `5`.
///
/// - Returns: The index of the largest (rightmost) parameter in the prepared statement.
///
/// - SeeAlso: [Number Of SQL Parameters](https://sqlite.org/c3ref/bind_parameter_count.html)
func parameterCount() -> Int32
/// Returns the index of a parameter identified by its name.
///
/// The `name` must exactly match the placeholder used in the SQL statement, including its
/// prefix character (`:`, `@`, or `$`). For example, if the SQL includes `WHERE id = :id`,
/// you must call `parameterIndexBy(":id")`.
///
/// If no parameter with the specified `name` exists in the prepared statement, this function
/// returns `0`.
///
/// - Parameter name: The parameter name as written in the SQL statement, including its prefix.
/// - Returns: The 1-based parameter index corresponding to `name`, or `0` if not found.
///
/// - SeeAlso: [Index Of A Parameter With A Given Name](https://sqlite.org/c3ref/bind_parameter_index.html)
func parameterIndexBy(_ name: String) -> Int32
/// Returns the name of the parameter at the specified index.
///
/// The returned string matches the placeholder as written in the SQL statement, including its
/// prefix (`:`, `@`, or `$`). For positional (unnamed) parameters, or if the `index` is out of
/// range, this function returns `nil`.
///
/// - Parameter index: A 1-based parameter index.
/// - Returns: The parameter name as written in the SQL statement, or `nil` if unavailable.
///
/// - SeeAlso: [Name Of A Host Parameter](https://sqlite.org/c3ref/bind_parameter_name.html)
func parameterNameBy(_ index: Int32) -> String?
/// Binds a raw SQLite value to a parameter at the specified index.
///
/// Assigns the given `SQLiteValue` to the parameter at the provided 1-based index within the
/// prepared statement. If the index is out of range, or if the statement is invalid or
/// finalized, this function throws an error.
///
/// - Parameters:
/// - value: The `SQLiteValue` to bind to the parameter.
/// - index: The 1-based index of the parameter to bind.
/// - Throws: ``SQLiteError`` if the value cannot be bound (e.g., index out of range).
///
/// - SeeAlso: [Binding Values To Prepared Statements](
/// https://sqlite.org/c3ref/bind_blob.html)
func bind(_ value: SQLiteValue, at index: Int32) throws(SQLiteError)
/// Binds a raw SQLite value to a parameter by its name.
///
/// Resolves `name` to an index and binds `value` to that parameter. The `name` must include
/// its prefix (e.g., `:AAA`, `@AAA`, `$AAA`). Binding a value to a parameter that does not
/// exist results in an error.
///
/// - Parameters:
/// - value: The ``SQLiteValue`` to bind.
/// - name: The parameter name as written in SQL, including its prefix.
/// - Throws: ``SQLiteError`` if binding fails.
///
/// - SeeAlso: [Binding Values To Prepared Statements](
/// https://sqlite.org/c3ref/bind_blob.html)
func bind(_ value: SQLiteValue, by name: String) throws(SQLiteError)
/// Binds a typed value conforming to `SQLiteBindable` by index.
///
/// Converts `value` to its raw SQLite representation and binds it at `index`. If `value` is
/// `nil`, binds `NULL`.
///
/// - Parameters:
/// - value: The value to bind. If `nil`, `NULL` is bound.
/// - index: The 1-based parameter index.
/// - Throws: ``SQLiteError`` if binding fails.
///
/// - SeeAlso: [Binding Values To Prepared Statements](
/// https://sqlite.org/c3ref/bind_blob.html)
func bind<T: SQLiteBindable>(_ value: T?, at index: Int32) throws(SQLiteError)
/// Binds a typed value conforming to `SQLiteBindable` by name.
///
/// Resolves `name` to a parameter index and binds the raw SQLite representation of `value`.
/// If `value` is `nil`, binds `NULL`. The `name` must include its prefix (e.g., `:AAA`,
/// `@AAA`, `$AAA`). Binding to a non-existent parameter results in an error.
///
/// - Parameters:
/// - value: The value to bind. If `nil`, `NULL` is bound.
/// - name: The parameter name as written in SQL, including its prefix.
/// - Throws: ``SQLiteError`` if binding fails.
///
/// - SeeAlso: [Binding Values To Prepared Statements](
/// https://sqlite.org/c3ref/bind_blob.html)
func bind<T: SQLiteBindable>(_ value: T?, by name: String) throws(SQLiteError)
/// Binds the contents of a row to named statement parameters by column name.
///
/// For each `(column, value)` pair in `row`, treats `column` as a named parameter `:column`
/// and binds `value` to that parameter. Parameter names in the SQL must match the row's
/// column names (including the leading colon). Binding to a non-existent parameter results
/// in an error.
///
/// - Parameter row: The row whose column values are to be bound.
/// - Throws: ``SQLiteError`` if any value cannot be bound.
///
/// - SeeAlso: [Binding Values To Prepared Statements](
/// https://sqlite.org/c3ref/bind_blob.html)
func bind(_ row: SQLiteRow) throws(SQLiteError)
/// Clears all parameter bindings of the prepared statement.
///
/// After calling this function, all parameters are set to `NULL`. Call this when reusing the
/// statement with a different set of parameter values.
///
/// - Throws: ``SQLiteError`` if clearing bindings fails.
///
/// - SeeAlso: [Reset All Bindings](https://sqlite.org/c3ref/clear_bindings.html)
func clearBindings() throws(SQLiteError)
// MARK: - Statement Execution
/// Evaluates the prepared statement and advances to the next result row.
///
/// Call repeatedly to iterate over all rows. It returns `true` while a new row is available.
/// After the final row it returns `false`. Statements that produce no rows return `false`
/// immediately. Reset the statement and clear bindings before re-executing.
///
/// - Returns: `true` if a new row is available, or `false` when no more rows remain.
/// - Throws: ``SQLiteError`` if evaluation fails.
///
/// - SeeAlso: [Evaluate An SQL Statement](https://sqlite.org/c3ref/step.html)
@discardableResult
func step() throws(SQLiteError) -> Bool
/// Resets the prepared SQLite statement to its initial state, ready for re-execution.
///
/// Undoes the effects of previous calls to ``step()``. After reset, the statement may be
/// executed again with the same or new inputs. This does not clear parameter bindings.
/// Call ``clearBindings()`` to set all parameters to `NULL` if needed.
///
/// - Throws: ``SQLiteError`` if the statement cannot be reset.
///
/// - SeeAlso: [Reset A Prepared Statement](https://sqlite.org/c3ref/reset.html)
func reset() throws(SQLiteError)
/// Executes the statement once per provided parameter row.
///
/// For each row, binds values, steps until completion (discarding any result rows), clears
/// bindings, and resets the statement. Use this for efficient batch executions (e.g., inserts
/// or updates) with different parameters per run.
///
/// - Parameter rows: Parameter rows to bind for each execution.
/// - Throws: ``SQLiteError`` if binding, stepping, clearing, or resetting fails.
func execute(_ rows: [SQLiteRow]) throws(SQLiteError)
// MARK: - Result Set
/// Returns the number of columns in the current result set.
///
/// If this value is `0`, the prepared statement does not produce rows. This is typically
/// the case for statements that do not return data.
///
/// - Returns: The number of columns in the result set, or `0` if there are no result columns.
///
/// - SeeAlso: [Number Of Columns In A Result Set](
/// https://sqlite.org/c3ref/column_count.html)
func columnCount() -> Int32
/// Returns the name of the column at the specified index in the result set.
///
/// The column name appears as defined in the SQL statement. If the index is out of bounds, this
/// function returns `nil`.
///
/// - Parameter index: The 0-based index of the column for which to retrieve the name.
/// - Returns: The name of the column at the given index, or `nil` if the index is invalid.
///
/// - SeeAlso: [Column Names In A Result Set](https://sqlite.org/c3ref/column_name.html)
func columnName(at index: Int32) -> String?
/// Returns the raw SQLite value at the given result column index.
///
/// Retrieves the value for the specified column in the current result row of the prepared
/// statement, represented as a ``SQLiteValue``. If the index is out of range, returns
/// ``SQLiteValue/null``.
///
/// - Parameter index: The 0-based index of the result column to access.
/// - Returns: The raw ``SQLiteValue`` at the specified column.
///
/// - SeeAlso: [Result Values From A Query](https://sqlite.org/c3ref/column_blob.html)
func columnValue(at index: Int32) -> SQLiteValue
/// Returns the value of the result column at `index`, converted to `T`.
///
/// Attempts to initialize `T` from the raw ``SQLiteValue`` at `index` using
/// ``SQLiteRepresentable``. Returns `nil` if the conversion is not possible.
///
/// - Parameter index: The 0-based result column index.
/// - Returns: A value of type `T` if conversion succeeds, otherwise `nil`.
///
/// - SeeAlso: [Result Values From A Query](https://sqlite.org/c3ref/column_blob.html)
func columnValue<T: SQLiteRepresentable>(at index: Int32) -> T?
/// Returns the current result row.
///
/// Builds a row by iterating over all result columns at the current cursor position, reading
/// each column's name and value, and inserting them into the row.
///
/// - Returns: A `SQLiteRow` mapping column names to values, or `nil` if there are no columns.
///
/// - SeeAlso: [Result Values From A Query](https://sqlite.org/c3ref/column_blob.html)
func currentRow() -> SQLiteRow?
}
// MARK: - Default Implementation
public extension StatementProtocol {
func bind(_ value: SQLiteValue, by name: String) throws(SQLiteError) {
try bind(value, at: parameterIndexBy(name))
}
func bind<T: SQLiteBindable>(_ value: T?, at index: Int32) throws(SQLiteError) {
try bind(value?.sqliteValue ?? .null, at: index)
}
func bind<T: SQLiteBindable>(_ value: T?, by name: String) throws(SQLiteError) {
try bind(value?.sqliteValue ?? .null, at: parameterIndexBy(name))
}
func bind(_ row: SQLiteRow) throws(SQLiteError) {
for (column, value) in row {
let index = parameterIndexBy(":\(column)")
try bind(value, at: index)
}
}
func execute(_ rows: [SQLiteRow]) throws(SQLiteError) {
for row in rows {
try bind(row)
var hasStep: Bool
repeat {
hasStep = try step()
} while hasStep
try clearBindings()
try reset()
}
}
func columnValue<T: SQLiteRepresentable>(at index: Int32) -> T? {
T(columnValue(at: index))
}
func currentRow() -> SQLiteRow? {
let columnCount = columnCount()
guard columnCount > 0 else { return nil }
var row = SQLiteRow()
row.reserveCapacity(columnCount)
for index in 0..<columnCount {
let name = columnName(at: index)!
let value = columnValue(at: index)
row[name] = value
}
return row
}
}

View File

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

View File

@@ -13,8 +13,8 @@ import Foundation
/// ///
/// ### Loading from a File /// ### Loading from a File
/// ///
/// To load a SQL script from a file in your project, use the following code. In this example, /// To load a SQL script from a file in your project, use the following code. In this example, we
/// we load a file named `sample_script.sql` from the main app bundle. /// load a file named `sample_script.sql` from the main app bundle.
/// ///
/// ```swift /// ```swift
/// do { /// do {
@@ -104,16 +104,16 @@ import Foundation
/// - Important: Nested comments are not supported, so avoid placing multi-line comments inside /// - Important: Nested comments are not supported, so avoid placing multi-line comments inside
/// other multi-line comments. /// other multi-line comments.
/// ///
/// - Important: `SQLScript` does not support SQL scripts containing transactions. /// - Important: `SQLScript` does not support SQL scripts containing transactions. To execute an
/// To execute an `SQLScript`, use the method ``Connection/execute(sql:)``, which executes /// `SQLScript`, use the method ``ConnectionProtocol/execute(sql:)``, which executes each statement
/// each statement individually in autocommit mode. /// individually in autocommit mode.
/// ///
/// If you need to execute the entire `SQLScript` within a single transaction, use the methods /// If you need to execute the entire `SQLScript` within a single transaction, use the methods
/// ``Connection/beginTransaction(_:)``, ``Connection/commitTransaction()``, and /// ``ConnectionProtocol/beginTransaction(_:)``, ``ConnectionProtocol/commitTransaction()``, and
/// ``Connection/rollbackTransaction()`` to manage the transaction explicitly. /// ``ConnectionProtocol/rollbackTransaction()`` to manage the transaction explicitly.
/// ///
/// If your SQL script includes transaction statements (e.g., BEGIN, COMMIT, ROLLBACK), /// If your SQL script includes transaction statements (e.g., BEGIN, COMMIT, ROLLBACK), execute
/// execute the entire script using ``Connection/execute(raw:)``. /// the entire script using ``ConnectionProtocol/execute(raw:)``.
/// ///
/// - Important: This class is not designed to work with untrusted user data. Never insert /// - 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. /// user-provided data directly into SQL queries without proper sanitization or parameterization.
@@ -199,10 +199,12 @@ public struct SQLScript: Collection, ExpressibleByStringLiteral {
extension: String? = nil, extension: String? = nil,
in bundle: Bundle = .main in bundle: Bundle = .main
) throws { ) throws {
guard let url = bundle.url( guard
let url = bundle.url(
forResource: name, forResource: name,
withExtension: `extension` withExtension: `extension`
) else { return nil } )
else { return nil }
try self.init(contentsOf: url) try self.init(contentsOf: url)
} }
@@ -239,7 +241,8 @@ public struct SQLScript: Collection, ExpressibleByStringLiteral {
/// ///
/// - Parameter string: The string containing SQL queries. /// - Parameter string: The string containing SQL queries.
public init(string: String) { public init(string: String) {
elements = string elements =
string
.removingComments() .removingComments()
.trimmingLines() .trimmingLines()
.splitStatements() .splitStatements()

View File

@@ -0,0 +1,69 @@
import Foundation
import DataLiteC
/// A structure that represents an error produced by SQLite operations.
///
/// `SQLiteError` encapsulates both the numeric SQLite error code and its associated human-readable
/// message. It provides a unified way to report and inspect failures that occur during database
/// interactions.
///
/// - SeeAlso: [Result and Error Codes](https://sqlite.org/rescode.html)
///
/// ## Topics
///
/// ### Instance Properties
///
/// - ``code``
/// - ``message``
/// - ``description``
///
/// ### Initializers
///
/// - ``init(code:message:)``
public struct SQLiteError: Error, Equatable, CustomStringConvertible, Sendable {
/// The extended SQLite result code associated with the error.
///
/// This numeric value identifies the specific type of error that occurred. Extended result
/// codes offer more precise information than primary codes, enabling finer-grained error
/// handling and diagnostics.
///
/// - SeeAlso: [Result and Error Codes](https://sqlite.org/rescode.html)
public let code: Int32
/// The human-readable message returned by SQLite.
///
/// This string describes the error condition reported by SQLite. It is typically retrieved
/// directly from the database engine and may include details about constraint violations,
/// syntax errors, I/O issues, or resource limitations.
///
/// - Note: The content of this message is determined by SQLite and may vary between error
/// occurrences. Always refer to this property for detailed diagnostic information.
public let message: String
/// A textual representation of the error including its code and message.
///
/// The value of this property is a concise string describing the error. It includes the type
/// name (`SQLiteError`), the numeric code, and the corresponding message, making it useful for
/// debugging, logging, or diagnostic displays.
public var description: String {
"\(Self.self) code: \(code) message: \(message)"
}
/// Creates a new error instance with the specified result code and message.
///
/// Use this initializer to represent an SQLite error explicitly by providing both the numeric
/// result code and the associated descriptive message.
///
/// - Parameters:
/// - code: The extended SQLite result code associated with the error.
/// - message: A human-readable description of the error, as reported by SQLite.
public init(code: Int32, message: String) {
self.code = code
self.message = message
}
init(_ connection: OpaquePointer) {
self.code = sqlite3_extended_errcode(connection)
self.message = String(cString: sqlite3_errmsg(connection))
}
}

View File

@@ -1,192 +1,65 @@
import Foundation import Foundation
import OrderedCollections import OrderedCollections
/// A structure representing a single row in an SQLite database, providing ordered access to columns and their values. /// An ordered collection that stores the column-value pairs of a single SQLite result row.
/// ///
/// The `SQLiteRow` structure allows for convenient access to the data stored in a row of an SQLite /// `SQLiteRow` preserves the order of columns as provided by the underlying data source. Each key
/// database, using an ordered dictionary to maintain the insertion order of columns. This makes it /// is the column name exposed by the executed statement, and every value is represented as a
/// easy to retrieve, update, and manage the values associated with each column in the row. /// ``SQLiteValue``.
/// ///
/// ```swift /// You can use dictionary-style lookup to access values by column name or iterate over ordered
/// let row = SQLiteRow() /// pairs using standard collection APIs. Column names are unique, and insertion order is maintained
/// row["name"] = "John Doe" /// deterministically, making `SQLiteRow` safe to pass into APIs that rely on stable row layouts.
/// row["age"] = 30 public struct SQLiteRow {
/// print(row.description) /// The type that identifies a column within a row.
/// // Outputs: ["name": 'John Doe', "age": 30] ///
/// ``` /// In SQLite, a column name corresponds to the alias or identifier returned by the executing
/// /// statement. Each column name is unique within a single row.
/// ## Topics public typealias Column = String
///
/// ### 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. /// The type that represents a value stored in a column.
/// public typealias Value = SQLiteValue
/// 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 // MARK: - Properties
/// An ordered dictionary that stores the columns and their associated values in the row. private var elements: OrderedDictionary<Column, Value>
///
/// 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. /// The column names in the order they appear in the result set.
/// ///
/// This property indicates the initial position of the row's elements. Since the /// The order of column names corresponds to the sequence defined in the executed SQL statement.
/// elements in the row are indexed starting from zero, this property consistently /// This order is preserved exactly as provided by SQLite, ensuring deterministic column
/// returns zero, allowing for predictable iteration through the row's data. /// indexing across rows.
/// public var columns: [Column] {
/// - 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 elements.keys.elements
} }
/// A list of SQL named parameters in the form `:column`, preserving column order. /// The named parameter tokens corresponding to each column, in result order.
/// ///
/// Useful for generating placeholders in SQL queries (e.g. `VALUES (:column1, :column2, ...)`) /// Each element is formed by prefixing the column name with a colon (`:`), matching the syntax
/// to match the row's structure. /// of SQLite named parameters (e.g., `:username`, `:id`). The order of tokens matches the order
/// /// of columns in the result set.
/// - Complexity: `O(n)`
public var namedParameters: [String] { public var namedParameters: [String] {
elements.keys.map { ":\($0)" } elements.keys.map { ":\($0)" }
} }
// MARK: - Inits // MARK: - Inits
/// Initializes an empty row. /// Creates an empty row with no columns.
///
/// This initializer creates a new instance of `SQLiteRow` with no columns or values.
public init() { public init() {
elements = [:] elements = [:]
} }
// MARK: - Subscripts // MARK: - Subscripts
/// Accesses the element at the specified index. /// Accesses the value associated with the specified column.
/// ///
/// This subscript allows you to retrieve a column-value pair from the row by its index. /// Use this subscript to read or modify the value of a particular column by name. If the column
/// It returns an `Element`, which is a tuple containing the column name and its associated /// does not exist, the getter returns `nil` and assigning a value to a new column name adds it
/// value. The index must be valid; otherwise, it will trigger a runtime error. /// to the row.
/// ///
/// - Parameter index: The index of the element to access. /// - Parameter column: The name of the column.
/// - Returns: A tuple containing the column name and its associated value. /// - Returns: The value for the specified column, or `nil` if the column is not present.
/// /// - Complexity: Average O(1) lookup and amortized O(1) mutation.
/// - 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? { public subscript(column: Column) -> Value? {
get { elements[column] } get { elements[column] }
set { elements[column] = newValue } set { elements[column] = newValue }
@@ -194,32 +67,132 @@ public struct SQLiteRow: Collection, CustomStringConvertible, Equatable {
// MARK: - Methods // MARK: - Methods
/// Returns the index immediately after the given index. /// Checks whether the row contains a column with the specified name.
/// ///
/// This method provides the next valid index in the row's collection after the specified index. /// Use this method to check if a column exists without retrieving its value or iterating
/// It increments the given index by one, allowing for iteration through the row's elements /// through all columns.
/// 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. /// - Parameter column: The name of the column to look for.
/// - Returns: The index immediately after `i`. /// - Returns: `true` if the column exists, otherwise `false`.
/// /// - Complexity: Average O(1).
/// - 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 { public func contains(_ column: Column) -> Bool {
elements.keys.contains(column) elements.keys.contains(column)
} }
/// Reserves enough storage to hold the specified number of columns.
///
/// Calling this method can minimize reallocations when adding multiple columns to the row.
///
/// - Parameter minimumCapacity: The requested number of column-value pairs to store.
/// - Complexity: O(max(count, minimumCapacity))
public mutating func reserveCapacity(_ minimumCapacity: Int) {
elements.reserveCapacity(minimumCapacity)
}
/// Reserves enough storage to hold the specified number of columns.
///
/// This overload provides a convenient interface for values originating from SQLite APIs, which
/// commonly use 32-bit integer sizes.
///
/// - Parameter minimumCapacity: The requested number of column-value pairs to store.
/// - Complexity: O(max(count, minimumCapacity))
public mutating func reserveCapacity(_ minimumCapacity: Int32) {
elements.reserveCapacity(Int(minimumCapacity))
}
} }
// MARK: - CustomStringConvertible
extension SQLiteRow: CustomStringConvertible {
/// A textual representation of the row as an ordered dictionary of column-value pairs.
public var description: String {
elements.description
}
}
// MARK: - Collection
extension SQLiteRow: Collection {
/// The element type of the row collection.
public typealias Element = (column: Column, value: Value)
/// The index type used to access elements in the row.
public typealias Index = OrderedDictionary<Column, Value>.Index
/// The position of the first element in the row.
///
/// If the row is empty, `startIndex` equals `endIndex`. Use this property as the starting
/// position when iterating over columns.
///
/// - Complexity: O(1)
public var startIndex: Index {
elements.elements.startIndex
}
/// The position one past the last valid element in the row.
///
/// Use this property to detect the end of iteration when traversing columns.
///
/// - Complexity: O(1)
public var endIndex: Index {
elements.elements.endIndex
}
/// A Boolean value that indicates whether the row contains no columns.
///
/// - Complexity: O(1)
public var isEmpty: Bool {
elements.isEmpty
}
/// The number of column-value pairs in the row.
///
/// - Complexity: O(1)
public var count: Int {
elements.count
}
/// Accesses the element at the specified position in the row.
///
/// - Parameter index: A valid index of the row.
/// - Returns: The (column, value) pair at the specified position.
/// - Complexity: O(1)
public subscript(index: Index) -> Element {
let element = elements.elements[index]
return (element.key, element.value)
}
/// Returns the position immediately after the specified index.
///
/// - Parameter i: A valid index of the row.
/// - Returns: The index immediately after `i`.
/// - Complexity: O(1)
public func index(after i: Index) -> Index {
elements.elements.index(after: i)
}
}
// MARK: - ExpressibleByDictionaryLiteral
extension SQLiteRow: ExpressibleByDictionaryLiteral {
/// Creates a `SQLiteRow` from a sequence of (column, value) pairs.
///
/// - Parameter elements: The column-value pairs to include in the row.
/// - Note: Preserves the argument order and requires unique column names.
/// - Complexity: O(n), where n is the number of pairs.
public init(dictionaryLiteral elements: (Column, Value)...) {
self.elements = .init(uniqueKeysWithValues: elements)
}
}
// MARK: - Equatable
extension SQLiteRow: Equatable {}
// MARK: - Hashable
extension SQLiteRow: Hashable {}
// MARK: - Sendable
extension SQLiteRow: Sendable {}

View File

@@ -17,15 +17,4 @@ struct ConnectionLocationTests {
let temporaryLocation = Connection.Location.temporary let temporaryLocation = Connection.Location.temporary
#expect(temporaryLocation.path == "") #expect(temporaryLocation.path == "")
} }
@Test func testFileLocationInitialization() {
let filePath = "/path/to/database.db"
let location = Connection.Location.file(path: filePath)
switch location {
case .file(let path):
#expect(path == filePath)
default:
Issue.record("Expected `.file` case but got \(location)")
}
}
} }

View File

@@ -101,7 +101,7 @@ struct ConnectionTests {
""") """)
#expect( #expect(
throws: Connection.Error( throws: SQLiteError(
code: SQLITE_BUSY, code: SQLITE_BUSY,
message: "database is locked" message: "database is locked"
), ),
@@ -229,7 +229,7 @@ struct ConnectionTests {
try connection.add(function: function) try connection.add(function: function)
try connection.remove(function: function) try connection.remove(function: function)
#expect( #expect(
throws: Connection.Error( throws: SQLiteError(
code: SQLITE_ERROR, code: SQLITE_ERROR,
message: "no such function: \(name)" message: "no such function: \(name)"
), ),
@@ -250,7 +250,7 @@ private extension ConnectionTests {
[.deterministic, .innocuous] [.deterministic, .innocuous]
} }
override class func invoke(args: Arguments) throws -> SQLiteRawRepresentable? { override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? {
args[0].description args[0].description
} }
} }
@@ -264,13 +264,13 @@ private extension ConnectionTests {
private var count: Int = 0 private var count: Int = 0
override func step(args: Arguments) throws { override func step(args: any ArgumentsProtocol) throws {
if args[0] != .null { if args[0] != .null {
count += 1 count += 1
} }
} }
override func finalize() throws -> SQLiteRawRepresentable? { override func finalize() throws -> SQLiteRepresentable? {
count count
} }
} }

View File

@@ -1,45 +1,41 @@
import Foundation
import Testing import Testing
import DataLiteCore
import DataLiteC import DataLiteC
import DataLiteCore
struct StatementOptionsTests { struct StatementOptionsTests {
@Test func testOptionsInitialization() { @Test func testPersistentOptions() {
let options: Statement.Options = [.persistent]
#expect(options.contains(.persistent))
#expect(options.contains(.noVtab) == false)
}
@Test func testOptionsCombination() {
var options: Statement.Options = [.persistent]
#expect(options.contains(.persistent))
#expect(options.contains(.noVtab) == false)
options.insert(.noVtab)
#expect(options.contains(.persistent))
#expect(options.contains(.noVtab))
}
@Test func testOptionsRemoval() {
var options: Statement.Options = [.persistent, .noVtab]
#expect(options.contains(.persistent))
#expect(options.contains(.noVtab))
options.remove(.noVtab)
#expect(options.contains(.persistent))
#expect(options.contains(.noVtab) == false)
}
@Test func testOptionsRawValue() {
let options: Statement.Options = [.persistent, .noVtab]
let rawOpts = UInt32(SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB)
#expect(options.rawValue == rawOpts)
#expect(Statement.Options.persistent.rawValue == UInt32(SQLITE_PREPARE_PERSISTENT)) #expect(Statement.Options.persistent.rawValue == UInt32(SQLITE_PREPARE_PERSISTENT))
}
@Test func testNoVtabOptions() {
#expect(Statement.Options.noVtab.rawValue == UInt32(SQLITE_PREPARE_NO_VTAB)) #expect(Statement.Options.noVtab.rawValue == UInt32(SQLITE_PREPARE_NO_VTAB))
} }
@Test func testCombineOptions() {
let options: Statement.Options = [.persistent, .noVtab]
let expected = UInt32(SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB)
#expect(options.contains(.persistent))
#expect(options.contains(.noVtab))
#expect(options.rawValue == expected)
}
@Test func testInitWithUInt32RawValue() {
let raw = UInt32(SQLITE_PREPARE_PERSISTENT)
let options = Statement.Options(rawValue: raw)
#expect(options == .persistent)
}
@Test func testInitWithInt32RawValue() {
let raw = Int32(SQLITE_PREPARE_NO_VTAB)
let options = Statement.Options(rawValue: raw)
#expect(options == .noVtab)
}
@Test func testEmptySetRawValueIsZero() {
let empty: Statement.Options = []
#expect(empty.rawValue == 0)
#expect(!empty.contains(.persistent))
#expect(!empty.contains(.noVtab))
}
} }

View File

@@ -1,135 +1,240 @@
import XCTest import Foundation
import Testing
import DataLiteC import DataLiteC
@testable import DataLiteCore @testable import DataLiteCore
final class StatementTests: XCTestCase { final class StatementTests {
private let databasePath = FileManager.default.temporaryDirectory.appendingPathComponent("test.db").path let connection: OpaquePointer
private var connection: OpaquePointer!
override func setUpWithError() throws { init() {
try super.setUpWithError() var connection: OpaquePointer! = nil
let opts = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE
XCTAssertEqual( sqlite3_open_v2(":memory:", &connection, opts, nil)
sqlite3_open(databasePath, &connection),
SQLITE_OK,
"Failed to open database"
)
XCTAssertEqual(
sqlite3_exec( sqlite3_exec(
connection, connection,
""" """
CREATE TABLE users ( CREATE TABLE t(
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT, n INTEGER,
age INTEGER r REAL,
s TEXT,
b BLOB
); );
""", """, nil, nil, nil
nil, nil, nil )
self.connection = connection
}
deinit {
sqlite3_close_v2(connection)
}
@Test func testInitWithError() throws {
#expect(
throws: SQLiteError(
code: SQLITE_ERROR,
message: "no such table: invalid"
), ),
SQLITE_OK, performing: {
"Failed to create table" try Statement(
db: connection,
sql: "SELECT * FROM invalid",
options: []
)
}
) )
} }
override func tearDownWithError() throws { @Test func testParameterCount() throws {
sqlite3_close(connection) let sql = "SELECT * FROM t WHERE id = ? AND s = ?"
try FileManager.default.removeItem(atPath: databasePath)
try super.tearDownWithError()
}
func testMixBindings() throws {
do {
let sql = "INSERT INTO users (name, age) VALUES (?, ?)"
let stmt = try Statement(db: connection, sql: sql, options: []) let stmt = try Statement(db: connection, sql: sql, options: [])
try stmt.bind("Alice", at: 1) #expect(stmt.parameterCount() == 2)
try stmt.bind(88, at: 2)
XCTAssertFalse(try stmt.step())
} }
do { @Test func testZeroParameterCount() throws {
let sql = "SELECT * FROM users WHERE age = ? AND name = $name" let sql = "SELECT * FROM t"
let stmt = try Statement(db: connection, sql: sql, options: []) let stmt = try Statement(db: connection, sql: sql, options: [])
try stmt.bind(88, at: 1) #expect(stmt.parameterCount() == 0)
try stmt.bind("Alice", at: stmt.bind(parameterIndexBy: "$name"))
XCTAssertTrue(try stmt.step())
XCTAssertEqual(stmt.columnValue(at: 1), "Alice")
XCTAssertEqual(stmt.columnValue(at: 2), 88)
}
} }
func testStatementInitialization() throws { @Test func testParameterIndexByName() throws {
let sql = "INSERT INTO users (name, age) VALUES (?, ?)" let sql = "SELECT * FROM t WHERE id = :id AND s = :s"
let statement = try Statement(db: connection, sql: sql, options: [.persistent]) let stmt = try Statement(db: connection, sql: sql, options: [])
XCTAssertNotNil(statement, "Statement should not be nil") #expect(stmt.parameterIndexBy(":id") == 1)
#expect(stmt.parameterIndexBy(":s") == 2)
#expect(stmt.parameterIndexBy(":invalid") == 0)
} }
func testBindAndExecute() throws { @Test func testParameterNameByIndex() throws {
let sql = "INSERT INTO users (name, age) VALUES (?, ?)" let sql = "SELECT * FROM t WHERE id = :id AND s = :s"
let statement = try Statement(db: connection, sql: sql, options: [.persistent]) let stmt = try Statement(db: connection, sql: sql, options: [])
try statement.bind("Alice", at: 1) #expect(stmt.parameterNameBy(1) == ":id")
try statement.bind(30, at: 2) #expect(stmt.parameterNameBy(2) == ":s")
XCTAssertEqual(statement.bindParameterCount(), 2) #expect(stmt.parameterNameBy(3) == nil)
XCTAssertFalse(try statement.step())
let query = "SELECT * FROM users WHERE name = ?"
let queryStatement = try Statement(db: connection, sql: query, options: [.persistent])
try queryStatement.bind("Alice", at: 1)
XCTAssertTrue(try queryStatement.step(), "Failed to execute SELECT query")
XCTAssertEqual(queryStatement.columnValue(at: 1), "Alice")
XCTAssertEqual(queryStatement.columnValue(at: 2), 30)
} }
func testClearBindings() throws { @Test func testBindValueAtIndex() throws {
let sql = "INSERT INTO users (name, age) VALUES (?, ?)" let sql = "SELECT * FROM t where id = ?"
let statement = try Statement(db: connection, sql: sql, options: [.persistent]) let stmt = try Statement(db: connection, sql: sql, options: [])
try statement.bind("Bob", at: 1) try stmt.bind(.int(42), at: 1)
try statement.bind(25, at: 2) try stmt.bind(.real(42), at: 1)
try statement.clearBindings() try stmt.bind(.text("42"), at: 1)
XCTAssertFalse(try statement.step()) try stmt.bind(.blob(Data([0x42])), at: 1)
try stmt.bind(.null, at: 1)
try stmt.bind(TestValue(value: 42), at: 1)
try stmt.bind(TestValue?.none, at: 1)
} }
func testResetStatement() throws { @Test func testErrorBindValueAtIndex() throws {
let sql = "INSERT INTO users (name, age) VALUES (?, ?)" let sql = "SELECT * FROM t where id = ?"
let statement = try Statement(db: connection, sql: sql, options: [.persistent]) let stmt = try Statement(db: connection, sql: sql, options: [])
try statement.bind("Charlie", at: 1) #expect(
try statement.bind(40, at: 2) throws: SQLiteError(
try statement.step() code: SQLITE_RANGE,
message: "column index out of range"
// Reset the statement and try executing it again with new values ),
try statement.reset() performing: {
try statement.bind("Dave", at: 1) try stmt.bind(.null, at: 0)
try statement.bind(45, at: 2) }
XCTAssertEqual(statement.bindParameterCount(), 2) )
XCTAssertFalse(try statement.step())
// Check if the record was actually inserted
let query = "SELECT * FROM users WHERE name = ?"
let queryStatement = try Statement(db: connection, sql: query, options: [.persistent])
try queryStatement.bind("Dave", at: 1)
XCTAssertTrue(try queryStatement.step(), "Failed to execute SELECT query")
XCTAssertEqual(queryStatement.columnValue(at: 1), "Dave")
XCTAssertEqual(queryStatement.columnValue(at: 2), 45)
} }
func testColumnValues() throws { @Test func testBindValueByName() throws {
let sql = "INSERT INTO users (name, age) VALUES (?, ?)" let sql = "SELECT * FROM t where id = :id"
let statement = try Statement(db: connection, sql: sql, options: [.persistent]) let stmt = try Statement(db: connection, sql: sql, options: [])
try statement.bind("Eve", at: 1) try stmt.bind(.int(42), by: ":id")
try statement.bind(28, at: 2) try stmt.bind(.real(42), by: ":id")
try statement.step() try stmt.bind(.text("42"), by: ":id")
try stmt.bind(.blob(Data([0x42])), by: ":id")
try stmt.bind(.null, by: ":id")
try stmt.bind(TestValue(value: 42), by: ":id")
try stmt.bind(TestValue?.none, by: ":id")
}
// Perform a SELECT query and check column data types @Test func testErrorBindValueByName() throws {
let query = "SELECT * FROM users WHERE name = ?" let sql = "SELECT * FROM t where id = :id"
let queryStatement = try Statement(db: connection, sql: query, options: [.persistent]) let stmt = try Statement(db: connection, sql: sql, options: [])
try queryStatement.bind("Eve", at: 1) #expect(
throws: SQLiteError(
code: SQLITE_RANGE,
message: "column index out of range"
),
performing: {
try stmt.bind(.null, by: ":invalid")
}
)
}
XCTAssertTrue(try queryStatement.step(), "Failed to execute SELECT query") @Test func testStepOneRow() throws {
XCTAssertEqual(queryStatement.columnType(at: 1), .text) let sql = "SELECT 1 where 1"
XCTAssertEqual(queryStatement.columnType(at: 2), .int) let stmt = try Statement(db: connection, sql: sql, options: [])
XCTAssertEqual(queryStatement.columnValue(at: 1), "Eve") #expect(try stmt.step())
XCTAssertEqual(queryStatement.columnValue(at: 2), 28) #expect(try stmt.step() == false)
}
@Test func testStepMultipleRows() throws {
sqlite3_exec(connection, "INSERT INTO t(n) VALUES (1),(2),(3)", nil, nil, nil)
let sql = "SELECT id FROM t ORDER BY id"
let stmt = try Statement(db: connection, sql: sql, options: [])
#expect(try stmt.step())
#expect(try stmt.step())
#expect(try stmt.step())
#expect(try stmt.step() == false)
}
@Test func testStepNoRows() throws {
let sql = "SELECT 1 WHERE 0"
let stmt = try Statement(db: connection, sql: sql, options: [])
#expect(try stmt.step() == false)
}
@Test func testStepWithError() throws {
sqlite3_exec(connection, "INSERT INTO t(id, n) VALUES (1, 10)", nil, nil, nil)
let sql = "INSERT INTO t(id, n) VALUES (?, ?)"
let stmt = try Statement(db: connection, sql: sql, options: [])
try stmt.bind(.int(1), at: 1)
try stmt.bind(.int(20), at: 2)
#expect(
throws: SQLiteError(
code: 1555,
message: "UNIQUE constraint failed: t.id"
),
performing: {
try stmt.step()
}
)
}
@Test func testColumnCount() throws {
let sql = "SELECT * FROM t"
let stmt = try Statement(db: connection, sql: sql, options: [])
#expect(stmt.columnCount() == 5)
}
@Test func testColumnName() throws {
let sql = "SELECT * FROM t"
let stmt = try Statement(db: connection, sql: sql, options: [])
#expect(stmt.columnName(at: 0) == "id")
#expect(stmt.columnName(at: 1) == "n")
#expect(stmt.columnName(at: 2) == "r")
#expect(stmt.columnName(at: 3) == "s")
#expect(stmt.columnName(at: 4) == "b")
}
@Test func testColumnValueAtIndex() throws {
sqlite3_exec(connection, """
INSERT INTO t (id, n, r, s, b)
VALUES (10, 42, 3.5, 'hello', x'DEADBEEF')
""", nil, nil, nil
)
let sql = "SELECT * FROM t WHERE id = 10"
let stmt = try Statement(db: connection, sql: sql, options: [])
#expect(try stmt.step())
#expect(stmt.columnValue(at: 0) == .int(10))
#expect(stmt.columnValue(at: 1) == .int(42))
#expect(stmt.columnValue(at: 1) == TestValue(value: 42))
#expect(stmt.columnValue(at: 2) == .real(3.5))
#expect(stmt.columnValue(at: 3) == .text("hello"))
#expect(stmt.columnValue(at: 4) == .blob(Data([0xDE, 0xAD, 0xBE, 0xEF])))
}
@Test func testColumnNullValueAtIndex() throws {
sqlite3_exec(connection, """
INSERT INTO t (id) VALUES (10)
""", nil, nil, nil
)
let sql = "SELECT * FROM t WHERE id = 10"
let stmt = try Statement(db: connection, sql: sql, options: [])
#expect(try stmt.step())
#expect(stmt.columnValue(at: 0) == .int(10))
#expect(stmt.columnValue(at: 1) == .null)
#expect(stmt.columnValue(at: 1) == TestValue?.none)
}
}
private extension StatementTests {
struct TestValue: SQLiteRepresentable, Equatable {
let value: Int
var sqliteValue: SQLiteValue {
.int(Int64(value))
}
init(value: Int) {
self.value = value
}
init?(_ value: SQLiteValue) {
if case .int(let intValue) = value {
self.value = Int(intValue)
} else {
return nil
}
}
} }
} }

View File

@@ -1,27 +0,0 @@
import Testing
import DataLiteC
import DataLiteCore
struct SQLiteRawTypeTests {
@Test func testInitializationFromRawValue() {
#expect(SQLiteRawType(rawValue: SQLITE_INTEGER) == .int)
#expect(SQLiteRawType(rawValue: SQLITE_FLOAT) == .real)
#expect(SQLiteRawType(rawValue: SQLITE_TEXT) == .text)
#expect(SQLiteRawType(rawValue: SQLITE_BLOB) == .blob)
#expect(SQLiteRawType(rawValue: SQLITE_NULL) == .null)
#expect(SQLiteRawType(rawValue: -1) == nil)
}
@Test func testRawValue() {
#expect(SQLiteRawType.int.rawValue == SQLITE_INTEGER)
#expect(SQLiteRawType.real.rawValue == SQLITE_FLOAT)
#expect(SQLiteRawType.text.rawValue == SQLITE_TEXT)
#expect(SQLiteRawType.blob.rawValue == SQLITE_BLOB)
#expect(SQLiteRawType.null.rawValue == SQLITE_NULL)
}
@Test func testInvalidRawValue() {
let invalidRawValue: Int32 = 9999
#expect(SQLiteRawType(rawValue: invalidRawValue) == nil)
}
}

View File

@@ -1,36 +0,0 @@
import Testing
import Foundation
import DataLiteCore
struct SQLiteRawValueTests {
@Test func testIntValue() {
let value = SQLiteRawValue.int(42)
#expect(value.description == "42")
}
@Test func testRealValue() {
let value = SQLiteRawValue.real(3.14)
#expect(value.description == "3.14")
}
@Test func testTextValue() {
let value = SQLiteRawValue.text("Hello, World!")
#expect(value.description == "'Hello, World!'")
}
@Test func testTextValueWithSingleQuote() {
let value = SQLiteRawValue.text("O'Reilly")
#expect(value.description == "'O''Reilly'") // Escaped single quote
}
@Test func testBlobValue() {
let data = Data([0xDE, 0xAD, 0xBE, 0xEF])
let value = SQLiteRawValue.blob(data)
#expect(value.description == "X'DEADBEEF'")
}
@Test func testNullValue() {
let value = SQLiteRawValue.null
#expect(value.description == "NULL")
}
}

View File

@@ -0,0 +1,49 @@
import Foundation
import Testing
import DataLiteCore
struct SQLiteValueTests {
@Test(arguments: [1, 42, 1234])
func testSQLiteIntValue(_ value: Int64) {
let value = SQLiteValue.int(value)
#expect(value.sqliteLiteral == "\(value)")
#expect(value.description == value.sqliteLiteral)
}
@Test(arguments: [12, 0.5, 123.99])
func testSQLiteRealValue(_ value: Double) {
let value = SQLiteValue.real(value)
#expect(value.sqliteLiteral == "\(value)")
#expect(value.description == value.sqliteLiteral)
}
@Test(arguments: [
("", "''"),
("'hello'", "'''hello'''"),
("hello", "'hello'"),
("O'Reilly", "'O''Reilly'"),
("It's John's \"book\"", "'It''s John''s \"book\"'")
])
func testSQLiteTextValue(_ value: String, _ expected: String) {
let value = SQLiteValue.text(value)
#expect(value.sqliteLiteral == expected)
#expect(value.description == value.sqliteLiteral)
}
@Test(arguments: [
(Data(), "X''"),
(Data([0x00]), "X'00'"),
(Data([0x00, 0xAB, 0xCD]), "X'00ABCD'")
])
func testSQLiteBlobValue(_ value: Data, _ expected: String) {
let value = SQLiteValue.blob(value)
#expect(value.sqliteLiteral == expected)
#expect(value.description == value.sqliteLiteral)
}
@Test func testSQLiteNullValue() {
let value = SQLiteValue.null
#expect(value.sqliteLiteral == "NULL")
#expect(value.description == value.sqliteLiteral)
}
}

View File

@@ -4,46 +4,46 @@ import DataLiteCore
struct BinaryFloatingPointTests { struct BinaryFloatingPointTests {
@Test func testFloatToSQLiteRawValue() { @Test func testFloatToSQLiteRawValue() {
let floatValue: Float = 3.14 let floatValue: Float = 3.14
let rawValue = floatValue.sqliteRawValue let rawValue = floatValue.sqliteValue
#expect(rawValue == .real(Double(floatValue))) #expect(rawValue == .real(Double(floatValue)))
} }
@Test func testDoubleToSQLiteRawValue() { @Test func testDoubleToSQLiteRawValue() {
let doubleValue: Double = 3.14 let doubleValue: Double = 3.14
let rawValue = doubleValue.sqliteRawValue let rawValue = doubleValue.sqliteValue
#expect(rawValue == .real(doubleValue)) #expect(rawValue == .real(doubleValue))
} }
@Test func testFloatInitializationFromSQLiteRawValue() { @Test func testFloatInitializationFromSQLiteRawValue() {
let realValue: SQLiteRawValue = .real(3.14) let realValue: SQLiteValue = .real(3.14)
let floatValue = Float(realValue) let floatValue = Float(realValue)
#expect(floatValue != nil) #expect(floatValue != nil)
#expect(floatValue == 3.14) #expect(floatValue == 3.14)
let intValue: SQLiteRawValue = .int(42) let intValue: SQLiteValue = .int(42)
let floatFromInt = Float(intValue) let floatFromInt = Float(intValue)
#expect(floatFromInt != nil) #expect(floatFromInt != nil)
#expect(floatFromInt == 42.0) #expect(floatFromInt == 42.0)
} }
@Test func testDoubleInitializationFromSQLiteRawValue() { @Test func testDoubleInitializationFromSQLiteRawValue() {
let realValue: SQLiteRawValue = .real(3.14) let realValue: SQLiteValue = .real(3.14)
let doubleValue = Double(realValue) let doubleValue = Double(realValue)
#expect(doubleValue != nil) #expect(doubleValue != nil)
#expect(doubleValue == 3.14) #expect(doubleValue == 3.14)
let intValue: SQLiteRawValue = .int(42) let intValue: SQLiteValue = .int(42)
let doubleFromInt = Double(intValue) let doubleFromInt = Double(intValue)
#expect(doubleFromInt != nil) #expect(doubleFromInt != nil)
#expect(doubleFromInt == 42.0) #expect(doubleFromInt == 42.0)
} }
@Test func testInitializationFailureFromInvalidSQLiteRawValue() { @Test func testInitializationFailureFromInvalidSQLiteRawValue() {
let nullValue: SQLiteRawValue = .null let nullValue: SQLiteValue = .null
#expect(Float(nullValue) == nil) #expect(Float(nullValue) == nil)
#expect(Double(nullValue) == nil) #expect(Double(nullValue) == nil)
let textValue: SQLiteRawValue = .text("Invalid") let textValue: SQLiteValue = .text("Invalid")
#expect(Float(textValue) == nil) #expect(Float(textValue) == nil)
#expect(Double(textValue) == nil) #expect(Double(textValue) == nil)
} }

View File

@@ -3,38 +3,38 @@ import Foundation
import DataLiteCore import DataLiteCore
struct BinaryIntegerTests { struct BinaryIntegerTests {
@Test func testIntegerToSQLiteRawValue() { @Test func testIntegerToSQLiteValue() {
#expect(Int(42).sqliteRawValue == .int(42)) #expect(Int(42).sqliteValue == .int(42))
#expect(Int8(42).sqliteRawValue == .int(42)) #expect(Int8(42).sqliteValue == .int(42))
#expect(Int16(42).sqliteRawValue == .int(42)) #expect(Int16(42).sqliteValue == .int(42))
#expect(Int32(42).sqliteRawValue == .int(42)) #expect(Int32(42).sqliteValue == .int(42))
#expect(Int64(42).sqliteRawValue == .int(42)) #expect(Int64(42).sqliteValue == .int(42))
#expect(UInt(42).sqliteRawValue == .int(42)) #expect(UInt(42).sqliteValue == .int(42))
#expect(UInt8(42).sqliteRawValue == .int(42)) #expect(UInt8(42).sqliteValue == .int(42))
#expect(UInt16(42).sqliteRawValue == .int(42)) #expect(UInt16(42).sqliteValue == .int(42))
#expect(UInt32(42).sqliteRawValue == .int(42)) #expect(UInt32(42).sqliteValue == .int(42))
#expect(UInt64(42).sqliteRawValue == .int(42)) #expect(UInt64(42).sqliteValue == .int(42))
} }
@Test func testIntegerInitializationFromSQLiteRawValue() { @Test func testIntegerInitializationFromSQLiteValue() {
#expect(Int(SQLiteRawValue.int(42)) == 42) #expect(Int(SQLiteValue.int(42)) == 42)
#expect(Int8(SQLiteRawValue.int(42)) == 42) #expect(Int8(SQLiteValue.int(42)) == 42)
#expect(Int16(SQLiteRawValue.int(42)) == 42) #expect(Int16(SQLiteValue.int(42)) == 42)
#expect(Int32(SQLiteRawValue.int(42)) == 42) #expect(Int32(SQLiteValue.int(42)) == 42)
#expect(Int64(SQLiteRawValue.int(42)) == 42) #expect(Int64(SQLiteValue.int(42)) == 42)
#expect(UInt(SQLiteRawValue.int(42)) == 42) #expect(UInt(SQLiteValue.int(42)) == 42)
#expect(UInt8(SQLiteRawValue.int(42)) == 42) #expect(UInt8(SQLiteValue.int(42)) == 42)
#expect(UInt16(SQLiteRawValue.int(42)) == 42) #expect(UInt16(SQLiteValue.int(42)) == 42)
#expect(UInt32(SQLiteRawValue.int(42)) == 42) #expect(UInt32(SQLiteValue.int(42)) == 42)
#expect(UInt64(SQLiteRawValue.int(42)) == 42) #expect(UInt64(SQLiteValue.int(42)) == 42)
} }
@Test func testInvalidIntegerInitialization() { @Test func testInvalidIntegerInitialization() {
#expect(Int(SQLiteRawValue.real(3.14)) == nil) #expect(Int(SQLiteValue.real(3.14)) == nil)
#expect(Int8(SQLiteRawValue.text("test")) == nil) #expect(Int8(SQLiteValue.text("test")) == nil)
#expect(UInt32(SQLiteRawValue.blob(Data([0x01, 0x02]))) == nil) #expect(UInt32(SQLiteValue.blob(Data([0x01, 0x02]))) == nil)
// Out-of-range conversion // Out-of-range conversion
let largeValue = Int64.max let largeValue = Int64.max

View File

@@ -4,8 +4,8 @@ import DataLiteCore
struct BoolTests { struct BoolTests {
@Test func testBoolToSQLiteRawValue() { @Test func testBoolToSQLiteRawValue() {
#expect(true.sqliteRawValue == .int(1)) #expect(true.sqliteValue == .int(1))
#expect(false.sqliteRawValue == .int(0)) #expect(false.sqliteValue == .int(0))
} }
@Test func testSQLiteRawValueToBool() { @Test func testSQLiteRawValueToBool() {

View File

@@ -5,12 +5,12 @@ import DataLiteCore
struct DataSQLiteRawRepresentableTests { struct DataSQLiteRawRepresentableTests {
@Test func testDataToSQLiteRawValue() { @Test func testDataToSQLiteRawValue() {
let data = Data([0x01, 0x02, 0x03]) let data = Data([0x01, 0x02, 0x03])
#expect(data.sqliteRawValue == .blob(data)) #expect(data.sqliteValue == .blob(data))
} }
@Test func testSQLiteRawValueToData() { @Test func testSQLiteRawValueToData() {
let data = Data([0x01, 0x02, 0x03]) let data = Data([0x01, 0x02, 0x03])
let rawValue = SQLiteRawValue.blob(data) let rawValue = SQLiteValue.blob(data)
#expect(Data(rawValue) == data) #expect(Data(rawValue) == data)

View File

@@ -8,7 +8,7 @@ struct DateSQLiteRawRepresentableTests {
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
let dateString = formatter.string(from: date) let dateString = formatter.string(from: date)
#expect(date.sqliteRawValue == .text(dateString)) #expect(date.sqliteValue == .text(dateString))
} }
@Test func testSQLiteRawValueToDate() { @Test func testSQLiteRawValueToDate() {
@@ -16,13 +16,13 @@ struct DateSQLiteRawRepresentableTests {
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
let dateString = formatter.string(from: date) let dateString = formatter.string(from: date)
let rawText = SQLiteRawValue.text(dateString) let rawText = SQLiteValue.text(dateString)
#expect(Date(rawText) == date) #expect(Date(rawText) == date)
let rawInt = SQLiteRawValue.int(1609459200) let rawInt = SQLiteValue.int(1609459200)
#expect(Date(rawInt) == date) #expect(Date(rawInt) == date)
let rawReal = SQLiteRawValue.real(1609459200) let rawReal = SQLiteValue.real(1609459200)
#expect(Date(rawReal) == date) #expect(Date(rawReal) == date)
#expect(Date(.blob(Data([0x01, 0x02, 0x03]))) == nil) #expect(Date(.blob(Data([0x01, 0x02, 0x03]))) == nil)

View File

@@ -5,7 +5,7 @@ import DataLiteCore
struct RawRepresentableTests { struct RawRepresentableTests {
@Test func testRawRepresentableToSQLiteRawValue() { @Test func testRawRepresentableToSQLiteRawValue() {
let color: Color = .green let color: Color = .green
#expect(color.sqliteRawValue == .int(1)) #expect(color.sqliteValue == .int(1))
} }
@Test func testSQLiteRawValueToRawRepresentable() { @Test func testSQLiteRawValueToRawRepresentable() {
@@ -19,7 +19,7 @@ struct RawRepresentableTests {
} }
private extension RawRepresentableTests { private extension RawRepresentableTests {
enum Color: Int, SQLiteRawRepresentable { enum Color: Int, SQLiteRepresentable {
case red case red
case green case green
case blue case blue

View File

@@ -4,14 +4,14 @@ import DataLiteCore
struct StringTests { struct StringTests {
@Test func testStringToSQLiteRawValue() { @Test func testStringToSQLiteRawValue() {
#expect("Hello, SQLite!".sqliteRawValue == .text("Hello, SQLite!")) #expect("Hello, SQLite!".sqliteValue == .text("Hello, SQLite!"))
} }
@Test func testSQLiteRawValueToString() { @Test func testSQLiteRawValueToString() {
#expect(String(SQLiteRawValue.text("Hello, SQLite!")) == "Hello, SQLite!") #expect(String(SQLiteValue.text("Hello, SQLite!")) == "Hello, SQLite!")
#expect(String(SQLiteRawValue.int(42)) == nil) #expect(String(SQLiteValue.int(42)) == nil)
#expect(String(SQLiteRawValue.blob(Data([0x01, 0x02]))) == nil) #expect(String(SQLiteValue.blob(Data([0x01, 0x02]))) == nil)
#expect(String(SQLiteRawValue.null) == nil) #expect(String(SQLiteValue.null) == nil)
} }
} }

View File

@@ -5,11 +5,11 @@ import DataLiteCore
struct UUIDTests { struct UUIDTests {
@Test func testUUIDToSQLiteRawValue() { @Test func testUUIDToSQLiteRawValue() {
let uuid = UUID(uuidString: "123e4567-e89b-12d3-a456-426614174000")! let uuid = UUID(uuidString: "123e4567-e89b-12d3-a456-426614174000")!
#expect(uuid.sqliteRawValue == .text("123E4567-E89B-12D3-A456-426614174000")) #expect(uuid.sqliteValue == .text("123E4567-E89B-12D3-A456-426614174000"))
} }
@Test func testSQLiteRawValueToUUID() { @Test func testSQLiteRawValueToUUID() {
let raw = SQLiteRawValue.text("123e4567-e89b-12d3-a456-426614174000") let raw = SQLiteValue.text("123e4567-e89b-12d3-a456-426614174000")
#expect(UUID(raw) == UUID(uuidString: "123e4567-e89b-12d3-a456-426614174000")) #expect(UUID(raw) == UUID(uuidString: "123e4567-e89b-12d3-a456-426614174000"))
#expect(UUID(.text("invalid-uuid-string")) == nil) #expect(UUID(.text("invalid-uuid-string")) == nil)

View File

@@ -0,0 +1,22 @@
import Foundation
import Testing
import DataLiteCore
private struct BindableStub: SQLiteBindable {
let value: SQLiteValue
var sqliteValue: SQLiteValue { value }
}
struct SQLiteBindableTests {
@Test(arguments: [
SQLiteValue.int(42),
SQLiteValue.real(0.5),
SQLiteValue.text("O'Reilly"),
SQLiteValue.blob(Data([0x00, 0xAB])),
SQLiteValue.null
])
func testDefaultSqliteLiteralPassThrough(_ value: SQLiteValue) {
let stub = BindableStub(value: value)
#expect(stub.sqliteLiteral == value.sqliteLiteral)
}
}

View File

@@ -3,26 +3,26 @@ import Testing
import DataLiteC import DataLiteC
@testable import DataLiteCore @testable import DataLiteCore
struct ConnectionErrorTests { struct SQLiteErrorTests {
@Test func testInitWithConnection() { @Test func testInitWithConnection() {
var db: OpaquePointer? = nil var db: OpaquePointer? = nil
defer { sqlite3_close(db) } defer { sqlite3_close(db) }
sqlite3_open(":memory:", &db) sqlite3_open(":memory:", &db)
sqlite3_exec(db, "INVALID SQL", nil, nil, nil) sqlite3_exec(db, "INVALID SQL", nil, nil, nil)
let error = Connection.Error(db!) let error = SQLiteError(db!)
#expect(error.code == SQLITE_ERROR) #expect(error.code == SQLITE_ERROR)
#expect(error.message == "near \"INVALID\": syntax error") #expect(error.message == "near \"INVALID\": syntax error")
} }
@Test func testInitWithCodeAndMessage() { @Test func testInitWithCodeAndMessage() {
let error = Connection.Error(code: 1, message: "Test Error Message") let error = SQLiteError(code: 1, message: "Test Error Message")
#expect(error.code == 1) #expect(error.code == 1)
#expect(error.message == "Test Error Message") #expect(error.message == "Test Error Message")
} }
@Test func testDescription() { @Test func testDescription() {
let error = Connection.Error(code: 1, message: "Test Error Message") let error = SQLiteError(code: 1, message: "Test Error Message")
#expect(error.description == "Connection.Error code: 1 message: Test Error Message") #expect(error.description == "SQLiteError code: 1 message: Test Error Message")
} }
} }

View File

@@ -26,7 +26,7 @@ final class SQLiteRowTests: XCTestCase {
XCTAssertEqual(row["name"], .text("Alice")) XCTAssertEqual(row["name"], .text("Alice"))
XCTAssertNil(row["age"]) XCTAssertNil(row["age"])
row["age"] = SQLiteRawValue.int(30) row["age"] = SQLiteValue.int(30)
XCTAssertEqual(row["age"], .int(30)) XCTAssertEqual(row["age"], .int(30))
} }