diff --git a/README.md b/README.md index 89d1982..88f148b 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ DataLiteCore provides an object-oriented API over the C interface, allowing deve ## Requirements -- **Swift**: 5.10+ -- **Platforms**: macOS 10.14+, iOS 12.0+, Linux +- **Swift**: 6.0+ +- **Platforms**: macOS 10.14+, iOS 12.0+ ## 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: ```swift -// swift-tools-version: 5.10 +// swift-tools-version: 6.0 import PackageDescription let package = Package( diff --git a/Sources/DataLiteCore/Classes/Connection+Error.swift b/Sources/DataLiteCore/Classes/Connection+Error.swift deleted file mode 100644 index bf6a6cd..0000000 --- a/Sources/DataLiteCore/Classes/Connection+Error.swift +++ /dev/null @@ -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)) - } - } -} diff --git a/Sources/DataLiteCore/Classes/Connection+Key.swift b/Sources/DataLiteCore/Classes/Connection+Key.swift index f127abb..09fb4e9 100644 --- a/Sources/DataLiteCore/Classes/Connection+Key.swift +++ b/Sources/DataLiteCore/Classes/Connection+Key.swift @@ -1,41 +1,39 @@ import Foundation extension Connection { - /// An encryption key for accessing an encrypted SQLite database. + /// An encryption key for opening an encrypted SQLite database. /// - /// Used after the connection is opened to unlock the contents of the database. - /// Two formats are supported: a passphrase with subsequent derivation, and - /// a raw 256-bit key (32 bytes) without transformation. + /// The key is applied after the connection is established to unlock the database contents. + /// Two formats are supported: + /// - a passphrase, which undergoes key derivation; + /// - a raw 256-bit key (32 bytes) passed without transformation. public enum Key { - /// A passphrase used to derive an encryption key. + /// A human-readable passphrase used for key derivation. /// - /// Intended for human-readable strings such as passwords or PIN codes. - /// The string is passed directly without escaping or quoting. + /// The passphrase is supplied as-is and processed by the underlying key derivation + /// mechanism configured in the database engine. case passphrase(String) /// A raw 256-bit encryption key (32 bytes). /// - /// No key derivation is performed. The key is passed as-is and must be - /// securely generated and stored. + /// The key is passed directly to the database without derivation. It must be securely + /// generated and stored. case rawKey(Data) - /// The string value to be passed to the database engine. + /// The string value passed to the database engine. /// - /// For `.passphrase`, this is the raw string exactly as provided. - /// For `.rawKey`, this is a hexadecimal literal in the format `X'...'`. + /// For `.passphrase`, returns the passphrase exactly as provided. + /// For `.rawKey`, returns a hexadecimal literal in the format `X'...'`. public var keyValue: String { switch self { case .passphrase(let string): - return string + string case .rawKey(let data): - return data.sqliteLiteral + data.sqliteLiteral } } - /// The length of the key value in bytes. - /// - /// Returns the number of bytes in the UTF-8 encoding of `keyValue`, - /// not the length of the original key or string. + /// The number of bytes in the string representation of the key. public var length: Int32 { Int32(keyValue.utf8.count) } diff --git a/Sources/DataLiteCore/Classes/Connection+Location.swift b/Sources/DataLiteCore/Classes/Connection+Location.swift index 5711440..26ed15a 100644 --- a/Sources/DataLiteCore/Classes/Connection+Location.swift +++ b/Sources/DataLiteCore/Classes/Connection+Location.swift @@ -1,140 +1,43 @@ import Foundation extension Connection { - /// The `Location` enum represents different locations for a SQLite database. + /// A location specifying where the SQLite database is stored or created. /// - /// This enum allows you to specify how and where a SQLite database will be stored or accessed. - /// You can choose from three options: - /// - /// - **File**: A database located at a specified file path or URI. This option is suitable - /// for persistent storage and can reference any valid file location in the filesystem or - /// a URI. - /// - /// - **In-Memory**: An in-memory database that exists only in RAM. This option is useful - /// for temporary data processing, testing, or scenarios where persistence is not required. - /// - /// - **Temporary**: A temporary database on disk that is created for the duration of the - /// connection and is automatically deleted when the connection is closed or when the - /// process ends. - /// - /// ### Usage - /// - /// You can create instances of the `Location` enum to specify the desired database location: - /// - /// ```swift - /// let fileLocation = Connection.Location.file(path: "/path/to/database.db") - /// let inMemoryLocation = Connection.Location.inMemory - /// let temporaryLocation = Connection.Location.temporary - /// ``` + /// Three locations are supported: + /// - ``file(path:)``: A database at a specific file path or URI (persistent). + /// - ``inMemory``: An in-memory database that exists only in RAM. + /// - ``temporary``: A temporary on-disk database deleted when the connection closes. public enum Location { - /// A database located at a given file path or URI. + /// A database stored at a given file path or URI. /// - /// This case allows you to specify the exact location of a SQLite database using a file - /// path or a URI. The provided path should point to a valid SQLite database file. If the - /// database file does not exist, the behavior will depend on the connection options - /// specified when opening the database. + /// Use this for persistent databases located on disk or referenced via SQLite URI. + /// The file is created if it does not exist (subject to open options). /// - /// - Parameter path: The path or URI to the database file. This can be an absolute or - /// relative path, or a URI scheme supported by SQLite. - /// - /// ### Example - /// - /// You can create a `Location.file` case as follows: - /// - /// ```swift - /// let databaseLocation = Connection.Location.file(path: "/path/to/database.db") - /// ``` - /// - /// - Important: Ensure that the specified path is correct and that your application has - /// the necessary permissions to access the file. - /// - /// For more details, refer to [Uniform Resource Identifiers](https://www.sqlite.org/uri.html). + /// - Parameter path: Absolute/relative file path or URI. + /// - SeeAlso: [Uniform Resource Identifiers](https://sqlite.org/uri.html) case file(path: String) - /// An in-memory database. + /// A transient in-memory database. /// - /// In-memory databases are temporary and exist only in RAM. They are not persisted to disk, - /// which makes them suitable for scenarios where you need fast access to data without the - /// overhead of disk I/O. + /// The database exists only in RAM and is discarded once the connection closes. + /// Suitable for testing, caching, or temporary data processing. /// - /// When you create an in-memory database, it is stored entirely in memory, meaning that - /// all data will be lost when the connection is closed or the application exits. - /// - /// ### Usage - /// - /// You can specify an in-memory database as follows: - /// - /// ```swift - /// let databaseLocation = Connection.Location.inMemory - /// ``` - /// - /// - Important: In-memory databases should only be used for scenarios where persistence is - /// not required, such as temporary data processing or testing. - /// - /// - Note: In-memory databases can provide significantly faster performance compared to - /// disk-based databases due to the absence of disk I/O operations. - /// - /// For more details, refer to [In-Memory Databases](https://www.sqlite.org/inmemorydb.html). + /// - SeeAlso: [In-Memory Databases](https://sqlite.org/inmemorydb.html) case inMemory - /// A temporary database on disk. + /// A temporary on-disk database. /// - /// Temporary databases are created on disk but are not intended for persistent storage. They - /// are automatically deleted when the connection is closed or when the process ends. This - /// allows you to use a database for temporary operations without worrying about the overhead - /// of file management. + /// Created on disk and removed automatically when the connection closes or the + /// process terminates. Useful for ephemeral data that should not persist. /// - /// Temporary databases can be useful for scenarios such as: - /// - Testing database operations without affecting permanent data. - /// - Storing transient data that only needs to be accessible during a session. - /// - /// ### Usage - /// - /// You can specify a temporary database as follows: - /// - /// ```swift - /// let databaseLocation = Connection.Location.temporary - /// ``` - /// - /// - Important: Since temporary databases are deleted when the connection is closed, make - /// sure to use this option only for non-persistent data requirements. - /// - /// For more details, refer to [Temporary Databases](https://www.sqlite.org/inmemorydb.html). + /// - SeeAlso: [Temporary Databases](https://sqlite.org/inmemorydb.html) case temporary - /// Returns the path to the database. - /// - /// This computed property provides the appropriate path representation for the selected - /// `Location` case. Depending on the case, it returns: - /// - The specified file path for `.file`. - /// - The string `":memory:"` for in-memory databases, indicating that the database exists - /// only in RAM. - /// - An empty string for temporary databases, as these are created on disk but do not - /// require a specific file path. - /// - /// ### Usage - /// - /// You can access the `path` property as follows: - /// - /// ```swift - /// let location = Connection.Location.file(path: "/path/to/database.db") - /// let databasePath = location.path // "/path/to/database.db" - /// - /// let inMemoryLocation = Connection.Location.inMemory - /// let inMemoryPath = inMemoryLocation.path // ":memory:" - /// - /// let temporaryLocation = Connection.Location.temporary - /// let temporaryPath = temporaryLocation.path // "" - /// ``` - /// - /// - Note: When using the `.temporary` case, the returned value is an empty string - /// because the database is created as a temporary file that does not have a - /// persistent path. var path: String { switch self { - case .file(let path): return path - case .inMemory: return ":memory:" - case .temporary: return "" + case .file(let path): path + case .inMemory: ":memory:" + case .temporary: "" } } } diff --git a/Sources/DataLiteCore/Classes/Connection+Options.swift b/Sources/DataLiteCore/Classes/Connection+Options.swift index e8b4304..358a399 100644 --- a/Sources/DataLiteCore/Classes/Connection+Options.swift +++ b/Sources/DataLiteCore/Classes/Connection+Options.swift @@ -2,437 +2,98 @@ import Foundation import DataLiteC extension Connection { - /// Options for controlling the connection to a SQLite database. + /// Options that control how the SQLite database connection is opened. /// - /// This type represents a set of options that can be used when opening a connection to a - /// SQLite database. Each option corresponds to one of the flags defined in the SQLite - /// library. For more details, read [Opening A New Database Connection](https://www.sqlite.org/c3ref/open.html). + /// Each option corresponds to a flag from the SQLite C API. Multiple options can be combined + /// using the `OptionSet` syntax. /// - /// ### Usage - /// - /// ```swift - /// do { - /// let dbFilePath = "path/to/your/database.db" - /// let options: Connection.Options = [.readwrite, .create] - /// let connection = try Connection(path: dbFilePath, options: options) - /// print("Database connection established successfully!") - /// } catch { - /// print("Error opening database: \(error)") - /// } - /// ``` - /// - /// ## Topics - /// - /// ### Initializers - /// - /// - ``init(rawValue:)`` - /// - /// ### Instance Properties - /// - /// - ``rawValue`` - /// - /// ### Type Properties - /// - /// - ``readonly`` - /// - ``readwrite`` - /// - ``create`` - /// - ``uri`` - /// - ``memory`` - /// - ``nomutex`` - /// - ``fullmutex`` - /// - ``sharedcache`` - /// - ``privatecache`` - /// - ``exrescode`` - /// - ``nofollow`` + /// - SeeAlso: [Opening A New Database Connection](https://sqlite.org/c3ref/open.html) public struct Options: OptionSet, Sendable { // MARK: - Properties - /// An integer value representing a combination of option flags. - /// - /// This property holds the raw integer representation of the selected options for the - /// SQLite database connection. Each option corresponds to a specific flag defined in the - /// SQLite library, allowing for a flexible and efficient way to specify multiple options - /// using bitwise operations. The value can be combined using the bitwise OR operator (`|`). - /// - /// ```swift - /// let options = [ - /// Connection.Options.readonly, - /// Connection.Options.create - /// ] - /// ``` - /// - /// In this example, the `rawValue` will represent a combination of the ``readonly`` and ``create`` options. - /// - /// - Important: When combining options, ensure that the selected flags are compatible and do not conflict, - /// as certain combinations may lead to unexpected behavior. For example, setting both ``readonly`` and - /// ``readwrite`` is not allowed. + /// The raw integer value representing the option flags. public var rawValue: Int32 - // MARK: - Instances - - /// Option: open the database for read-only access. - /// - /// This option configures the SQLite database connection to be opened in read-only mode. When this - /// option is specified, the database can be accessed for querying, but any attempts to modify the - /// data (such as inserting, updating, or deleting records) will result in an error. If the specified - /// database file does not already exist, an error will also be returned. - /// - /// This is particularly useful when you want to ensure that your application does not accidentally - /// modify the database, or when you need to work with a database that is being shared among multiple - /// processes or applications that require read-only access. - /// - /// ### Usage - /// - /// You can specify the `readonly` option when opening a database connection, as shown in the example: - /// - /// ```swift - /// let options: Connection.Options = [.readonly] - /// let connection = try Connection(path: dbFilePath, options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: If you attempt to write to a read-only database, an error will be thrown. - /// - /// - Note: Ensure that the database file exists before opening it in read-only mode, as the connection - /// will fail if the file does not exist. - /// - /// For more details, refer to the SQLite documentation on - /// [opening a new database connection](https://www.sqlite.org/c3ref/open.html). - public static let readonly = Self(rawValue: SQLITE_OPEN_READONLY) - - /// Option: open the database for reading and writing. - /// - /// This option configures the SQLite database connection to be opened in read-write mode. When this - /// option is specified, the database can be accessed for both querying and modifying data. This means - /// you can perform operations such as inserting, updating, or deleting records in addition to reading. - /// - /// If the database file does not exist, an error will be returned. If the file is write-protected by - /// the operating system, the connection will be opened in read-only mode instead, as a fallback. - /// - /// ### Usage - /// - /// You can specify the `readwrite` option when opening a database connection, as shown below: - /// - /// ```swift - /// let options: Connection.Options = [.readwrite] - /// let connection = try Connection(path: dbFilePath, options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: If the database file does not exist, an error will be thrown. - /// - Note: If you are unable to open the database in read-write mode due to permissions, it will - /// attempt to open it in read-only mode. - /// - /// For more details, refer to the SQLite documentation on - /// [opening a new database connection](https://www.sqlite.org/c3ref/open.html). - public static let readwrite = Self(rawValue: SQLITE_OPEN_READWRITE) - - /// Option: create the database if it does not exist. - /// - /// This option instructs SQLite to create a new database file if it does not already exist. If the - /// specified database file already exists, the connection will open that existing database instead. - /// - /// ### Usage - /// - /// You can specify the `create` option when opening a database connection, as shown below: - /// - /// ```swift - /// let options: Connection.Options = [.create] - /// let connection = try Connection(path: dbFilePath, options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: If the database file exists, it will be opened normally, and no new file will be created. - /// - Note: If the database file does not exist, a new file will be created at the specified path. - /// - /// This option is often used in conjunction with other options, such as `readwrite`, to ensure that a - /// new database can be created and written to right away. - /// - /// For more details, refer to the SQLite documentation on - /// [opening a new database connection](https://www.sqlite.org/c3ref/open.html). - public static let create = Self(rawValue: SQLITE_OPEN_CREATE) - - /// Option: specify a URI for opening the database. - /// - /// This option allows the filename provided to be interpreted as a Uniform Resource Identifier (URI). - /// When this flag is set, SQLite will parse the filename as a URI, enabling the use of URI features - /// such as special encoding and various URI schemes. - /// - /// ### Usage - /// - /// You can specify the `uri` option when opening a database connection. Here’s an example: - /// - /// ```swift - /// let options: Connection.Options = [.uri] - /// let connection = try Connection(path: "file:///path/to/database.db", options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: Using this option allows you to take advantage of SQLite's URI capabilities, such as - /// specifying various parameters in the URI (e.g., caching, locking, etc.). - /// - Note: If this option is not set, the filename will be treated as a simple path without URI - /// interpretation. - /// - /// For more details, refer to the SQLite documentation on - /// [opening a new database connection](https://www.sqlite.org/c3ref/open.html). - public static let uri = Self(rawValue: SQLITE_OPEN_URI) - - /// Option: open the database in memory. - /// - /// This option opens the database as an in-memory database, meaning that all data is stored in RAM - /// rather than on disk. This can be useful for temporary databases or for testing purposes where - /// persistence is not required. - /// - /// When using this option, the "filename" argument is ignored, but it is still used for cache-sharing - /// if shared cache mode is enabled. - /// - /// ### Usage - /// - /// You can specify the `memory` option when opening a database connection. Here’s an example: - /// - /// ```swift - /// let options: Connection.Options = [.memory] - /// let connection = try Connection(path: ":memory:", options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: Since the database is stored in memory, all data will be lost when the connection is - /// closed or the program exits. Therefore, this option is best suited for scenarios where data - /// persistence is not necessary. - /// - Note: In-memory databases can be significantly faster than disk-based databases due to the - /// absence of disk I/O operations. - /// - /// For more details, refer to the SQLite documentation on - /// [opening a new database connection](https://www.sqlite.org/c3ref/open.html). - public static let memory = Self(rawValue: SQLITE_OPEN_MEMORY) - - /// Option: do not use mutexes. - /// - /// This option configures the new database connection to use the "multi-thread" - /// [threading mode](https://www.sqlite.org/threadsafe.html). In this mode, separate threads can - /// concurrently access SQLite, provided that each thread is utilizing a different - /// [database connection](https://www.sqlite.org/c3ref/sqlite3.html). - /// - /// ### Usage - /// - /// You can specify the `nomutex` option when opening a database connection. Here’s an example: - /// - /// ```swift - /// let options: Connection.Options = [.nomutex] - /// let connection = try Connection(path: "myDatabase.sqlite", options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: When using this option, ensure that each thread has its own database connection, as - /// concurrent access to the same connection is not safe. - /// - Note: This option can improve performance in multi-threaded applications by reducing the - /// overhead of mutex locking, but it may lead to undefined behavior if not used carefully. - /// - Note: If your application requires safe concurrent access to a single database connection - /// from multiple threads, consider using the ``fullmutex`` option instead. - /// - /// For more details, refer to the SQLite documentation on - /// [thread safety](https://www.sqlite.org/threadsafe.html). - public static let nomutex = Self(rawValue: SQLITE_OPEN_NOMUTEX) - - /// Option: use full mutexing. - /// - /// This option configures the new database connection to utilize the "serialized" - /// [threading mode](https://www.sqlite.org/threadsafe.html). In this mode, multiple threads can safely - /// attempt to access the same database connection simultaneously. Although mutexes will block any - /// actual concurrency, this mode allows for multiple threads to operate without causing data corruption - /// or undefined behavior. - /// - /// ### Usage - /// - /// You can specify the `fullmutex` option when opening a database connection. Here’s an example: - /// - /// ```swift - /// let options: Connection.Options = [.fullmutex] - /// let connection = try Connection(path: "myDatabase.sqlite", options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: Using the `fullmutex` option is recommended when you need to ensure thread safety when - /// multiple threads access the same database connection. - /// - Note: This option may introduce some performance overhead due to the locking mechanisms in place. - /// If your application is designed for high concurrency and can manage separate connections per thread, - /// consider using the ``nomutex`` option for better performance. - /// - Note: It's essential to be aware of potential deadlocks if multiple threads are competing for the - /// same resources. Proper design can help mitigate these risks. - /// - /// For more details, refer to the SQLite documentation on - /// [thread safety](https://www.sqlite.org/threadsafe.html). - public static let fullmutex = Self(rawValue: SQLITE_OPEN_FULLMUTEX) - - /// Option: use a shared cache. - /// - /// This option enables the database to be opened in [shared cache](https://www.sqlite.org/sharedcache.html) - /// mode. In this mode, multiple database connections can share cached data, potentially improving - /// performance when accessing the same database from different connections. - /// - /// ### Usage - /// - /// You can specify the `sharedcache` option when opening a database connection. Here’s an example: - /// - /// ```swift - /// let options: Connection.Options = [.sharedcache] - /// let connection = try Connection(path: "myDatabase.sqlite", options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: **Discouraged Usage**: The use of shared cache mode is - /// [discouraged](https://www.sqlite.org/sharedcache.html#dontuse). It may lead to unpredictable behavior, - /// especially in applications with complex threading models or multiple database connections. - /// - /// - Note: **Build Variability**: Shared cache capabilities may be omitted from many builds of SQLite. - /// If your SQLite build does not support shared cache, this option will be a no-op, meaning it will - /// have no effect on the behavior of your database connection. - /// - /// - Note: **Performance Considerations**: While shared cache can improve performance by reducing memory - /// usage, it may introduce complexity in managing concurrent access. Consider your application's design - /// and the potential for contention among connections when using this option. - /// - /// For more information, consult the SQLite documentation on - /// [shared cache mode](https://www.sqlite.org/sharedcache.html). - public static let sharedcache = Self(rawValue: SQLITE_OPEN_SHAREDCACHE) - - /// Option: use a private cache. - /// - /// This option disables the use of [shared cache](https://www.sqlite.org/sharedcache.html) mode. - /// When a database is opened with this option, it uses a private cache for its connections, meaning - /// that the cached data will not be shared with other database connections. - /// - /// ### Usage - /// - /// You can specify the `privatecache` option when opening a database connection. Here’s an example: - /// - /// ```swift - /// let options: Connection.Options = [.privatecache] - /// let connection = try Connection(path: "myDatabase.sqlite", options: options) - /// ``` - /// - /// ### Important Notes - /// - /// - Note: **Isolation**: Using a private cache ensures that the database connection operates in - /// isolation, preventing any caching interference from other connections. This can be beneficial - /// in multi-threaded applications where shared cache might lead to unpredictable behavior. - /// - /// - Note: **Performance Impact**: While a private cache avoids the complexities associated with - /// shared caching, it may increase memory usage since each connection maintains its own cache. - /// Consider your application’s performance requirements when choosing between shared and private - /// cache options. - /// - /// - Note: **Build Compatibility**: Ensure that your SQLite build supports the private cache option. - /// While most builds do, it’s always a good idea to verify if you encounter any issues. - /// - /// For more information, refer to the SQLite documentation on - /// [shared cache mode](https://www.sqlite.org/sharedcache.html). - public static let privatecache = Self(rawValue: SQLITE_OPEN_PRIVATECACHE) - - /// Option: use extended result code mode. - /// - /// This option enables "extended result code mode" for the database connection. When this mode is - /// enabled, SQLite provides additional error codes that can help in diagnosing issues that may - /// arise during database operations. - /// - /// ### Usage - /// - /// You can specify the `exrescode` option when opening a database connection. Here’s an example: - /// - /// ```swift - /// let options: Connection.Options = [.exrescode] - /// let connection = try Connection(path: "myDatabase.sqlite", options: options) - /// ``` - /// - /// ### Benefits - /// - /// - **Improved Error Handling**: By using extended result codes, you can get more granular - /// information about errors, which can be particularly useful for debugging and error handling - /// in your application. - /// - /// - **Detailed Diagnostics**: Extended result codes may provide context about the failure, - /// allowing for more targeted troubleshooting and resolution of issues. - /// - /// ### Considerations - /// - /// - **Compatibility**: Make sure your version of SQLite supports extended result codes. This - /// option should be available in most modern builds of SQLite. - /// - /// For more information, refer to the SQLite documentation on - /// [extended result codes](https://www.sqlite.org/rescode.html). - public static let exrescode = Self(rawValue: SQLITE_OPEN_EXRESCODE) - - /// Option: do not follow symbolic links when opening a file. - /// - /// When this option is enabled, the database filename must not contain a symbolic link. If the - /// filename refers to a symbolic link, an error will be returned when attempting to open the - /// database. - /// - /// ### Usage - /// - /// You can specify the `nofollow` option when opening a database connection. Here’s an example: - /// - /// ```swift - /// let options: Connection.Options = [.nofollow] - /// let connection = try Connection(path: "myDatabase.sqlite", options: options) - /// ``` - /// - /// ### Benefits - /// - /// - **Increased Security**: By disallowing symbolic links, you reduce the risk of unintended - /// file access or manipulation through links that may point to unexpected locations. - /// - /// - **File Integrity**: Ensures that the database connection directly references the intended - /// file without any indirection that symbolic links could introduce. - /// - /// ### Considerations - /// - /// - **Filesystem Limitations**: This option may limit your ability to use symbolic links in - /// your application. Make sure this behavior is acceptable for your use case. - /// - /// For more information, refer to the SQLite documentation on [file opening](https://www.sqlite.org/c3ref/open.html). - public static let nofollow = Self(rawValue: SQLITE_OPEN_NOFOLLOW) - // MARK: - Inits - /// Initializes a set of options for connecting to a SQLite database. + /// Creates a new set of options from a raw integer value. /// - /// This initializer allows you to create a combination of option flags that dictate how the - /// database connection will behave. The `rawValue` parameter should be an integer that - /// represents one or more options, combined using a bitwise OR operation. - /// - /// - Parameter rawValue: An integer value representing a combination of option flags. This - /// value can be constructed using the predefined options, e.g., `SQLITE_OPEN_READWRITE | - /// SQLITE_OPEN_CREATE`. - /// - /// ### Example - /// - /// You can create a set of options as follows: + /// Combine multiple flags using bitwise OR (`|`). /// /// ```swift - /// let options = Connection.Options( + /// let opts = Connection.Options( /// rawValue: SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE /// ) /// ``` - /// - /// In this example, the `options` variable will have both the ``readwrite`` and - /// ``create`` options enabled, allowing for read/write access and creating the database if - /// it does not exist. - /// - /// ### Important Notes - /// - /// - Note: Be cautious when combining options, as some combinations may lead to conflicts or - /// unintended behavior (e.g., ``readonly`` and ``readwrite`` cannot be set together). public init(rawValue: Int32) { self.rawValue = rawValue } + + // MARK: - Instances + + /// Opens the database in read-only mode. + /// + /// Fails if the database file does not exist. + public static let readonly = Self(rawValue: SQLITE_OPEN_READONLY) + + /// Opens the database for reading and writing. + /// + /// Fails if the file does not exist or is write-protected. + public static let readwrite = Self(rawValue: SQLITE_OPEN_READWRITE) + + /// Creates the database file if it does not exist. + /// + /// Commonly combined with `.readwrite`. + public static let create = Self(rawValue: SQLITE_OPEN_CREATE) + + /// Interprets the filename as a URI. + /// + /// Enables SQLite’s URI parameters and schemes. + /// - SeeAlso: [Uniform Resource Identifiers](https://sqlite.org/uri.html) + public static let uri = Self(rawValue: SQLITE_OPEN_URI) + + /// Opens an in-memory database. + /// + /// Data is stored in RAM and discarded when closed. + /// - SeeAlso: [In-Memory Databases](https://sqlite.org/inmemorydb.html) + public static let memory = Self(rawValue: SQLITE_OPEN_MEMORY) + + /// Disables mutexes for higher concurrency. + /// + /// Each thread must use a separate connection. + /// - SeeAlso: [Using SQLite In Multi-Threaded Applications]( + /// https://sqlite.org/threadsafe.html) + public static let nomutex = Self(rawValue: SQLITE_OPEN_NOMUTEX) + + /// Enables serialized access using full mutexes. + /// + /// Safe for concurrent access from multiple threads. + /// - SeeAlso: [Using SQLite In Multi-Threaded Applications]( + /// https://sqlite.org/threadsafe.html) + public static let fullmutex = Self(rawValue: SQLITE_OPEN_FULLMUTEX) + + /// Enables shared cache mode. + /// + /// Allows multiple connections to share cached data. + /// - SeeAlso: [SQLite Shared-Cache Mode](https://sqlite.org/sharedcache.html) + /// - Warning: Shared cache mode is discouraged by SQLite. + public static let sharedcache = Self(rawValue: SQLITE_OPEN_SHAREDCACHE) + + /// Disables shared cache mode. + /// + /// Each connection uses a private cache. + /// - SeeAlso: [SQLite Shared-Cache Mode](https://sqlite.org/sharedcache.html) + public static let privatecache = Self(rawValue: SQLITE_OPEN_PRIVATECACHE) + + /// Enables extended result codes. + /// + /// Provides more detailed SQLite error codes. + /// - SeeAlso: [Result and Error Codes](https://sqlite.org/rescode.html) + public static let exrescode = Self(rawValue: SQLITE_OPEN_EXRESCODE) + + /// Disallows following symbolic links. + /// + /// Improves security by preventing indirect file access. + public static let nofollow = Self(rawValue: SQLITE_OPEN_NOFOLLOW) } } diff --git a/Sources/DataLiteCore/Classes/Connection.swift b/Sources/DataLiteCore/Classes/Connection.swift index 8b66c1c..0e26862 100644 --- a/Sources/DataLiteCore/Classes/Connection.swift +++ b/Sources/DataLiteCore/Classes/Connection.swift @@ -1,29 +1,79 @@ import Foundation import DataLiteC -public final class Connection: ConnectionProtocol { +/// A class representing a connection to an SQLite database. +/// +/// The `Connection` class manages the connection to an SQLite database. It provides an interface +/// for preparing SQL queries, managing transactions, and handling errors. This class serves as the +/// main object for interacting with the database. +public final class Connection { // MARK: - Private Properties private let connection: OpaquePointer - fileprivate var delegates = [DelegateBox]() - // MARK: - Connection State - - public var isAutocommit: Bool { - sqlite3_get_autocommit(connection) != 0 + fileprivate var delegates = [DelegateBox]() { + didSet { + switch (oldValue.isEmpty, delegates.isEmpty) { + case (true, false): + let ctx = Unmanaged.passUnretained(self).toOpaque() + sqlite3_update_hook(connection, updateHookCallback(_:_:_:_:_:), ctx) + sqlite3_commit_hook(connection, commitHookCallback(_:), ctx) + sqlite3_rollback_hook(connection, rollbackHookCallback(_:), ctx) + case (false, true): + sqlite3_update_hook(connection, nil, nil) + sqlite3_commit_hook(connection, nil, nil) + sqlite3_rollback_hook(connection, nil, nil) + default: + break + } + } } - public var isReadonly: Bool { - sqlite3_db_readonly(connection, "main") == 1 - } - - public var busyTimeout: Int32 { - get { try! get(pragma: .busyTimeout) ?? 0 } - set { try! set(pragma: .busyTimeout, value: newValue) } + fileprivate var traceDelegates = [TraceDelegateBox]() { + didSet { + switch (oldValue.isEmpty, traceDelegates.isEmpty) { + case (true, false): + let ctx = Unmanaged.passUnretained(self).toOpaque() + sqlite3_trace_stmt(connection, traceCallback(_:_:_:_:), ctx) + case (false, true): + sqlite3_trace_stmt(connection, nil, nil) + default: + break + } + } } // MARK: - Inits + /// Initializes a new connection to an SQLite database. + /// + /// Opens a connection to the database at the specified `location` using the given `options`. + /// If the location represents a file path, this method ensures that the parent directory + /// exists, creating intermediate directories if needed. + /// + /// ### Example + /// + /// ```swift + /// do { + /// let connection = try Connection( + /// location: .file(path: "/path/to/sqlite.db"), + /// options: .readwrite + /// ) + /// // Use the connection to execute SQL statements + /// } catch { + /// print("Failed to open database: \\(error)") + /// } + /// ``` + /// + /// - Parameters: + /// - location: The location of the database. Can represent a file path, an in-memory + /// database, or a temporary database. + /// - options: Connection options that define behavior such as read-only mode, creation + /// flags, and cache type. + /// + /// - Throws: ``SQLiteError`` if the connection cannot be opened or initialized due to + /// SQLite-related issues such as invalid path, missing permissions, or corruption. + /// - Throws: An error if directory creation fails for file-based database locations. public init(location: Location, options: Options) throws { if case let Location.file(path) = location, !path.isEmpty { try FileManager.default.createDirectory( @@ -35,21 +85,43 @@ public final class Connection: ConnectionProtocol { var connection: OpaquePointer! = nil let status = sqlite3_open_v2(location.path, &connection, options.rawValue, nil) - if status == SQLITE_OK, let connection = connection { - self.connection = connection - - let ctx = Unmanaged.passUnretained(self).toOpaque() - sqlite3_trace_v2(connection, UInt32(SQLITE_TRACE_STMT), traceCallback(_:_:_:_:), ctx) - sqlite3_update_hook(connection, updateHookCallback(_:_:_:_:_:), ctx) - sqlite3_commit_hook(connection, commitHookCallback(_:), ctx) - sqlite3_rollback_hook(connection, rollbackHookCallback(_:), ctx) - } else { - let error = Error(connection) + guard status == SQLITE_OK, let connection else { + let error = SQLiteError(connection) sqlite3_close_v2(connection) throw error } + + self.connection = connection } + /// Initializes a new connection to an SQLite database using a file path. + /// + /// Opens a connection to the SQLite database located at the specified `path` using the provided + /// `options`. Internally, this method calls the designated initializer to perform the actual + /// setup and validation. + /// + /// ### Example + /// + /// ```swift + /// do { + /// let connection = try Connection( + /// path: "/path/to/sqlite.db", + /// options: .readwrite + /// ) + /// // Use the connection to execute SQL statements + /// } catch { + /// print("Failed to open database: \\(error)") + /// } + /// ``` + /// + /// - Parameters: + /// - path: The file system path to the SQLite database file. Can be absolute or relative. + /// - options: Options that control how the database is opened, such as access mode and + /// cache type. + /// + /// - Throws: ``SQLiteError`` if the connection cannot be opened due to SQLite-level errors, + /// invalid path, missing permissions, or corruption. + /// - Throws: An error if the required directory structure cannot be created. public convenience init(path: String, options: Options) throws { try self.init(location: .file(path: path), options: options) } @@ -57,73 +129,116 @@ public final class Connection: ConnectionProtocol { deinit { sqlite3_close_v2(connection) } - - // MARK: - Delegation - - public func addDelegate(_ delegate: ConnectionDelegate) { - delegates.removeAll { $0.delegate == nil } - delegates.append(.init(delegate: delegate)) +} + +// MARK: - ConnectionProtocol + +extension Connection: ConnectionProtocol { + public var isAutocommit: Bool { + sqlite3_get_autocommit(connection) != 0 } - public func removeDelegate(_ delegate: ConnectionDelegate) { - delegates.removeAll { $0.delegate == nil || $0.delegate === delegate } + public var isReadonly: Bool { + sqlite3_db_readonly(connection, "main") == 1 } - // MARK: - Custom SQL Functions - - public func add(function: Function.Type) throws(Error) { - try function.install(db: connection) - } - - public func remove(function: Function.Type) throws(Error) { - try function.uninstall(db: connection) - } - - // MARK: - Statement Preparation - - public func prepare(sql query: String, options: Statement.Options = []) throws(Error) -> Statement { - try Statement(db: connection, sql: query, options: options) - } - - // MARK: - Script Execution - - public func execute(raw sql: String) throws(Error) { - let status = sqlite3_exec(connection, sql, nil, nil, nil) - if status != SQLITE_OK { - throw Error(connection) + public static func initialize() throws(SQLiteError) { + let status = sqlite3_initialize() + guard status == SQLITE_OK else { + throw SQLiteError(code: status, message: "") } } - // MARK: - Encryption Keys + public static func shutdown() throws(SQLiteError) { + let status = sqlite3_shutdown() + guard status == SQLITE_OK else { + throw SQLiteError(code: status, message: "") + } + } - public func apply(_ key: Key, name: String? = nil) throws(Error) { + public func apply(_ key: Key, name: String?) throws(SQLiteError) { let status = if let name { sqlite3_key_v2(connection, name, key.keyValue, key.length) } else { sqlite3_key(connection, key.keyValue, key.length) } - if status != SQLITE_OK { - throw Error(connection) + guard status == SQLITE_OK else { + throw SQLiteError(connection) } } - public func rekey(_ key: Key, name: String? = nil) throws(Error) { + public func rekey(_ key: Key, name: String?) throws(SQLiteError) { let status = if let name { sqlite3_rekey_v2(connection, name, key.keyValue, key.length) } else { sqlite3_rekey(connection, key.keyValue, key.length) } - if status != SQLITE_OK { - throw Error(connection) + guard status == SQLITE_OK else { + throw SQLiteError(connection) } } + + public func add(delegate: any ConnectionDelegate) { + delegates.append(.init(delegate: delegate)) + delegates.removeAll { $0.delegate == nil } + } + + public func remove(delegate: any ConnectionDelegate) { + delegates.removeAll { + $0.delegate === delegate || $0.delegate == nil + } + } + + public func add(trace delegate: any ConnectionTraceDelegate) { + traceDelegates.append(.init(delegate: delegate)) + traceDelegates.removeAll { $0.delegate == nil } + } + + public func remove(trace delegate: any ConnectionTraceDelegate) { + traceDelegates.removeAll { + $0.delegate === delegate || $0.delegate == nil + } + } + + public func add(function: Function.Type) throws(SQLiteError) { + try function.install(db: connection) + } + + public func remove(function: Function.Type) throws(SQLiteError) { + try function.uninstall(db: connection) + } + + public func prepare( + sql query: String, options: Statement.Options + ) throws(SQLiteError) -> any StatementProtocol { + try Statement(db: connection, sql: query, options: options) + } + + public func execute(raw sql: String) throws(SQLiteError) { + let status = sqlite3_exec(connection, sql, nil, nil, nil) + guard status == SQLITE_OK else { throw SQLiteError(connection) } + } } +// MARK: - DelegateBox + fileprivate extension Connection { class DelegateBox { weak var delegate: ConnectionDelegate? - init(delegate: ConnectionDelegate? = nil) { + init(delegate: ConnectionDelegate) { + self.delegate = delegate + } + } +} + +// MARK: - TraceDelegateBox + +fileprivate extension Connection { + class TraceDelegateBox { + weak var delegate: ConnectionTraceDelegate? + + init(delegate: ConnectionTraceDelegate) { self.delegate = delegate } } @@ -131,28 +246,60 @@ fileprivate extension Connection { // MARK: - Functions +private typealias TraceCallback = @convention(c) ( + UInt32, + UnsafeMutableRawPointer?, + UnsafeMutableRawPointer?, + UnsafeMutableRawPointer? +) -> Int32 + +@discardableResult +private func sqlite3_trace_stmt( + _ db: OpaquePointer!, + _ callback: TraceCallback!, + _ ctx: UnsafeMutableRawPointer! +) -> Int32 { + sqlite3_trace_v2(db, SQLITE_TRACE_STMT, callback, ctx) +} + +@discardableResult +private func sqlite3_trace_v2( + _ db: OpaquePointer!, + _ mask: Int32, + _ callback: TraceCallback!, + _ ctx: UnsafeMutableRawPointer! +) -> Int32 { + sqlite3_trace_v2(db, UInt32(mask), callback, ctx) +} + private func traceCallback( _ flag: UInt32, _ ctx: UnsafeMutableRawPointer?, _ p: UnsafeMutableRawPointer?, _ x: UnsafeMutableRawPointer? ) -> Int32 { - guard let ctx = ctx else { return SQLITE_OK } + guard let ctx, + let stmt = OpaquePointer(p) + else { return SQLITE_OK } + let connection = Unmanaged .fromOpaque(ctx) .takeUnretainedValue() - guard !connection.delegates.isEmpty, - let stmt = OpaquePointer(p), - let pSql = sqlite3_expanded_sql(stmt), - let xSql = x?.assumingMemoryBound(to: CChar.self) - else { return SQLITE_OK } + let xSql = x?.assumingMemoryBound(to: CChar.self) + let pSql = sqlite3_expanded_sql(stmt) + + defer { sqlite3_free(pSql) } + + guard let xSql, let pSql else { + return SQLITE_OK + } - let pSqlString = String(cString: pSql) let xSqlString = String(cString: xSql) + let pSqlString = String(cString: pSql) let trace = (xSqlString, pSqlString) - for box in connection.delegates { + for box in connection.traceDelegates { box.delegate?.connection(connection, trace: trace) } @@ -166,37 +313,36 @@ private func updateHookCallback( _ tName: UnsafePointer?, _ rowID: sqlite3_int64 ) { - guard let ctx = ctx else { return } + guard let ctx else { return } + let connection = Unmanaged .fromOpaque(ctx) .takeUnretainedValue() - if !connection.delegates.isEmpty { - guard let dName = dName, let tName = tName else { return } - - let dbName = String(cString: dName) - let tableName = String(cString: tName) - let updateAction: SQLiteAction - - switch action { - case SQLITE_INSERT: - updateAction = .insert(db: dbName, table: tableName, rowID: rowID) - case SQLITE_UPDATE: - updateAction = .update(db: dbName, table: tableName, rowID: rowID) - case SQLITE_DELETE: - updateAction = .delete(db: dbName, table: tableName, rowID: rowID) - default: - return - } - - for box in connection.delegates { - box.delegate?.connection(connection, didUpdate: updateAction) - } + guard let dName = dName, let tName = tName else { return } + + let dbName = String(cString: dName) + let tableName = String(cString: tName) + + let updateAction: SQLiteAction? = switch action { + case SQLITE_INSERT: .insert(db: dbName, table: tableName, rowID: rowID) + case SQLITE_UPDATE: .update(db: dbName, table: tableName, rowID: rowID) + case SQLITE_DELETE: .delete(db: dbName, table: tableName, rowID: rowID) + default: nil + } + + guard let updateAction else { return } + + for box in connection.delegates { + box.delegate?.connection(connection, didUpdate: updateAction) } } -private func commitHookCallback(_ ctx: UnsafeMutableRawPointer?) -> Int32 { +private func commitHookCallback( + _ ctx: UnsafeMutableRawPointer? +) -> Int32 { guard let ctx = ctx else { return SQLITE_OK } + let connection = Unmanaged .fromOpaque(ctx) .takeUnretainedValue() @@ -211,8 +357,11 @@ private func commitHookCallback(_ ctx: UnsafeMutableRawPointer?) -> Int32 { } } -private func rollbackHookCallback(_ ctx: UnsafeMutableRawPointer?) { +private func rollbackHookCallback( + _ ctx: UnsafeMutableRawPointer? +) { guard let ctx = ctx else { return } + let connection = Unmanaged .fromOpaque(ctx) .takeUnretainedValue() diff --git a/Sources/DataLiteCore/Classes/Function+Aggregate.swift b/Sources/DataLiteCore/Classes/Function+Aggregate.swift index 24d15b0..6ad460e 100644 --- a/Sources/DataLiteCore/Classes/Function+Aggregate.swift +++ b/Sources/DataLiteCore/Classes/Function+Aggregate.swift @@ -2,24 +2,23 @@ import Foundation import DataLiteC extension Function { - /// Base class for creating custom SQLite aggregate functions. + /// Base class for defining custom SQLite aggregate functions. /// - /// This class provides a basic implementation for creating aggregate functions in SQLite. - /// Aggregate functions process a set of input values and return a single value. - /// To create a custom aggregate function, subclass `Function.Aggregate` and override the - /// following properties and methods: + /// The `Aggregate` class provides a foundation for creating aggregate + /// functions in SQLite. Aggregate functions operate on multiple rows of + /// input and return a single result value. /// - /// - ``name``: The name of the function used in SQL queries. - /// - ``argc``: The number of arguments the function accepts. - /// - ``options``: Options for the function, such as `deterministic` and `innocuous`. - /// - ``step(args:)``: Method called for each input value. - /// - ``finalize()``: Method called after processing all input values. + /// To define a custom aggregate function, subclass `Function.Aggregate` and + /// override the following: + /// + /// - ``name`` – The SQL name of the function. + /// - ``argc`` – The number of arguments accepted by the function. + /// - ``options`` – Function options, such as `.deterministic` or `.innocuous`. + /// - ``step(args:)`` – Called for each row's argument values. + /// - ``finalize()`` – Called once to compute and return the final result. /// /// ### Example /// - /// This example shows how to create a custom aggregate function to calculate the sum - /// of integers. - /// /// ```swift /// final class SumAggregate: Function.Aggregate { /// enum Error: Swift.Error { @@ -34,23 +33,20 @@ extension Function { /// /// private var sum: Int = 0 /// - /// override func step(args: Arguments) throws { + /// override func step(args: ArgumentsProtocol) throws { /// guard let value = args[0] as Int? else { /// throw Error.argumentsWrong /// } /// sum += value /// } /// - /// override func finalize() throws -> SQLiteRawRepresentable? { + /// override func finalize() throws -> SQLiteRepresentable? { /// return sum /// } /// } /// ``` /// - /// ### Usage - /// - /// To use a custom aggregate function, first establish a database connection and - /// register the function. + /// ### Registration /// /// ```swift /// let connection = try Connection( @@ -62,16 +58,13 @@ extension Function { /// /// ### SQL Example /// - /// Example SQL query using the custom aggregate function to calculate the sum of - /// values in the `value` column of the `my_table`. - /// /// ```sql /// SELECT sum_aggregate(value) FROM my_table /// ``` /// /// ## Topics /// - /// ### Initializers + /// ### Initialization /// /// - ``init()`` /// @@ -80,139 +73,23 @@ extension Function { /// - ``step(args:)`` /// - ``finalize()`` open class Aggregate: Function { - // MARK: - Context - - /// Helper class for storing and managing the context of a custom SQLite aggregate function. - /// - /// This class is used to hold a reference to the `Aggregate` function implementation. - /// It is created and managed when the aggregate function is installed in the SQLite - /// database connection. The context is passed to SQLite and is used to invoke the - /// corresponding function implementation when called. - fileprivate final class Context { - // MARK: Properties - - /// The type of the aggregate function managed by this context. - /// - /// This property holds a reference to the subclass of `Aggregate` that implements - /// the custom aggregate function. It is used to create instances of the function - /// and manage its state during SQL query execution. - private let function: Aggregate.Type - - // MARK: Inits - - /// Initializes a new `Context` with a reference to the aggregate function type. - /// - /// This initializer creates an instance of the `Context` class that will hold - /// a reference to the aggregate function type. It is used to manage state and - /// perform operations with the custom aggregate function in the SQLite context. - /// - /// - Parameter function: The subclass of `Aggregate` implementing the custom - /// aggregate function. This parameter specifies which function type will be - /// used in the context. - /// - /// - Note: The initializer establishes a link between the context and the function - /// type, allowing extraction of function instances and management of their state - /// during SQL query processing. - init(function: Aggregate.Type) { - self.function = function - } - - // MARK: Methods - - /// Retrieves or creates an instance of the aggregate function. - /// - /// This method retrieves an existing instance of the aggregate function from the - /// SQLite context or creates a new one if it has not yet been created. The returned - /// instance allows management of the aggregate function's state during query execution. - /// - /// - Parameter ctx: Pointer to the SQLite context associated with the current - /// query. This parameter is used to access the aggregate context where the - /// function state is stored. - /// - /// - Returns: An unmanaged reference to the `Aggregate` instance. - /// - /// - Note: The method checks whether an instance of the function already exists in - /// the context. If no instance is found, a new one is created and saved in the - /// context for use in subsequent calls. - func function(ctx: OpaquePointer?) -> Unmanaged { - let stride = MemoryLayout>.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.self - ).pointee - } else { - let function = self.function.init() - let unmanagedFunction = Unmanaged.passRetained(function) - let functionPointer = unmanagedFunction.toOpaque() - withUnsafeBytes(of: functionPointer) { - functionBuffer.copyMemory(from: $0) - } - return unmanagedFunction - } - } - } - // MARK: - Properties - /// Flag indicating whether an error occurred during execution. fileprivate var hasErrored = false // MARK: - Inits - /// Initializes a new instance of the `Aggregate` class. + /// Initializes a new aggregate function instance. /// - /// This initializer is required for subclasses of ``Aggregate``. - /// In the current implementation, it performs no additional actions but provides - /// a basic structure for creating instances. + /// Subclasses may override this initializer to perform custom setup. + /// The base implementation performs no additional actions. /// - /// Subclasses may override this initializer to implement their own initialization - /// logic, including setting up additional properties or performing other necessary - /// operations. - /// - /// ```swift - /// public class MyCustomAggregate: Function.Aggregate { - /// required public init() { - /// super.init() // Call to superclass initializer - /// // Additional initialization if needed - /// } - /// } - /// ``` - /// - /// - Note: Always call `super.init()` in the overridden initializer to ensure - /// proper initialization of the parent class. + /// - Important: Always call `super.init()` when overriding. required public override init() {} // MARK: - Methods - /// Installs the custom SQLite aggregate function into the specified database connection. - /// - /// This method registers the custom aggregate function in the SQLite database, - /// allowing it to be used in SQL queries. The method creates a context for the function - /// and passes it to SQLite, as well as specifying callback functions to handle input - /// values and finalize results. - /// - /// ```swift - /// // Assuming the database connection is already open - /// let db: OpaquePointer = ... - /// // Register the function in the database - /// try MyCustomAggregate.install(db: db) - /// ``` - /// - /// - Parameter connection: Pointer to the SQLite database connection where the function - /// will be installed. - /// - /// - Throws: ``Connection/Error`` if the function installation fails. The error is thrown if - /// the `sqlite3_create_function_v2` call does not return `SQLITE_OK`. - /// - /// - Note: This method must be called to register the custom function before using - /// it in SQL queries. Ensure that the database connection is open and available at - /// the time of this method call. - override class func install(db connection: OpaquePointer) throws(Connection.Error) { + override class func install(db connection: OpaquePointer) throws(SQLiteError) { let context = Context(function: self) let ctx = Unmanaged.passRetained(context).toOpaque() let status = sqlite3_create_function_v2( @@ -220,105 +97,91 @@ extension Function { nil, xStep(_:_:_:), xFinal(_:), xDestroy(_:) ) if status != SQLITE_OK { - throw Connection.Error(connection) + throw SQLiteError(connection) } } - /// Called for each input value during aggregate computation. + /// Processes one step of the aggregate computation. /// - /// This method should be overridden by subclasses to implement the specific logic - /// for processing each input value. Your implementation should handle the input - /// arguments and accumulate results for later finalization. + /// This method is called once for each row of input data. Subclasses must override it to + /// accumulate intermediate results. /// - /// ```swift - /// class MyCustomAggregate: Function.Aggregate { - /// // ... + /// - Parameter args: The set of arguments passed to the function. + /// - Throws: An error if the input arguments are invalid or the computation fails. /// - /// private var sum: Int = 0 - /// - /// override func step(args: Arguments) throws { - /// guard let value = args[0].intValue else { - /// throw MyCustomError.invalidInput - /// } - /// sum += value - /// } - /// - /// // ... - /// } - /// ``` - /// - /// - Parameter args: An ``Arguments`` object that contains the number of - /// arguments and their values. - /// - /// - Throws: An error if the function execution fails. Subclasses may throw errors - /// if the input values do not match the expected format or if other issues arise - /// during processing. - /// - /// - Note: It is important to override this method in subclasses; otherwise, a - /// runtime error will occur due to calling `fatalError()`. - open func step(args: Arguments) throws { - fatalError("The 'step' method should be overridden.") + /// - Note: The default implementation triggers a runtime error. + open func step(args: any ArgumentsProtocol) throws { + fatalError("Subclasses must override `step(args:)`.") } - /// Called when the aggregate computation is complete. + /// Finalizes the aggregate computation and returns the result. /// - /// This method should be overridden by subclasses to return the final result - /// of the aggregate computation. Your implementation should return a value that will - /// be used in SQL queries. If the aggregate should not return a value, you can - /// return `nil`. + /// SQLite calls this method once after all input rows have been processed. + /// Subclasses must override it to produce the final result of the aggregate. /// - /// ```swift - /// class MyCustomAggregate: Function.Aggregate { - /// // ... - /// - /// private var sum: Int = 0 - /// - /// override func finalize() throws -> SQLiteRawRepresentable? { - /// return sum - /// } - /// } - /// ``` - /// - /// - Returns: An optional ``SQLiteRawRepresentable`` representing the result of the - /// aggregate function. The return value may be `nil` if a result is not required. - /// - /// - Throws: An error if the function execution fails. Subclasses may throw errors - /// if the aggregate cannot be computed correctly or if other issues arise. - /// - /// - Note: It is important to override this method in subclasses; otherwise, a - /// runtime error will occur due to calling `fatalError()`. - open func finalize() throws -> SQLiteRawRepresentable? { - fatalError("The 'finalize' method should be overridden.") + /// - Returns: The final computed value, or `nil` if the function produces no result. + /// - Throws: An error if the computation cannot be finalized. + /// - Note: The default implementation triggers a runtime error. + open func finalize() throws -> SQLiteRepresentable? { + fatalError("Subclasses must override `finalize()`.") + } + } +} + +extension Function.Aggregate { + fileprivate final class Context { + // MARK: - Properties + + private let function: Aggregate.Type + + // MARK: - Inits + + init(function: Aggregate.Type) { + self.function = function + } + + // MARK: - Methods + + func function( + for ctx: OpaquePointer?, isFinal: Bool = false + ) -> Unmanaged? { + typealias U = Unmanaged + + let bytes = isFinal ? 0 : MemoryLayout.stride + let raw = sqlite3_aggregate_context(ctx, Int32(bytes)) + guard let raw else { return nil } + + let pointer = raw.assumingMemoryBound(to: U?.self) + + if let pointer = pointer.pointee { + return pointer + } else { + let function = self.function.init() + pointer.pointee = Unmanaged.passRetained(function) + return pointer.pointee + } } } } // MARK: - Functions -/// C callback function to perform a step of the custom SQLite aggregate function. -/// -/// This function is called by SQLite for each input value passed to the aggregate function. -/// It retrieves the function implementation from the context associated with the SQLite -/// request, calls the `step(args:)` method, and handles any errors that may occur during -/// execution. -/// -/// - Parameters: -/// - ctx: Pointer to the SQLite context associated with the current query. -/// - argc: Number of arguments passed to the function. -/// - argv: Array of pointers to the argument values passed to the function. private func xStep( _ ctx: OpaquePointer?, _ argc: Int32, _ argv: UnsafeMutablePointer? ) { - let context = Unmanaged + let function = Unmanaged .fromOpaque(sqlite3_user_data(ctx)) .takeUnretainedValue() - - let function = context - .function(ctx: ctx) + .function(for: ctx)? .takeUnretainedValue() + guard let function else { + sqlite3_result_error_nomem(ctx) + return + } + assert(!function.hasErrored) do { @@ -330,31 +193,28 @@ private func xStep( let message = "Error executing function '\(name)': \(description)" function.hasErrored = true sqlite3_result_error(ctx, message, -1) + sqlite3_result_error_code(ctx, SQLITE_ERROR) } } -/// C callback function to finalize the result of the custom SQLite aggregate function. -/// -/// This function is called by SQLite when the aggregate computation is complete. -/// It retrieves the function implementation from the context, calls the `finalize()` -/// method, and sets the query result based on the returned value. -/// -/// - Parameter ctx: Pointer to the SQLite context associated with the current query. private func xFinal(_ ctx: OpaquePointer?) { - let context = Unmanaged + let pointer = Unmanaged .fromOpaque(sqlite3_user_data(ctx)) .takeUnretainedValue() + .function(for: ctx, isFinal: true) - let unmanagedFunction = context.function(ctx: ctx) - let function = unmanagedFunction.takeUnretainedValue() + defer { pointer?.release() } - defer { unmanagedFunction.release() } + guard let function = pointer?.takeUnretainedValue() else { + sqlite3_result_null(ctx) + return + } guard !function.hasErrored else { return } do { let result = try function.finalize() - sqlite3_result_value(ctx, result?.sqliteRawValue) + sqlite3_result_value(ctx, result?.sqliteValue) } catch { let name = type(of: function).name let description = error.localizedDescription @@ -364,12 +224,6 @@ private func xFinal(_ ctx: OpaquePointer?) { } } -/// C callback function to destroy the context associated with the custom SQLite aggregate function. -/// -/// This function is called by SQLite when the function is uninstalled. It frees the memory -/// allocated for the `Context` object associated with the function to avoid memory leaks. -/// -/// - Parameter ctx: Pointer to the SQLite query context. private func xDestroy(_ ctx: UnsafeMutableRawPointer?) { guard let ctx else { return } Unmanaged.fromOpaque(ctx).release() diff --git a/Sources/DataLiteCore/Classes/Function+Arguments.swift b/Sources/DataLiteCore/Classes/Function+Arguments.swift index 68a2763..910c37f 100644 --- a/Sources/DataLiteCore/Classes/Function+Arguments.swift +++ b/Sources/DataLiteCore/Classes/Function+Arguments.swift @@ -4,29 +4,14 @@ import DataLiteC extension Function { /// A collection representing the arguments passed to an SQLite function. /// - /// This structure provides a collection interface to access the arguments passed to an SQLite - /// function. Each argument is represented by an instance of `SQLiteValue`, which can hold - /// various types of SQLite values such as integers, floats, text, blobs, or nulls. - /// - /// - Important: This collection does not perform bounds checking when accessing arguments via - /// subscripts. It is the responsibility of the caller to ensure that the provided index is within the bounds - /// of the argument list. - /// - /// - Important: The indices of this collection start from 0 and go up to, but not including, the - /// count of arguments. - public struct Arguments: Collection { - /// Alias for the type representing an element in `Arguments`, which is a `SQLiteValue`. - public typealias Element = SQLiteRawValue - - /// Alias for the index type used in `Arguments`. - public typealias Index = Int - + /// The `Arguments` structure provides a type-safe interface for accessing the arguments + /// received by a user-defined SQLite function. Each element of the collection is represented + /// by a ``SQLiteValue`` instance that can store integers, floating-point numbers, text, blobs, + /// or nulls. + public struct Arguments: ArgumentsProtocol { // MARK: - Properties - /// The number of arguments passed to the SQLite function. private let argc: Int32 - - /// A pointer to an array of `OpaquePointer?` representing SQLite values. private let argv: UnsafeMutablePointer? /// The number of arguments passed to the SQLite function. @@ -34,28 +19,23 @@ extension Function { Int(argc) } - /// A Boolean value indicating whether there are no arguments passed to the SQLite function. + /// A Boolean value indicating whether there are no arguments. public var isEmpty: Bool { count == 0 } - /// The starting index of the arguments passed to the SQLite function. + /// The index of the first argument. public var startIndex: Index { 0 } - /// The ending index of the arguments passed to the SQLite function. + /// The index immediately after the last valid argument. public var endIndex: Index { count } // MARK: - Inits - /// Initializes the argument list with the provided count and pointer to SQLite values. - /// - /// - Parameters: - /// - argc: The number of arguments. - /// - argv: A pointer to an array of `OpaquePointer?` representing SQLite values. init(argc: Int32, argv: UnsafeMutablePointer?) { self.argc = argc self.argv = argv @@ -63,18 +43,17 @@ extension Function { // MARK: - Subscripts - /// Accesses the SQLite value at the specified index. + /// Returns the SQLite value at the specified index. /// - /// - Parameter index: The index of the SQLite value to access. + /// Retrieves the raw value from the SQLite function arguments and returns it as an + /// instance of ``SQLiteValue``. + /// + /// - Parameter index: The index of the argument to retrieve. /// - Returns: The SQLite value at the specified index. - /// - /// This subscript allows accessing the SQLite value at a specific index within the argument list. - /// If the index is out of bounds, a fatal error is triggered. - /// /// - Complexity: O(1) public subscript(index: Index) -> Element { - guard count > index else { - fatalError("\(index) out of bounds") + guard index < count else { + fatalError("Index \(index) out of bounds") } let arg = argv.unsafelyUnwrapped[index] switch sqlite3_value_type(arg) { @@ -86,31 +65,12 @@ extension Function { } } - /// Accesses the SQLite value at the specified index and converts it to a type conforming to - /// `SQLiteConvertible`. - /// - /// - Parameter index: The index of the SQLite value to access. - /// - Returns: The SQLite value at the specified index, converted to the specified type, - /// or `nil` if conversion fails. - /// - /// This subscript allows accessing the SQLite value at a specific index within the argument - /// list and converting it to a type conforming to `SQLiteConvertible`. - /// - /// - Complexity: O(1) - public subscript(index: Index) -> T? { - T(self[index]) - } - // MARK: - Methods - /// Returns the index after the specified index. - /// - /// - Parameter i: The index. - /// - Returns: The index immediately after the specified index. - /// - /// This method is used to advance to the next index in the argument list when iterating over - /// its elements. + /// Returns the index that follows the specified index. /// + /// - Parameter i: The current index. + /// - Returns: The index immediately after the specified one. /// - Complexity: O(1) public func index(after i: Index) -> Index { i + 1 @@ -120,39 +80,10 @@ extension Function { // MARK: - Functions -/// Retrieves the textual data from an SQLite value. -/// -/// - Parameter value: An opaque pointer to an SQLite value. -/// - Returns: A `String` representing the text value extracted from the SQLite value. -/// -/// This function retrieves the textual data from an SQLite value and converts it into a Swift `String`. -/// -/// - Note: The returned string may contain UTF-8 encoded text. -/// - Note: Ensure the provided `OpaquePointer` is valid and points to a valid SQLite value. -/// Passing a null pointer will result in undefined behavior. -/// -/// - Important: This function does not perform error checking for null pointers or invalid SQLite values. -/// It is the responsibility of the caller to ensure the validity of the provided pointer. -/// -/// - SeeAlso: [SQLite Documentation](https://www.sqlite.org/index.html) private func sqlite3_value_text(_ value: OpaquePointer!) -> String { String(cString: DataLiteC.sqlite3_value_text(value)) } -/// Retrieves binary data from an SQLite value. -/// -/// - Parameter value: An opaque pointer to an SQLite value. -/// - Returns: A `Data` object representing the binary data extracted from the SQLite value. -/// -/// This function retrieves binary data from an SQLite value and converts it into a Swift `Data` object. -/// -/// - Note: Ensure the provided `OpaquePointer` is valid and points to a valid SQLite value. -/// Passing a null pointer will result in undefined behavior. -/// -/// - Important: This function does not perform error checking for null pointers or invalid SQLite values. -/// It is the responsibility of the caller to ensure the validity of the provided pointer. -/// -/// - SeeAlso: [SQLite Documentation](https://www.sqlite.org/index.html) private func sqlite3_value_blob(_ value: OpaquePointer!) -> Data { Data( bytes: sqlite3_value_blob(value), diff --git a/Sources/DataLiteCore/Classes/Function+Options.swift b/Sources/DataLiteCore/Classes/Function+Options.swift index fbee4f2..5ca96ec 100644 --- a/Sources/DataLiteCore/Classes/Function+Options.swift +++ b/Sources/DataLiteCore/Classes/Function+Options.swift @@ -2,57 +2,50 @@ import Foundation import DataLiteC extension Function { - /// An option set representing the options for an SQLite function. + /// An option set representing the configuration flags for an SQLite function. /// - /// This structure defines an option set to configure various options for an SQLite function. - /// Options can be combined using bitwise OR operations. + /// The `Options` structure defines a set of flags that control the behavior of a user-defined + /// SQLite function. Multiple options can be combined using bitwise OR operations. /// - /// Example usage: - /// ```swift - /// let options: Function.Options = [.deterministic, .directonly] - /// ``` - /// - /// - SeeAlso: [SQLite Function Flags](https://www.sqlite.org/c3ref/c_deterministic.html) + /// - SeeAlso: [Function Flags](https://sqlite.org/c3ref/c_deterministic.html) public struct Options: OptionSet, Hashable, Sendable { // MARK: - Properties - /// The raw value type used to store the SQLite function options. + /// The raw integer value representing the combined SQLite function options. public var rawValue: Int32 // MARK: - Options - /// Indicates that the function is deterministic. + /// Marks the function as deterministic. /// - /// A deterministic function always gives the same output when it has the same input parameters. - /// For example, a mathematical function like sqrt() is deterministic. + /// A deterministic function always produces the same output for the same input parameters. + /// For example, mathematical functions like `sqrt()` or `abs()` are deterministic. public static let deterministic = Self(rawValue: SQLITE_DETERMINISTIC) - /// Indicates that the function may only be invoked from top-level SQL. + /// Restricts the function to be invoked only from top-level SQL. /// - /// A function with the `directonly` option cannot be used in a VIEWs or TRIGGERs, or in schema structures - /// such as CHECK constraints, DEFAULT clauses, expression indexes, partial indexes, or generated columns. + /// A function with the `directonly` flag cannot be used in views, triggers, or schema + /// definitions such as `CHECK` constraints, `DEFAULT` clauses, expression indexes, partial + /// indexes, or generated columns. /// - /// The `directonly` option is recommended for any application-defined SQL function that has side-effects - /// or that could potentially leak sensitive information. This will prevent attacks in which an application - /// is tricked into using a database file that has had its schema surreptitiously modified to invoke the - /// application-defined function in ways that are harmful. + /// This option is recommended for functions that may have side effects or expose sensitive + /// information. It helps prevent attacks involving maliciously crafted database schemas + /// that attempt to invoke such functions implicitly. public static let directonly = Self(rawValue: SQLITE_DIRECTONLY) - /// Indicates that the function is innocuous. + /// Marks the function as innocuous. /// - /// The `innocuous` option means that the function is unlikely to cause problems even if misused. - /// An innocuous function should have no side effects and should not depend on any values other - /// than its input parameters. - /// The `abs()` function is an example of an innocuous function. - /// The `load_extension()` SQL function is not innocuous because of its side effects. + /// The `innocuous` flag indicates that the function is safe even if misused. Such a + /// function should have no side effects and depend only on its input parameters. For + /// instance, `abs()` is innocuous, while `load_extension()` is not due to its side effects. /// - /// `innocuous` is similar to `deterministic`, but is not exactly the same. - /// The `random()` function is an example of a function that is innocuous but not deterministic. + /// This option is similar to ``deterministic`` but not identical. For example, `random()` + /// is innocuous but not deterministic. public static let innocuous = Self(rawValue: SQLITE_INNOCUOUS) // MARK: - Inits - /// Creates an SQLite function option set from a raw value. + /// Creates a new set of SQLite function options from the specified raw value. /// /// - Parameter rawValue: The raw value representing the SQLite function options. public init(rawValue: Int32) { diff --git a/Sources/DataLiteCore/Classes/Function+Regexp.swift b/Sources/DataLiteCore/Classes/Function+Regexp.swift index 41e6aef..cec89fd 100644 --- a/Sources/DataLiteCore/Classes/Function+Regexp.swift +++ b/Sources/DataLiteCore/Classes/Function+Regexp.swift @@ -13,7 +13,7 @@ extension Function { /// ) /// try connection.add(function: Function.Regexp.self) /// - /// try connection.execute(sql: """ + /// try connection.execute(raw: """ /// SELECT * FROM users WHERE name REGEXP 'John.*'; /// """) /// ``` @@ -64,8 +64,8 @@ extension Function { /// - Throws: ``Error/invalidArguments`` if the arguments are invalid or missing. /// - Throws: ``Error/regexError(_:)`` if an error occurs during regex evaluation. public override class func invoke( - args: Arguments - ) throws -> SQLiteRawRepresentable? { + args: any ArgumentsProtocol + ) throws -> SQLiteRepresentable? { guard let regex = args[0] as String?, let value = args[1] as String? else { throw Error.invalidArguments } diff --git a/Sources/DataLiteCore/Classes/Function+Scalar.swift b/Sources/DataLiteCore/Classes/Function+Scalar.swift index 9a4d243..0cd33da 100644 --- a/Sources/DataLiteCore/Classes/Function+Scalar.swift +++ b/Sources/DataLiteCore/Classes/Function+Scalar.swift @@ -2,19 +2,21 @@ import Foundation import DataLiteC extension Function { - /// A base class for creating custom scalar SQLite functions. + /// A base class for defining custom scalar SQLite functions. /// - /// This class provides a base implementation for creating scalar functions in SQLite. - /// Scalar functions take one or more input arguments and return a single value. To - /// create a custom scalar function, subclass `Function.Scalar` and override the - /// ``name``, ``argc``, ``options``, and ``invoke(args:)`` methods. + /// The `Scalar` class provides a foundation for defining scalar functions in SQLite. Scalar + /// functions take one or more input arguments and return a single value for each function call. + /// + /// To define a custom scalar function, subclass `Function.Scalar` and override the following + /// members: + /// + /// - ``name`` – The SQL name of the function. + /// - ``argc`` – The number of arguments the function accepts. + /// - ``options`` – Function options, such as `.deterministic` or `.innocuous`. + /// - ``invoke(args:)`` – The method implementing the function’s logic. /// /// ### Example /// - /// To create a custom scalar function, subclass `Function.Scalar` and implement the - /// required methods. Here's an example of creating a custom `REGEXP` function that - /// checks if a string matches a regular expression. - /// /// ```swift /// @available(macOS 13.0, *) /// final class Regexp: Function.Scalar { @@ -23,19 +25,15 @@ extension Function { /// case regexError(Swift.Error) /// } /// - /// // MARK: - Properties - /// /// override class var argc: Int32 { 2 } /// override class var name: String { "REGEXP" } /// override class var options: Function.Options { /// [.deterministic, .innocuous] /// } /// - /// // MARK: - Methods - /// /// override class func invoke( - /// args: Function.Arguments - /// ) throws -> SQLiteRawRepresentable? { + /// args: ArgumentsProtocol + /// ) throws -> SQLiteRepresentable? { /// guard let regex = args[0] as String?, /// let value = args[1] as String? /// else { throw Error.argumentsWrong } @@ -50,8 +48,7 @@ extension Function { /// /// ### Usage /// - /// Once you've created your custom function, you need to install it into the SQLite database - /// connection. Here's how you can add the `Regexp` function to a ``Connection`` instance: + /// To use a custom function, register it with an SQLite connection: /// /// ```swift /// let connection = try Connection( @@ -63,71 +60,15 @@ extension Function { /// /// ### SQL Example /// - /// With the `Regexp` function installed, you can use it in your SQL queries. For - /// example, to find rows where the `name` column matches the regular expression - /// `John.*`, you would write: + /// After registration, the function becomes available in SQL expressions: /// /// ```sql - /// -- Find rows where 'name' column matches the regular expression 'John.*' /// SELECT * FROM users WHERE REGEXP('John.*', name); /// ``` open class Scalar: Function { - // MARK: - Context - - /// A helper class to store and manage context for a custom scalar SQLite function. - /// - /// This class is used internally to hold a reference to the `Scalar` function - /// implementation. It is created and managed during the installation of the scalar - /// function into the SQLite database connection. The context is passed to SQLite - /// and used to call the appropriate function implementation when the function is - /// invoked. - fileprivate final class Context { - // MARK: Properties - - /// The type of the `Scalar` function being managed. - /// - /// This property holds a reference to the `Scalar` subclass that implements the - /// custom scalar function logic. It is used to invoke the function with the - /// provided arguments. - let function: Scalar.Type - - // MARK: Inits - - /// Initializes a new `Context` with a reference to the `Scalar` function type. - /// - /// - Parameter function: The `Scalar` subclass that implements the custom scalar - /// function. - init(function: Scalar.Type) { - self.function = function - } - } - // MARK: - Methods - /// Installs a custom scalar SQLite function into the specified database connection. - /// - /// This method registers the scalar function with the SQLite database. It creates - /// a `Context` object to hold a reference to the function implementation and sets up - /// the function using `sqlite3_create_function_v2`. The context is passed to SQLite, - /// allowing the implementation to be called later. - /// - /// ```swift - /// // Assume the database connection is already open - /// let db: OpaquePointer = ... - /// // Registering the function in the database - /// try MyCustomScalar.install(db: db) - /// ``` - /// - /// - Parameter connection: A pointer to the SQLite database connection where the - /// function will be installed. - /// - /// - Throws: ``Connection/Error`` if the function installation fails. This error occurs if - /// the call to `sqlite3_create_function_v2` does not return `SQLITE_OK`. - /// - /// - Note: This method should be called to register the custom function before using - /// it in SQL queries. Ensure the database connection is open and available at the - /// time of this method call. - override class func install(db connection: OpaquePointer) throws(Connection.Error) { + override class func install(db connection: OpaquePointer) throws(SQLiteError) { let context = Context(function: self) let ctx = Unmanaged.passRetained(context).toOpaque() let status = sqlite3_create_function_v2( @@ -135,69 +76,58 @@ extension Function { xFunc(_:_:_:), nil, nil, xDestroy(_:) ) if status != SQLITE_OK { - throw Connection.Error(connection) + throw SQLiteError(connection) } } - /// Implementation of the custom scalar function. + /// Implements the logic of the custom scalar function. /// - /// This method must be overridden by subclasses to implement the specific logic - /// of the function. Your implementation should handle the input arguments and return - /// the result as ``SQLiteRawRepresentable``. + /// Subclasses must override this method to process the provided arguments and return a + /// result value for the scalar function call. /// - /// - Parameter args: An ``Arguments`` object containing the input arguments of the - /// function. + /// - Parameter args: The set of arguments passed to the function. + /// - Returns: The result of the function call, represented as ``SQLiteRepresentable``. + /// - Throws: An error if the arguments are invalid or the computation fails. /// - /// - Returns: The result of the function execution, represented as - /// ``SQLiteRawRepresentable``. - /// - /// - Throws: An error if the function execution fails. Subclasses can throw - /// errors for invalid input values or other issues during processing. - /// - /// - Note: It is important to override this method in subclasses; otherwise, - /// a runtime error will occur due to calling `fatalError()`. - open class func invoke(args: Arguments) throws -> SQLiteRawRepresentable? { + /// - Note: The default implementation triggers a runtime error. + open class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? { fatalError("Subclasses must override this method to implement function logic.") } } } +extension Function.Scalar { + fileprivate final class Context { + // MARK: - Properties + + let function: Scalar.Type + + // MARK: - Inits + + init(function: Scalar.Type) { + self.function = function + } + } +} + // MARK: - Functions -/// The C function callback for executing a custom SQLite scalar function. -/// -/// This function is called by SQLite when the scalar function is invoked. It retrieves the -/// function implementation from the context associated with the SQLite query, invokes the -/// function with the provided arguments, and sets the result of the query based on the -/// returned value. If an error occurs during the function invocation, it sets an error -/// message. -/// -/// - Parameters: -/// - ctx: A pointer to the SQLite context associated with the current query. This context -/// contains information about the query execution and is used to set the result or error. -/// - argc: The number of arguments passed to the function. This is used to determine how -/// many arguments are available in the `argv` array. -/// - argv: An array of pointers to the values of the arguments passed to the function. Each -/// pointer corresponds to a value that the function will process. -/// -/// - Note: The `xFunc` function should handle the function invocation logic, including -/// argument extraction and result setting. It should also handle errors by setting -/// appropriate error messages using `sqlite3_result_error`. private func xFunc( _ ctx: OpaquePointer?, _ argc: Int32, _ argv: UnsafeMutablePointer? ) { - let context = Unmanaged + let function = Unmanaged .fromOpaque(sqlite3_user_data(ctx)) .takeUnretainedValue() + .function do { let args = Function.Arguments(argc: argc, argv: argv) - let result = try context.function.invoke(args: args) - sqlite3_result_value(ctx, result?.sqliteRawValue) + let result = try function.invoke(args: args) + sqlite3_result_value(ctx, result?.sqliteValue) } catch { - let name = context.function.name + let name = function.name let description = error.localizedDescription let message = "Error executing function '\(name)': \(description)" sqlite3_result_error(ctx, message, -1) @@ -205,17 +135,6 @@ private func xFunc( } } -/// The C function callback for destroying the context associated with a custom SQLite scalar function. -/// -/// This function is called by SQLite when the function is uninstalled. It releases the memory -/// allocated for the `Context` object associated with the function to avoid memory leaks. -/// -/// - Parameter ctx: A pointer to the context of the SQLite query. This context contains the -/// `Context` object that should be released. -/// -/// - Note: The `xDestroy` function should only release the memory allocated for the `Context` -/// object. It should not perform any other operations or access the context beyond freeing -/// the memory. private func xDestroy(_ ctx: UnsafeMutableRawPointer?) { guard let ctx else { return } Unmanaged.fromOpaque(ctx).release() diff --git a/Sources/DataLiteCore/Classes/Function.swift b/Sources/DataLiteCore/Classes/Function.swift index 8b2c748..88e5e24 100644 --- a/Sources/DataLiteCore/Classes/Function.swift +++ b/Sources/DataLiteCore/Classes/Function.swift @@ -1,70 +1,66 @@ import Foundation import DataLiteC -/// A base class representing a custom SQLite function. +/// A base class representing a user-defined SQLite function. /// -/// This class provides a framework for defining custom functions in SQLite. Subclasses must -/// override specific properties and methods to define the function's behavior, including -/// its name, argument count, and options. +/// The `Function` class defines the common interface and structure for implementing custom SQLite +/// functions. Subclasses are responsible for specifying the function name, argument count, and +/// behavior. This class should not be used directly — instead, use one of its specialized +/// subclasses, such as ``Scalar`` or ``Aggregate``. /// -/// To create a custom SQLite function, you should subclass either ``Scalar`` or -/// ``Aggregate`` depending on whether your function is a scalar function (returns -/// a single result) or an aggregate function (returns a result accumulated from multiple -/// rows). The subclass will then override the necessary properties and methods to implement -/// the function's behavior. +/// To define a new SQLite function, subclass either ``Scalar`` or ``Aggregate`` depending on +/// whether the function computes a value from a single row or aggregates results across multiple +/// rows. Override the required properties and implement the necessary logic to define the +/// function’s behavior. /// /// ## Topics /// /// ### Base Function Classes /// -/// - ``Aggregate`` /// - ``Scalar`` +/// - ``Aggregate`` /// /// ### Custom Function Classes /// /// - ``Regexp`` +/// +/// ### Configuration +/// +/// - ``argc`` +/// - ``name`` +/// - ``options`` +/// - ``Options`` open class Function { // MARK: - Properties - /// The number of arguments that the custom SQLite function accepts. + /// The number of arguments that the function accepts. /// - /// This property must be overridden by subclasses to specify how many arguments - /// the function expects. The value should be a positive integer representing the - /// number of arguments, or zero if the function does not accept arguments. + /// Subclasses must override this property to specify the expected number of arguments. The + /// value should be a positive integer, or zero if the function does not accept any arguments. open class var argc: Int32 { fatalError("Subclasses must override this property to specify the number of arguments.") } - /// The name of the custom SQLite function. + /// The name of the function. /// - /// This property must be overridden by subclasses to provide the name that the SQLite - /// engine will use to identify the function. The name should be a valid SQLite function - /// name according to SQLite naming conventions. + /// Subclasses must override this property to provide the name by which the SQLite engine + /// identifies the function. The name must comply with SQLite function naming rules. open class var name: String { fatalError("Subclasses must override this property to provide the function name.") } - /// The options for the custom SQLite function. + /// The configuration options for the function. /// - /// This property must be overridden by subclasses to specify options such as whether the - /// function is deterministic or not. Options are represented as a bitmask of `Function.Options`. + /// Subclasses must override this property to specify the function’s behavioral flags, such as + /// whether it is deterministic, direct-only, or innocuous. open class var options: Options { fatalError("Subclasses must override this property to specify function options.") } - /// The encoding used by the function, which defaults to UTF-8. - /// - /// This is used to set the encoding for text data in the custom SQLite function. The default - /// encoding is UTF-8, but this can be modified if necessary. This encoding is combined with - /// the function's options to configure the function. class var encoding: Function.Options { Function.Options(rawValue: SQLITE_UTF8) } - /// The combined options for the custom SQLite function. - /// - /// This property combines the function's options with the encoding. The result is used when - /// registering the function with SQLite. This property is derived from `options` and `encoding`. class var opts: Int32 { var options = options options.insert(encoding) @@ -73,91 +69,35 @@ open class Function { // MARK: - Methods - /// Installs the custom SQLite function into the specified database connection. - /// - /// Subclasses must override this method to provide the implementation for installing - /// the function into the SQLite database. This typically involves registering the function - /// with SQLite using `sqlite3_create_function_v2` or similar APIs. - /// - /// - Parameter connection: A pointer to the SQLite database connection where the function - /// will be installed. - /// - Throws: An error if the function installation fails. The method will throw an exception - /// if the installation cannot be completed successfully. - class func install(db connection: OpaquePointer) throws(Connection.Error) { + class func install(db connection: OpaquePointer) throws(SQLiteError) { fatalError("Subclasses must override this method to implement function installation.") } - /// Uninstalls the custom SQLite function from the specified database connection. - /// - /// This method unregisters the function from the SQLite database using `sqlite3_create_function_v2` - /// with `NULL` for the function implementations. This effectively removes the function from the - /// database. - /// - /// - Parameter connection: A pointer to the SQLite database connection from which the function - /// will be uninstalled. - /// - Throws: An error if the function uninstallation fails. An exception is thrown if the function - /// cannot be removed successfully. - class func uninstall(db connection: OpaquePointer) throws(Connection.Error) { + class func uninstall(db connection: OpaquePointer) throws(SQLiteError) { let status = sqlite3_create_function_v2( connection, name, argc, opts, nil, nil, nil, nil, nil ) if status != SQLITE_OK { - throw Connection.Error(connection) + throw SQLiteError(connection) } } } // MARK: - Functions -/// Sets the result of an SQLite query as a text string. -/// -/// This function sets the result of the query to the specified text string. SQLite will store -/// this string inside the database as the result of the custom function. -/// -/// - Parameters: -/// - ctx: A pointer to the SQLite context that provides information about the current query. -/// - string: A `String` that will be returned as the result of the query. -/// -/// - Note: The `SQLITE_TRANSIENT` flag is used, meaning that SQLite makes a copy of the passed -/// data. This ensures that the string remains valid after the function execution is completed. func sqlite3_result_text(_ ctx: OpaquePointer!, _ string: String) { sqlite3_result_text(ctx, string, -1, SQLITE_TRANSIENT) } -/// Sets the result of an SQLite query as binary data (BLOB). -/// -/// This function sets the result of the query to the specified binary data. This is useful for -/// returning non-textual data such as images or other binary content from a custom function. -/// -/// - Parameters: -/// - ctx: A pointer to the SQLite context that provides information about the current query. -/// - data: A `Data` object representing the binary data to be returned as the result. -/// -/// - Note: The `SQLITE_TRANSIENT` flag is used, ensuring that SQLite makes a copy of the binary -/// data. This prevents issues related to memory management if the original data is modified -/// or deallocated after the function completes. func sqlite3_result_blob(_ ctx: OpaquePointer!, _ data: Data) { data.withUnsafeBytes { sqlite3_result_blob(ctx, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT) } } -/// Sets the result of an SQLite query based on the `SQLiteRawValue` type. -/// -/// This function sets the result of the query according to the type of the provided value. It can -/// handle integers, floating-point numbers, strings, binary data, or `NULL` values. -/// -/// - Parameters: -/// - ctx: A pointer to the SQLite context that provides information about the current query. -/// - value: A `SQLiteRawValue` that represents the result to be returned. If the value is `nil`, -/// the result will be set to `NULL`. -/// -/// - Note: The function uses a `switch` statement to determine the type of the value and then -/// calls the appropriate SQLite function to set the result. This ensures that the correct SQLite -/// result type is used based on the provided value. -func sqlite3_result_value(_ ctx: OpaquePointer!, _ value: SQLiteRawValue?) { +func sqlite3_result_value(_ ctx: OpaquePointer!, _ value: SQLiteValue?) { switch value ?? .null { case .int(let value): sqlite3_result_int64(ctx, value) case .real(let value): sqlite3_result_double(ctx, value) diff --git a/Sources/DataLiteCore/Classes/Statement+Arguments.swift b/Sources/DataLiteCore/Classes/Statement+Arguments.swift deleted file mode 100644 index 124ddfc..0000000 --- a/Sources/DataLiteCore/Classes/Statement+Arguments.swift +++ /dev/null @@ -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 - - /// The value type used in the underlying elements dictionary. - /// - /// This represents a SQLite-compatible raw value, such as a string, number, or null. - public typealias RawValue = Elements.Value - - /// The index type used to traverse the arguments collection. - public typealias Index = Elements.Index - - /// A key–value pair representing an argument token and its associated value. - public typealias Element = (token: Token, value: RawValue) - - // MARK: - Private Properties - - private var elements: Elements - - // MARK: - Public Properties - - /// The starting index of the arguments collection, which is always zero. - /// - /// This property represents the initial position in the arguments collection. - /// Since the elements are indexed starting from zero, it consistently returns zero, - /// allowing predictable forward iteration. - /// - /// - Complexity: `O(1)` - public var startIndex: Index { - 0 - } - - /// The ending index of the arguments collection, equal to the number of elements. - /// - /// This property marks the position one past the last element in the collection. - /// It returns the total number of arguments and defines the upper bound for iteration - /// over tokens and their associated values. - /// - /// - Complexity: `O(1)` - public var endIndex: Index { - elements.count - } - - /// A Boolean value indicating whether the arguments collection is empty. - /// - /// Returns `true` if the collection contains no arguments; otherwise, returns `false`. - /// - /// - Complexity: `O(1)` - public var isEmpty: Bool { - elements.isEmpty - } - - /// The number of arguments in the collection. - /// - /// This property reflects the total number of token–value pairs - /// currently stored in the arguments set. - /// - /// - Complexity: `O(1)` - public var count: Int { - elements.count - } - - /// A textual representation of the arguments collection. - /// - /// The description includes all tokens and their associated values - /// in the order they appear in the collection. This is useful for debugging. - /// - /// - Complexity: `O(n)` - public var description: String { - elements.description - } - - /// An array of all tokens present in the arguments collection. - /// - /// The tokens are returned in insertion order and include both - /// indexed and named forms, depending on how the arguments were constructed. - /// - /// - Complexity: `O(1)` - public var tokens: [Token] { - elements.keys.elements - } - - // MARK: - Inits - - /// Initializes an empty `Arguments`. - /// - /// - Complexity: `O(1)` - public init() { - self.elements = [:] - } - - /// Initializes `Arguments` with an array of values. - /// - /// - Parameter elements: An array of `SQLiteRawBindable` values. - /// - /// - Complexity: `O(n)`, where `n` is the number of elements in the input array. - public init(_ elements: [SQLiteRawBindable?]) { - self.elements = .init( - uniqueKeysWithValues: elements.enumerated().map { offset, value in - (.indexed(index: offset + 1), value?.sqliteRawValue ?? .null) - } - ) - } - - /// Initializes `Arguments` with a dictionary of named values. - /// - /// - Parameter elements: A dictionary mapping names to `SQLiteRawBindable` values. - /// - /// - Complexity: `O(n)`, where `n` is the number of elements in the input dictionary. - public init(_ elements: [String: SQLiteRawBindable?]) { - self.elements = .init( - uniqueKeysWithValues: elements.map { name, value in - (.named(name: name), value?.sqliteRawValue ?? .null) - } - ) - } - - /// Initializes `Arguments` from an array literal. - /// - /// This initializer enables array literal syntax for positional (indexed) arguments. - /// - /// ```swift - /// let args: Statement.Arguments = ["Alice", 42] - /// ``` - /// - /// Each value is bound to a token of the form `?1`, `?2`, etc., based on its position. - /// - /// - Complexity: `O(n)`, where `n` is the number of elements. - public init(arrayLiteral elements: SQLiteRawBindable?...) { - self.elements = .init( - uniqueKeysWithValues: elements.enumerated().map { offset, value in - (.indexed(index: offset + 1), value?.sqliteRawValue ?? .null) - } - ) - } - - /// Initializes `Arguments` from a dictionary literal. - /// - /// This initializer enables dictionary literal syntax for named arguments. - /// - /// ```swift - /// let args: Statement.Arguments = ["name": "Alice", "age": 42] - /// ``` - /// - /// Each key becomes a named token (`:name`, `:age`, etc.). - /// - /// - Complexity: `O(n)`, where `n` is the number of elements. - public init(dictionaryLiteral elements: (String, SQLiteRawBindable?)...) { - self.elements = .init( - uniqueKeysWithValues: elements.map { name, value in - (.named(name: name), value?.sqliteRawValue ?? .null) - } - ) - } - - // MARK: - Subscripts - - /// Accesses the element at the specified position. - /// - /// This subscript returns the `(token, value)` pair located at the given index - /// in the arguments collection. The order of elements reflects their insertion order. - /// - /// - Parameter index: The position of the element to access. - /// - Returns: A tuple containing the token and its associated value. - /// - /// - Complexity: `O(1)` - public subscript(index: Index) -> Element { - let element = elements.elements[index] - return (element.key, element.value) - } - - // MARK: - Methods - - /// Returns the position immediately after the given index. - /// - /// Use this method to advance an index when iterating over the arguments collection. - /// - /// - Parameter i: A valid index of the collection. - /// - Returns: The index value immediately following `i`. - /// - /// - Complexity: `O(1)` - public func index(after i: Index) -> Index { - i + 1 - } - - /// Returns a Boolean value indicating whether the specified token exists in the arguments. - /// - /// Use this method to check whether a token—either indexed or named—is present in the collection. - /// - /// - Parameter token: The token to search for in the arguments. - /// - Returns: `true` if the token exists in the collection; otherwise, `false`. - /// - /// - Complexity: On average, the complexity is `O(1)`. - public func contains(_ token: Token) -> Bool { - elements.keys.contains(token) - } - - /// Merges the contents of another `Arguments` instance into this one using a custom resolver. - /// - /// For each token present in `other`, the method either inserts the new value - /// or resolves conflicts when the token already exists in the current collection. - /// - /// - Parameters: - /// - other: Another `Arguments` instance whose contents will be merged into this one. - /// - resolve: A closure that determines how to resolve conflicts between existing and new values. - /// - Complexity: `O(n)`, where `n` is the number of elements in `other`. - public mutating func merge(with other: Self, using resolve: Resolver) { - for (token, newValue) in other.elements { - if let index = elements.index(forKey: token) { - let currentValue = elements.values[index] - switch resolve(token, currentValue, newValue) { - case .ignore: continue - case .replace: elements[token] = newValue - } - } else { - elements[token] = newValue - } - } - } - - /// Merges the contents of another `Arguments` instance into this one using a fixed conflict resolution strategy. - /// - /// This variant applies the same resolution strategy to all conflicts without requiring a custom closure. - /// - /// - Parameters: - /// - other: Another `Arguments` instance whose contents will be merged into this one. - /// - resolution: A fixed strategy to apply when a token conflict occurs. - /// - Complexity: `O(n)`, where `n` is the number of elements in `other`. - public mutating func merge(with other: Self, using resolution: ConflictResolution) { - merge(with: other) { _, _, _ in resolution } - } - - /// Returns a new `Arguments` instance by merging the contents of another one using a custom resolver. - /// - /// This method creates a copy of the current arguments and merges `other` into it. - /// For each conflicting token, the provided resolver determines whether to keep the existing value - /// or replace it with the new one. - /// - /// - Parameters: - /// - other: Another `Arguments` instance whose contents will be merged into the copy. - /// - resolve: A closure that determines how to resolve conflicts between existing and new values. - /// - Returns: A new `Arguments` instance containing the merged values. - /// - Complexity: `O(n)`, where `n` is the number of elements in `other`. - public func merged(with other: Self, using resolve: Resolver) -> Self { - var copy = self - copy.merge(with: other, using: resolve) - return copy - } - - /// Returns a new `Arguments` instance by merging the contents of another one using a fixed strategy. - /// - /// This variant uses the same resolution strategy for all conflicts without requiring a custom closure. - /// - /// - Parameters: - /// - other: Another `Arguments` instance whose contents will be merged into the copy. - /// - resolution: A fixed strategy to apply when a token conflict occurs. - /// - Returns: A new `Arguments` instance containing the merged values. - /// - Complexity: `O(n)`, where `n` is the number of elements in `other`. - public func merged(with other: Self, using resolution: ConflictResolution) -> Self { - merged(with: other) { _, _, _ in resolution } - } - } -} diff --git a/Sources/DataLiteCore/Classes/Statement+Options.swift b/Sources/DataLiteCore/Classes/Statement+Options.swift index 88b80d8..e74c16b 100644 --- a/Sources/DataLiteCore/Classes/Statement+Options.swift +++ b/Sources/DataLiteCore/Classes/Statement+Options.swift @@ -2,34 +2,19 @@ import Foundation import DataLiteC extension Statement { - /// Provides a set of options for preparing SQLite statements. + /// A set of options that control how an SQLite statement is prepared. /// - /// This struct conforms to the `OptionSet` protocol, allowing multiple options to be combined using - /// bitwise operations. Each option corresponds to a specific SQLite preparation flag. + /// `Options` conforms to the `OptionSet` protocol, allowing multiple flags to be combined. + /// Each option corresponds to a specific SQLite preparation flag. /// - /// ## Example - /// - /// ```swift - /// let options: Statement.Options = [.persistent, .noVtab] - /// - /// if options.contains(.persistent) { - /// print("Persistent option is set") - /// } - /// - /// if options.contains(.noVtab) { - /// print("noVtab option is set") - /// } - /// ``` - /// - /// The example demonstrates how to create an `Options` instance with `persistent` and `noVtab` - /// options set, and then check each option using the `contains` method. + /// - SeeAlso: [Prepare Flags](https://sqlite.org/c3ref/c_prepare_normalize.html) /// /// ## Topics /// /// ### Initializers /// - /// - ``init(rawValue:)-(Int32)`` /// - ``init(rawValue:)-(UInt32)`` + /// - ``init(rawValue:)-(Int32)`` /// /// ### Instance Properties /// @@ -42,83 +27,44 @@ extension Statement { public struct Options: OptionSet, Sendable { // MARK: - Properties - /// The underlying raw value representing the set of options as a bitmask. + /// The raw bitmask value that represents the combined options. /// - /// Each bit in the raw value corresponds to a specific option in the `Statement.Options` set. You can - /// use this value to perform low-level bitmask operations or to directly initialize an `Options` - /// instance. - /// - /// ## Example - /// - /// ```swift - /// let options = Statement.Options( - /// rawValue: SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB - /// ) - /// print(options.rawValue) // Output: bitmask representing the combined options - /// ``` - /// - /// The example shows how to access the raw bitmask value from an `Options` instance. + /// Each bit in the mask corresponds to a specific SQLite preparation flag. ou can use this + /// value for low-level bitwise operations or to construct an `Options` instance directly. public var rawValue: UInt32 - /// Specifies that the prepared statement should be persistent and reusable. + /// Indicates that the prepared statement is persistent and reusable. /// - /// The `persistent` flag hints to SQLite that the prepared statement will be retained and reused - /// multiple times. Without this flag, SQLite assumes the statement will be used only once or a few - /// times and then destroyed. + /// This flag hints to SQLite that the prepared statement will be kept and reused multiple + /// times. Without this hint, SQLite assumes the statement will be used only a few times and + /// then destroyed. /// - /// The current implementation uses this hint to avoid depleting the limited store of lookaside - /// memory, potentially improving performance for frequently executed statements. Future versions - /// of SQLite may handle this flag differently. - public static let persistent = Self(rawValue: UInt32(SQLITE_PREPARE_PERSISTENT)) + /// Using `.persistent` can help avoid excessive lookaside memory usage and improve + /// performance for frequently executed statements. + public static let persistent = Self(rawValue: SQLITE_PREPARE_PERSISTENT) - /// Specifies that virtual tables should not be used in the prepared statement. + /// Disables the use of virtual tables in the prepared statement. /// - /// The `noVtab` flag instructs SQLite to prevent the use of virtual tables when preparing the SQL - /// statement. This can be useful in cases where the use of virtual tables is undesirable or - /// restricted by the application logic. If this flag is set, any attempt to access a virtual table - /// during the execution of the prepared statement will result in an error. - /// - /// This option ensures that the prepared statement will only work with standard database tables. - public static let noVtab = Self(rawValue: UInt32(SQLITE_PREPARE_NO_VTAB)) + /// When this flag is set, any attempt to reference a virtual table during statement + /// preparation results in an error. Use this option when virtual tables are restricted or + /// undesirable for security or policy reasons. + public static let noVtab = Self(rawValue: SQLITE_PREPARE_NO_VTAB) // MARK: - Inits - /// Initializes an `Options` instance with the given `UInt32` raw value. + /// Creates a new set of options from a raw `UInt32` bitmask value. /// - /// Use this initializer to create a set of options using the raw bitmask value, where each bit - /// corresponds to a specific option. - /// - /// ## Example - /// - /// ```swift - /// let options = Statement.Options( - /// rawValue: UInt32(SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB) - /// ) - /// print(options.contains(.persistent)) // Output: true - /// print(options.contains(.noVtab)) // Output: true - /// ``` - /// - /// - Parameter rawValue: The `UInt32` raw bitmask value representing the set of options. + /// - Parameter rawValue: The bitmask value that represents the combined options. public init(rawValue: UInt32) { self.rawValue = rawValue } - /// Initializes an `Options` instance with the given `Int32` raw value. + /// Creates a new set of options from a raw `Int32` bitmask value. /// - /// This initializer allows the use of `Int32` values directly, converting them to the `UInt32` type - /// required for bitmask operations. + /// This initializer allows working directly with SQLite C constants that use + /// 32-bit integers. /// - /// ## Example - /// - /// ```swift - /// let options = Statement.Options( - /// rawValue: SQLITE_PREPARE_PERSISTENT | SQLITE_PREPARE_NO_VTAB - /// ) - /// print(options.contains(.persistent)) // Output: true - /// print(options.contains(.noVtab)) // Output: true - /// ``` - /// - /// - Parameter rawValue: The `Int32` raw bitmask value representing the set of options. + /// - Parameter rawValue: The bitmask value that represents the combined options. public init(rawValue: Int32) { self.rawValue = UInt32(rawValue) } diff --git a/Sources/DataLiteCore/Classes/Statement.swift b/Sources/DataLiteCore/Classes/Statement.swift index 99d8912..05aafcc 100644 --- a/Sources/DataLiteCore/Classes/Statement.swift +++ b/Sources/DataLiteCore/Classes/Statement.swift @@ -1,687 +1,139 @@ import Foundation import DataLiteC -/// A value representing a static destructor for SQLite. +/// A prepared SQLite statement used to execute SQL commands. /// -/// `SQLITE_STATIC` is used to indicate that the SQLite library should not free the associated -/// memory when the statement is finalized. -let SQLITE_STATIC = unsafeBitCast( - OpaquePointer(bitPattern: 0), - to: sqlite3_destructor_type.self -) - -/// A value representing a transient destructor for SQLite. +/// `Statement` encapsulates the lifecycle of a compiled SQL statement, including parameter binding, +/// execution, and result retrieval. The statement is finalized automatically when the instance is +/// deallocated. /// -/// `SQLITE_TRANSIENT` is used to indicate that the SQLite library should make a copy of the -/// associated memory and free the original memory when the statement is finalized. -let SQLITE_TRANSIENT = unsafeBitCast( - OpaquePointer(bitPattern: -1), - to: sqlite3_destructor_type.self -) - -/// A class representing a prepared SQL statement in SQLite. -/// -/// ## Overview -/// -/// This class provides functionality for preparing, binding parameters, and executing SQL -/// statements using SQLite. It also supports retrieving results and resource management, ensuring -/// the statement is finalized when no longer needed. -/// -/// ## Preparing an SQL Statement -/// -/// To create a prepared SQL statement, use the ``Connection/prepare(sql:options:)`` method of the -/// ``Connection`` object. -/// -/// ```swift -/// do { -/// let statement = try connection.prepare( -/// sql: "SELECT id, name FROM users WHERE age > ?", -/// options: [.persistent, .normalize] -/// ) -/// } catch { -/// print("Error: \(error)") -/// } -/// ``` -/// -/// ## Binding Parameters -/// -/// SQL queries can contain parameters whose values can be bound after the statement is prepared. -/// This prevents SQL injection and makes the code more secure. -/// -/// ### Binding Parameters by Index -/// -/// When preparing an SQL query, you can use the question mark (`?`) as a placeholder for parameter -/// values. Parameter indexing starts from one (1). It is important to keep this in mind for -/// correctly binding values to parameters in the SQL query. The method ``bind(_:at:)-(T?,_)`` -/// is used to bind values to parameters. -/// -/// ```swift -/// do { -/// let query = "INSERT INTO users (name, age) VALUES (?, ?)" -/// let statement = try connection.prepare(sql: query) -/// try statement.bind("John Doe", at: 1) -/// try statement.bind(30, at: 2) -/// } catch { -/// print("Error binding parameters: \(error)") -/// } -/// ``` -/// -/// ### Binding Parameters by Explicit Index -/// -/// Parameters can be explicitly bound by indices `?1`, `?2`, and so on. This improves readability -/// and simplifies working with queries containing many parameters. Explicit indices do not need to -/// start from one, be sequential, or contiguous. -/// -/// ```swift -/// do { -/// let query = "INSERT INTO users (name, age) VALUES (?1, ?2)" -/// let statement = try connection.prepare(sql: query) -/// try statement.bind("Jane Doe", at: 1) -/// try statement.bind(25, at: 2) -/// } catch { -/// print("Error binding parameters: \(error)") -/// } -/// ``` -/// -/// ### Binding Parameters by Name -/// -/// Parameters can also be bound by names. This increases code readability and simplifies managing -/// complex queries. Use ``bind(parameterIndexBy:)`` to retrieve the index of a named parameter. -/// -/// ```swift -/// do { -/// let query = "INSERT INTO users (name, age) VALUES (:userName, :userAge)" -/// let statement = try connection.prepare(sql: query) -/// -/// let indexName = statement.bind(parameterIndexBy: ":userName") -/// let indexAge = statement.bind(parameterIndexBy: ":userAge") -/// -/// try statement.bind("Jane Doe", at: indexName) -/// try statement.bind(25, at: indexAge) -/// } catch { -/// print("Error binding parameters: \(error)") -/// } -/// ``` -/// -/// ### Duplicating Parameters -/// -/// Parameters with explicit indices or names can be duplicated. This allows the same value to be -/// bound to multiple places in the query. -/// -/// ```swift -/// do { -/// let query = """ -/// INSERT INTO users (name, age) -/// VALUES -/// (:userName, :userAge), -/// (:userName, :userAge) -/// """ -/// let statement = try connection.prepare(sql: query) -/// -/// let indexName = statement.bind(parameterIndexBy: ":userName") -/// let indexAge = statement.bind(parameterIndexBy: ":userAge") -/// -/// try statement.bind("Jane Doe", at: indexName) -/// try statement.bind(25, at: indexAge) -/// } catch { -/// print("Error binding parameters: \(error)") -/// } -/// ``` -/// -/// ### Mixing Indexed and Named Parameters -/// -/// You can mix positional (`?`, `?NNN`) and named (`:name`, `@name`, `$name`) parameters -/// in a single SQL statement. This is supported by SQLite and allows you to use different parameter -/// styles simultaneously. -/// -/// ```swift -/// do { -/// let query = """ -/// SELECT * FROM users WHERE age = ? AND name = :name -/// """ -/// let statement = try connection.prepare(sql: query) -/// let nameIndex = statement.bind(parameterIndexBy: ":name") -/// -/// try statement.bind(88, at: 1) -/// try statement.bind("Alice", at: nameIndex) -/// } catch { -/// print("Error binding parameters: \(error)") -/// } -/// ``` -/// -/// - Important: Although mixing parameter styles is technically allowed, it is generally not recommended. -/// For clarity and maintainability, you should consistently use either indexed or named parameters -/// throughout a query. Mixing styles may lead to confusion or hard-to-diagnose bugs in more complex queries. -/// -/// ## Generating SQL Using SQLiteRow -/// -/// The ``SQLiteRow`` type can be used not only for retrieving query results, but also for dynamically -/// generating SQL statements. Its ordered keys and parameter-friendly formatting make it especially -/// convenient for constructing `INSERT`, `UPDATE`, and similar queries with named parameters. -/// -/// ### Inserting a Row -/// -/// To insert a new row into a table using values from a ``SQLiteRow``, you can use the -/// ``SQLiteRow/columns`` and ``SQLiteRow/namedParameters`` properties. -/// This ensures the correct number and order of columns and parameters. -/// -/// ```swift -/// var row = SQLiteRow() -/// row["name"] = .text("Alice") -/// row["age"] = .int(30) -/// row["email"] = .text("alice@example.com") -/// -/// let columns = row.columns.joined(separator: ", ") // name, age, email -/// let values = row.namedParameters.joined(separator: ", ") // :name, :age, :email -/// -/// let sql = "INSERT INTO users (\(columns)) VALUES (\(values))" -/// let statement = try connection.prepare(sql: sql) -/// try statement.bind(row) -/// ``` -/// -/// This approach eliminates the need to manually write parameter placeholders or maintain their order. -/// It also ensures full compatibility with the ``bind(_:)-(SQLiteRow)`` method. -/// -/// ### Updating a Row -/// -/// To construct an `UPDATE` statement using a ``SQLiteRow``, you can dynamically -/// map the column names to SQL assignments in the form `column = :column`. -/// -/// ```swift -/// var row = SQLiteRow() -/// row["id"] = .int(123) -/// row["name"] = .text("Alice") -/// row["age"] = .int(30) -/// row["email"] = .text("alice@example.com") -/// -/// let assignments = zip(row.columns, row.namedParameters) -/// .map { "\($0.0) = \($0.1)" } -/// .joined(separator: ", ") -/// -/// let sql = "UPDATE users SET \(assignments) WHERE id = :id" -/// let statement = try connection.prepare(sql: sql) -/// try statement.bind(row) -/// try statement.step() -/// ``` -/// -/// - Important: Ensure the SQLiteRow includes any values used in conditions -/// (e.g., `:id` in `WHERE`), or binding will fail. -/// -/// ## Executing an SQL Statement -/// -/// The SQL statement is executed using the ``step()`` method. It returns `true` if there is a -/// result to process, and `false` when execution is complete. To retrieve the results of an SQL -/// statement, use ``columnCount()``, ``columnType(at:)``, ``columnName(at:)``, -/// ``columnValue(at:)->SQLiteRawValue``, and ``currentRow()``. -/// -/// ```swift -/// do { -/// let query = "SELECT id, name FROM users WHERE age > ?" -/// let statement = try connection.prepare(sql: query) -/// try statement.bind(18, at: 1) -/// while try statement.step() { -/// for index in 0..SQLiteRawValue`` -/// - ``columnValue(at:)->T?`` -/// - ``currentRow()`` -/// -/// ### Evaluating -/// -/// - ``step()`` -/// - ``reset()`` -/// - ``execute(rows:)`` -/// - ``execute(args:)`` -/// -/// ### Hashing -/// -/// - ``hash(into:)`` -public final class Statement: Equatable, Hashable { +public final class Statement { // MARK: - Private Properties - /// The SQLite statement pointer associated with this `Statement` instance. private let statement: OpaquePointer - - /// The SQLite database connection pointer used to create this statement. private let connection: OpaquePointer // MARK: - Inits - /// Initializes a new `Statement` instance with a given SQL query and options. - /// - /// This initializer prepares the SQL statement for execution and sets up any necessary - /// options. It throws an ``Connection/Error`` if the SQL preparation fails. - /// - /// - Parameters: - /// - connection: A pointer to the SQLite database connection to use. - /// - query: The SQL query string to prepare. - /// - options: The options to use when preparing the SQL statement. - /// - Throws: ``Connection/Error`` if the SQL statement preparation fails. - init(db connection: OpaquePointer, sql query: String, options: Options) throws(Connection.Error) { + init( + db connection: OpaquePointer, + sql query: String, + options: Options + ) throws(SQLiteError) { var statement: OpaquePointer! = nil - let status = sqlite3_prepare_v3(connection, query, -1, options.rawValue, &statement, nil) + let status = sqlite3_prepare_v3( + connection, query, -1, + options.rawValue, &statement, nil + ) if status == SQLITE_OK, let statement { self.statement = statement self.connection = connection } else { sqlite3_finalize(statement) - throw Connection.Error(connection) + throw SQLiteError(connection) } } - /// Finalizes the SQL statement, releasing any associated resources. deinit { sqlite3_finalize(statement) } - - // MARK: - Binding Parameters - - /// Returns the count of parameters that can be bound to this statement. - /// - /// This method provides the number of parameters that can be bound in the SQL statement, - /// allowing you to determine how many parameters need to be set. - /// - /// - Returns: The number of bindable parameters in the statement. - public func bindParameterCount() -> Int32 { +} + +// MARK: - StatementProtocol + +extension Statement: StatementProtocol { + public func parameterCount() -> Int32 { sqlite3_bind_parameter_count(statement) } - /// Returns the index of a parameter by its name. - /// - /// This method is used to find the index of a parameter in the SQL statement given its name. - /// This is useful for binding values to named parameters. - /// - /// - Parameters: - /// - name: The name of the parameter. - /// - Returns: The index of the parameter, or 0 if the parameter does not exist. - public func bind(parameterIndexBy name: String) -> Int32 { + public func parameterIndexBy(_ name: String) -> Int32 { sqlite3_bind_parameter_index(statement, name) } - /// Returns the name of a parameter by its index. - /// - /// This method retrieves the name of a parameter based on its index in the SQL statement. This - /// is useful for debugging or when parameter names are needed. - /// - /// - Parameters: - /// - index: The index of the parameter (1-based). - /// - Returns: The name of the parameter, or `nil` if the name could not be retrieved. - public func bind(parameterNameBy index: Int32) -> String? { - guard let cString = sqlite3_bind_parameter_name(statement, index) else { - return nil - } - return String(cString: cString) + public func parameterNameBy(_ index: Int32) -> String? { + sqlite3_bind_parameter_name(statement, index) } - /// Binds a value to a parameter at a specified index. - /// - /// This method allows you to bind various types of values (integer, real, text, or blob) to a - /// parameter in the SQL statement. The appropriate SQLite function is called based on the type - /// of value being bound. - /// - /// - Parameters: - /// - value: The value to bind to the parameter. - /// - index: The index of the parameter to bind (1-based). - /// - Throws: ``Connection/Error`` if the binding operation fails. - public func bind(_ value: SQLiteRawValue, at index: Int32) throws { - let status: Int32 - switch value { - case .int(let value): status = sqlite3_bind_int64(statement, index, value) - case .real(let value): status = sqlite3_bind_double(statement, index, value) - case .text(let value): status = sqlite3_bind_text(statement, index, value) - case .blob(let value): status = sqlite3_bind_blob(statement, index, value) - case .null: status = sqlite3_bind_null(statement, index) + public func bind(_ value: SQLiteValue, at index: Int32) throws(SQLiteError) { + let status = switch value { + case .int(let value): sqlite3_bind_int64(statement, index, value) + case .real(let value): sqlite3_bind_double(statement, index, value) + case .text(let value): sqlite3_bind_text(statement, index, value) + case .blob(let value): sqlite3_bind_blob(statement, index, value) + case .null: sqlite3_bind_null(statement, index) } if status != SQLITE_OK { - throw Connection.Error(connection) + throw SQLiteError(connection) } } - /// Binds a value conforming to `RawBindable` to a parameter at a specified index. - /// - /// This method provides a generic way to bind values that conform to `RawBindable`, - /// allowing for flexibility in the types of values that can be bound to SQL statements. - /// - /// - Parameters: - /// - value: The value to bind to the parameter. - /// - index: The index of the parameter to bind (1-based). - /// - Throws: ``Connection/Error`` if the binding operation fails. - public func bind(_ value: T?, at index: Int32) throws { - try bind(value?.sqliteRawValue ?? .null, at: index) - } - - /// Binds all values from a `SQLiteRow` to their corresponding named parameters in the statement. - /// - /// This method iterates through each key-value pair in the given `SQLiteRow` and binds the value to - /// the statement’s named parameter using the `:` syntax. Column names from the row must - /// match named parameters defined in the SQL statement. - /// - /// For example, a column named `"userID"` will be bound to a parameter `:userID` in the SQL. - /// - /// - Throws: ``Connection/Error`` if a parameter is missing or if a binding operation fails. - public func bind(_ row: SQLiteRow) throws { - try row.forEach { column, value in - try bind(value, at: bind(parameterIndexBy: ":\(column)")) - } - } - - /// Binds all values from an `Arguments` instance to their corresponding parameters in the statement. - /// - /// This method iterates through each token–value pair in the provided `Arguments` collection and binds - /// the value to the appropriate parameter in the SQL statement. Both indexed (`?NNN`) and named (`:name`) - /// parameters are supported. - /// - /// - Parameter arguments: The `Arguments` instance containing tokens and their associated values. - /// - Throws: ``Connection/Error`` if a parameter is not found or if the binding fails. - public func bind(_ arguments: Arguments) throws { - try arguments.forEach { token, value in - let index = switch token { - case .indexed(let index): - Int32(index) - case .named(let name): - bind(parameterIndexBy: ":\(name)") - } - try bind(value, at: index) - } - } - - /// Clears all parameter bindings from the statement. - /// - /// This method resets any parameter bindings, allowing you to reuse the same SQL statement - /// with different parameter values. This is useful for executing the same statement multiple - /// times with different parameters. - /// - /// - Throws: ``Connection/Error`` if the operation to clear bindings fails. - public func clearBindings() throws { + public func clearBindings() throws(SQLiteError) { if sqlite3_clear_bindings(statement) != SQLITE_OK { - throw Connection.Error(connection) + throw SQLiteError(connection) } } - // MARK: - Retrieving Results + @discardableResult + public func step() throws(SQLiteError) -> Bool { + switch sqlite3_step(statement) { + case SQLITE_ROW: true + case SQLITE_DONE: false + default: throw SQLiteError(connection) + } + } + + public func reset() throws(SQLiteError) { + if sqlite3_reset(statement) != SQLITE_OK { + throw SQLiteError(connection) + } + } - /// Returns the number of columns in the result set. - /// - /// This method provides the count of columns returned by the SQL statement result, which is - /// useful for iterating over query results and processing data. - /// - /// - Returns: The number of columns in the result set. public func columnCount() -> Int32 { sqlite3_column_count(statement) } - /// Returns the type of data stored in a column at a specified index. - /// - /// This method retrieves the type of data stored in a particular column of the result set, - /// allowing you to handle different data types appropriately. - /// - /// - Parameters: - /// - index: The index of the column (0-based). - /// - Returns: The type of data in the column as `SQLiteRawType`. - public func columnType(at index: Int32) -> SQLiteRawType { - .init(rawValue: sqlite3_column_type(statement, index)) ?? .null + public func columnName(at index: Int32) -> String? { + sqlite3_column_name(statement, index) } - /// Returns the name of a column at a specified index. - /// - /// This method retrieves the name of a column, which is useful for debugging or when you need - /// to work with column names directly. - /// - /// - Parameters: - /// - index: The index of the column (0-based). - /// - Returns: The name of the column as a `String`. - public func columnName(at index: Int32) -> String { - String(cString: sqlite3_column_name(statement, index)) - } - - /// Retrieves the value from a column at a specified index. - /// - /// This method extracts the value from a column and returns it as an `SQLiteRawValue`, which - /// can represent different data types like integer, real, text, or blob. - /// - /// - Parameters: - /// - index: The index of the column (0-based). - /// - Returns: The value from the column as `SQLiteRawValue`. - public func columnValue(at index: Int32) -> SQLiteRawValue { - switch columnType(at: index) { - case .int: return .int(sqlite3_column_int64(statement, index)) - case .real: return .real(sqlite3_column_double(statement, index)) - case .text: return .text(sqlite3_column_text(statement, index)) - case .blob: return .blob(sqlite3_column_blob(statement, index)) - case .null: return .null + public func columnValue(at index: Int32) -> SQLiteValue { + switch sqlite3_column_type(statement, index) { + case SQLITE_INTEGER: .int(sqlite3_column_int64(statement, index)) + case SQLITE_FLOAT: .real(sqlite3_column_double(statement, index)) + case SQLITE_TEXT: .text(sqlite3_column_text(statement, index)) + case SQLITE_BLOB: .blob(sqlite3_column_blob(statement, index)) + default: .null } } - - /// Retrieves the value from a column at a specified index and converts it to a value - /// conforming to `SQLiteRawRepresentable`. - /// - /// This method provides a way to convert column values into types that conform to - /// ``SQLiteRawRepresentable``, allowing for easier integration with custom data models. - /// - /// - Parameters: - /// - index: The index of the column (0-based). - /// - Returns: The value from the column converted to `T`, or `nil` if conversion fails. - public func columnValue(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.. Bool { - switch sqlite3_step(statement) { - case SQLITE_ROW: return true - case SQLITE_DONE: return false - default: throw Connection.Error(connection) - } - } - - /// Resets the prepared SQL statement to its initial state. - /// - /// Use this method before re-executing the statement. It does not clear the bound parameters, - /// allowing their values to persist between executions. To clear the parameters, use the - /// `clearBindings()` method. - /// - /// - Throws: ``Connection/Error`` if the statement reset fails. - public func reset() throws { - if sqlite3_reset(statement) != SQLITE_OK { - throw Connection.Error(connection) - } - } - - /// Executes the statement once for each row, returning the collected result rows if any. - /// - /// This method binds each row’s named values to the statement parameters and executes the - /// statement. After each execution, any resulting rows are collected and returned. If the `rows` - /// array is empty, the statement will still execute once with no parameters bound. - /// - /// Use this method for queries such as `INSERT` or `UPDATE` statements with changing - /// parameter values. - /// - /// - Note: If `rows` is empty, the statement executes once with no bound values. - /// - /// - Parameter rows: A list of `SQLiteRow` values to bind to the statement. - /// - Returns: An array of result rows collected from all executions of the statement. - /// - Throws: ``Connection/Error`` if binding or execution fails. - @discardableResult - public func execute(rows: [SQLiteRow]) throws -> [SQLiteRow] { - var result = [SQLiteRow]() - var index = 0 - - repeat { - if rows.count > index { - try bind(rows[index]) - } - while try step() { - result.append(currentRow()) - } - try clearBindings() - try reset() - index += 1 - } while index < rows.count - - return result - } - - /// Executes the statement once for each arguments set, returning any resulting rows. - /// - /// This method binds each `Arguments` set (indexed or named) to the statement and executes it. All - /// result rows from each execution are collected and returned. If no arguments are provided, the - /// statement executes once with no values bound. - /// - /// Use this method for queries such as `SELECT`, `INSERT`, `UPDATE`, or `DELETE` where results - /// may be expected and multiple executions are needed. - /// - /// ```swift - /// let stmt = try connection.prepare( - /// sql: "SELECT * FROM logs WHERE level = :level" - /// ) - /// let result = try stmt.execute(args: [ - /// ["level": "info"], - /// ["level": "error"] - /// ]) - /// ``` - /// - /// - Note: If `args` is `nil` or empty, the statement executes once with no bound values. - /// - /// - Parameter args: A list of `Arguments` to bind and execute. Defaults to `nil`. - /// - Returns: A flat array of result rows produced by all executions. - /// - Throws: ``Connection/Error`` if binding or execution fails. - @discardableResult - public func execute(args: [Arguments]? = nil) throws -> [SQLiteRow] { - var result = [SQLiteRow]() - var index = 0 - - repeat { - if let args, args.count > index { - try bind(args[index]) - } - while try step() { - result.append(currentRow()) - } - try clearBindings() - try reset() - index += 1 - } while index < args?.count ?? 0 - - return result - } - - // MARK: - Equatable - - /// Compares two `Statement` instances for equality. - /// - /// This method checks whether two `Statement` instances are equal by comparing their - /// underlying SQLite statement pointers and connection pointers. - /// - /// - Parameters: - /// - lhs: The first `Statement` instance. - /// - rhs: The second `Statement` instance. - /// - Returns: `true` if the two instances are equal, `false` otherwise. - public static func == (lhs: Statement, rhs: Statement) -> Bool { - lhs.statement == rhs.statement && lhs.connection == rhs.connection - } - - // MARK: - Hashable - - /// Computes a hash value for the `Statement` instance. - /// - /// This method computes a hash value based on the SQLite statement pointer and connection - /// pointer. It is used to support hash-based collections like sets and dictionaries. - /// - /// - Parameter hasher: The hasher to use for computing the hash value. - public func hash(into hasher: inout Hasher) { - hasher.combine(statement) - hasher.combine(connection) - } } -// MARK: - Functions +// MARK: - Constants + +let SQLITE_STATIC = unsafeBitCast( + OpaquePointer(bitPattern: 0), + to: sqlite3_destructor_type.self +) + +let SQLITE_TRANSIENT = unsafeBitCast( + OpaquePointer(bitPattern: -1), + to: sqlite3_destructor_type.self +) + +// MARK: - Private Sunctions + +private func sqlite3_bind_parameter_name(_ stmt: OpaquePointer!, _ index: Int32) -> String? { + guard let cString = DataLiteC.sqlite3_bind_parameter_name(stmt, index) else { return nil } + return String(cString: cString) +} private func sqlite3_bind_text(_ stmt: OpaquePointer!, _ index: Int32, _ string: String) -> Int32 { sqlite3_bind_text(stmt, index, string, -1, SQLITE_TRANSIENT) @@ -693,6 +145,13 @@ private func sqlite3_bind_blob(_ stmt: OpaquePointer!, _ index: Int32, _ data: D } } +private func sqlite3_column_name(_ stmt: OpaquePointer!, _ iCol: Int32) -> String? { + guard let cString = DataLiteC.sqlite3_column_name(stmt, iCol) else { + return nil + } + return String(cString: cString) +} + private func sqlite3_column_text(_ stmt: OpaquePointer!, _ iCol: Int32) -> String { String(cString: DataLiteC.sqlite3_column_text(stmt, iCol)) } diff --git a/Sources/DataLiteCore/Docs.docc/Articles/CustomFunctions.md b/Sources/DataLiteCore/Docs.docc/Articles/CustomFunctions.md new file mode 100644 index 0000000..1651a66 --- /dev/null +++ b/Sources/DataLiteCore/Docs.docc/Articles/CustomFunctions.md @@ -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 +function’s 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 function’s 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. It’s 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(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) diff --git a/Sources/DataLiteCore/Docs.docc/Articles/DatabaseEncryption.md b/Sources/DataLiteCore/Docs.docc/Articles/DatabaseEncryption.md new file mode 100644 index 0000000..f9bf5fe --- /dev/null +++ b/Sources/DataLiteCore/Docs.docc/Articles/DatabaseEncryption.md @@ -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 SQLCipher’s 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/) diff --git a/Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md b/Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md new file mode 100644 index 0000000..3deded9 --- /dev/null +++ b/Sources/DataLiteCore/Docs.docc/Articles/ErrorHandling.md @@ -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 SQLite’s diagnostics only. + +## Custom Functions + +Errors thrown from ``Function/Scalar`` or ``Function/Aggregate`` implementations are reported back +to SQLite as `SQLITE_ERROR`, with the error’s `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) diff --git a/Sources/DataLiteCore/Docs.docc/Articles/Multithreading.md b/Sources/DataLiteCore/Docs.docc/Articles/Multithreading.md new file mode 100644 index 0000000..b03236c --- /dev/null +++ b/Sources/DataLiteCore/Docs.docc/Articles/Multithreading.md @@ -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 don’t 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 SQLite’s synchronous API. + +## Synchronization Options + +- ``Connection/Options/nomutex`` — disables SQLite’s 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 SQLite’s +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 SQLite’s internal thread from being blocked by +slow I/O or external operations. diff --git a/Sources/DataLiteCore/Docs.docc/Articles/PreparedStatements.md b/Sources/DataLiteCore/Docs.docc/Articles/PreparedStatements.md new file mode 100644 index 0000000..2412ed9 --- /dev/null +++ b/Sources/DataLiteCore/Docs.docc/Articles/PreparedStatements.md @@ -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 SQLite’s 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 Swift’s 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, it’s 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) diff --git a/Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md b/Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md new file mode 100644 index 0000000..11cd858 --- /dev/null +++ b/Sources/DataLiteCore/Docs.docc/Articles/SQLScripts.md @@ -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 statement’s 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:)`` diff --git a/Sources/DataLiteCore/Docs.docc/Articles/SQLiteRows.md b/Sources/DataLiteCore/Docs.docc/Articles/SQLiteRows.md new file mode 100644 index 0000000..f0f3068 --- /dev/null +++ b/Sources/DataLiteCore/Docs.docc/Articles/SQLiteRows.md @@ -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 row’s 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`` diff --git a/Sources/DataLiteCore/Docs.docc/Articles/WorkingWithConnections.md b/Sources/DataLiteCore/Docs.docc/Articles/WorkingWithConnections.md new file mode 100644 index 0000000..2d84a01 --- /dev/null +++ b/Sources/DataLiteCore/Docs.docc/Articles/WorkingWithConnections.md @@ -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 = ` +statement, while `get` issues `PRAGMA `. + +```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. diff --git a/Sources/DataLiteCore/Docs.docc/Connection/Connection+InitLocation.md b/Sources/DataLiteCore/Docs.docc/Connection/Connection+InitLocation.md deleted file mode 100644 index 49a9452..0000000 --- a/Sources/DataLiteCore/Docs.docc/Connection/Connection+InitLocation.md +++ /dev/null @@ -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. diff --git a/Sources/DataLiteCore/Docs.docc/Connection/Connection+InitPath.md b/Sources/DataLiteCore/Docs.docc/Connection/Connection+InitPath.md deleted file mode 100644 index c1345f2..0000000 --- a/Sources/DataLiteCore/Docs.docc/Connection/Connection+InitPath.md +++ /dev/null @@ -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. diff --git a/Sources/DataLiteCore/Docs.docc/Connection/Connection.md b/Sources/DataLiteCore/Docs.docc/Connection/Connection.md deleted file mode 100644 index af55e35..0000000 --- a/Sources/DataLiteCore/Docs.docc/Connection/Connection.md +++ /dev/null @@ -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:)`` diff --git a/Sources/DataLiteCore/Docs.docc/DataLiteCore.md b/Sources/DataLiteCore/Docs.docc/DataLiteCore.md index 8396735..50a453d 100644 --- a/Sources/DataLiteCore/Docs.docc/DataLiteCore.md +++ b/Sources/DataLiteCore/Docs.docc/DataLiteCore.md @@ -2,6 +2,19 @@ **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 + +- +- +- +- +- +- +- +- diff --git a/Sources/DataLiteCore/Enums/JournalMode.swift b/Sources/DataLiteCore/Enums/JournalMode.swift index 09490eb..350166a 100644 --- a/Sources/DataLiteCore/Enums/JournalMode.swift +++ b/Sources/DataLiteCore/Enums/JournalMode.swift @@ -2,33 +2,36 @@ import Foundation /// Represents the journal modes available for an SQLite database. /// -/// The journal mode determines how the database handles transactions and how it -/// maintains the journal for rollback and recovery. For more details, refer to -/// [Journal Mode Pragma](https://www.sqlite.org/pragma.html#pragma_journal_mode). -public enum JournalMode: String, SQLiteRawRepresentable { +/// The journal mode determines how the database handles transactions and how it maintains the +/// journal for rollback and recovery. +/// +/// - SeeAlso: [journal_mode](https://sqlite.org/pragma.html#pragma_journal_mode) +public enum JournalMode: String, SQLiteRepresentable { /// DELETE journal mode. /// - /// This is the default behavior. The rollback journal is deleted at the conclusion - /// of each transaction. The delete operation itself causes the transaction to commit. - /// For more details, refer to the - /// [Atomic Commit In SQLite](https://www.sqlite.org/atomiccommit.html). + /// This is the default behavior. The rollback journal is deleted at the conclusion of each + /// transaction. The delete operation itself causes the transaction to commit. + /// + /// - SeeAlso: [Atomic Commit In SQLite](https://sqlite.org/atomiccommit.html) case delete /// TRUNCATE journal mode. /// - /// In this mode, the rollback journal is truncated to zero length at the end of each transaction - /// instead of being deleted. On many systems, truncating a file is much faster than deleting it - /// because truncating does not require modifying the containing directory. + /// In this mode, the rollback journal is truncated to zero length at the end of each + /// transaction instead of being deleted. On many systems, truncating a file is much faster than + /// deleting it because truncating does not require modifying the containing directory. case truncate /// PERSIST journal mode. /// /// In this mode, the rollback journal is not deleted at the end of each transaction. Instead, - /// the header of the journal is overwritten with zeros. This prevents other database connections - /// from rolling the journal back. The PERSIST mode is useful as an optimization on platforms - /// where deleting or truncating a file is more expensive than overwriting the first block of a file - /// with zeros. For additional configuration, refer to - /// [journal_size_limit](https://www.sqlite.org/pragma.html#pragma_journal_size_limit). + /// the header of the journal is overwritten with zeros. This prevents other database + /// connections from rolling the journal back. The PERSIST mode is useful as an optimization on + /// platforms where deleting or truncating a file is more expensive than overwriting the first + /// block of a file with zeros. + /// + /// - SeeAlso: [journal_size_limit]( + /// https://sqlite.org/pragma.html#pragma_journal_size_limit) case persist /// MEMORY journal mode. @@ -42,41 +45,63 @@ public enum JournalMode: String, SQLiteRawRepresentable { /// /// This mode uses a write-ahead log instead of a rollback journal to implement transactions. /// The WAL mode is persistent, meaning it stays in effect across multiple database connections - /// and persists even after closing and reopening the database. For more details, refer to the - /// [Write-Ahead Logging](https://www.sqlite.org/wal.html). + /// and persists even after closing and reopening the database. + /// + /// - SeeAlso: [Write-Ahead Logging](https://sqlite.org/wal.html) case wal /// OFF journal mode. /// - /// In this mode, the rollback journal is completely disabled, meaning no rollback journal is ever created. - /// This disables SQLite's atomic commit and rollback capabilities. The `ROLLBACK` command will no longer work - /// and behaves in an undefined way. Applications must avoid using the `ROLLBACK` command when the journal mode is OFF. - /// If the application crashes in the middle of a transaction, the database file will likely become corrupt, - /// as there is no way to unwind partially completed operations. For example, if a duplicate entry causes a - /// `CREATE UNIQUE INDEX` statement to fail halfway through, it will leave behind a partially created index, - /// resulting in a corrupted database state. + /// In this mode, the rollback journal is completely disabled, meaning no rollback journal is + /// ever created. This disables SQLite's atomic commit and rollback capabilities. The `ROLLBACK` + /// command will no longer work and behaves in an undefined way. Applications must avoid using + /// the `ROLLBACK` command when the journal mode is OFF. If the application crashes in the + /// middle of a transaction, the database file will likely become corrupt, as there is no way to + /// unwind partially completed operations. For example, if a duplicate entry causes a + /// `CREATE UNIQUE INDEX` statement to fail halfway through, it will leave behind a partially + /// created index, resulting in a corrupted database state. case off + /// 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 { switch self { - case .delete: "DELETE" - case .truncate: "TRUNCATE" - case .persist: "PERSIST" - case .memory: "MEMORY" - case .wal: "WAL" - case .off: "OFF" + case .delete: "DELETE" + case .truncate: "TRUNCATE" + case .persist: "PERSIST" + case .memory: "MEMORY" + case .wal: "WAL" + case .off: "OFF" } } + /// 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) { switch rawValue.uppercased() { - case "DELETE": self = .delete - case "TRUNCATE": self = .truncate - case "PERSIST": self = .persist - case "MEMORY": self = .memory - case "WAL": self = .wal - case "OFF": self = .off - default: return nil + case "DELETE": self = .delete + case "TRUNCATE": self = .truncate + case "PERSIST": self = .persist + case "MEMORY": self = .memory + case "WAL": self = .wal + case "OFF": self = .off + default: return nil } } } diff --git a/Sources/DataLiteCore/Enums/SQLiteAction.swift b/Sources/DataLiteCore/Enums/SQLiteAction.swift index 645321d..9d3f87b 100644 --- a/Sources/DataLiteCore/Enums/SQLiteAction.swift +++ b/Sources/DataLiteCore/Enums/SQLiteAction.swift @@ -1,40 +1,34 @@ 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 -/// performed on a database, such as insertion, updating, or deletion. +/// The `SQLiteAction` enumeration describes an action that modifies a database table. It +/// 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 { - /// Indicates the insertion of a new row into a table. - /// - /// This case is used to represent the action of adding a new - /// row to a specific table in a database. + /// A new row was inserted into a table. /// /// - Parameters: - /// - db: The name of the database where the insertion occurred. - /// - table: The name of the table where the insertion occurred. - /// - rowID: The row ID of the newly inserted row. + /// - db: The name of the database 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. case insert(db: String, table: String, rowID: Int64) - /// Indicates the modification of an existing row in a table. - /// - /// This case is used to represent the action of updating an - /// existing row within a specific table in a database. + /// An existing row was modified in a table. /// /// - Parameters: - /// - db: The name of the database where the update occurred. - /// - table: The name of the table where the update occurred. - /// - rowID: The row ID of the updated row. + /// - db: The name of the database where the update occurred. + /// - table: The name of the table containing the updated row. + /// - rowID: The row ID of the modified row. case update(db: String, table: String, rowID: Int64) - /// Indicates the removal of a row from a table. - /// - /// This case is used to represent the action of deleting a - /// row from a specific table in a database. + /// A row was deleted from a table. /// /// - Parameters: - /// - db: The name of the database from which the row was deleted. - /// - table: The name of the table from which the row was deleted. - /// - rowID: The row ID of the deleted row. + /// - db: The name of the database where the deletion occurred. + /// - table: The name of the table from which the row was deleted. + /// - rowID: The row ID of the deleted row. case delete(db: String, table: String, rowID: Int64) } diff --git a/Sources/DataLiteCore/Enums/SQLiteRawType.swift b/Sources/DataLiteCore/Enums/SQLiteRawType.swift deleted file mode 100644 index 2d34d6c..0000000 --- a/Sources/DataLiteCore/Enums/SQLiteRawType.swift +++ /dev/null @@ -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 - } - } -} diff --git a/Sources/DataLiteCore/Enums/SQLiteRawValue.swift b/Sources/DataLiteCore/Enums/SQLiteRawValue.swift deleted file mode 100644 index 2d190c5..0000000 --- a/Sources/DataLiteCore/Enums/SQLiteRawValue.swift +++ /dev/null @@ -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() - } -} diff --git a/Sources/DataLiteCore/Enums/SQLiteValue.swift b/Sources/DataLiteCore/Enums/SQLiteValue.swift new file mode 100644 index 0000000..8c5dc62 --- /dev/null +++ b/Sources/DataLiteCore/Enums/SQLiteValue.swift @@ -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() + } +} diff --git a/Sources/DataLiteCore/Enums/Synchronous.swift b/Sources/DataLiteCore/Enums/Synchronous.swift index f55df0e..2e21a92 100644 --- a/Sources/DataLiteCore/Enums/Synchronous.swift +++ b/Sources/DataLiteCore/Enums/Synchronous.swift @@ -1,45 +1,41 @@ 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. -/// For more details, refer to [Synchronous Pragma](https://www.sqlite.org/pragma.html#pragma_synchronous). -public enum Synchronous: UInt8, SQLiteRawRepresentable { - /// Synchronous mode off. Disables synchronization for maximum performance. +/// The synchronous mode controls how thoroughly SQLite ensures that data is physically written to +/// disk. It defines the balance between durability, consistency, and performance during commits. +/// +/// - 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 - /// to the operating system. If the application running SQLite crashes, the data will be safe, - /// but the database might become corrupted if the operating system crashes or the computer loses - /// power before the data is written to the disk surface. On the other hand, commits can be orders - /// of magnitude faster with synchronous OFF. + /// With `synchronous=OFF`, SQLite does not wait for data to reach non-volatile storage before + /// continuing. The database may become inconsistent if the operating system crashes or power is + /// lost, although application-level crashes do not cause corruption. + /// Best suited for temporary databases or rebuildable data. case off = 0 - /// Normal synchronous mode. + /// Enables normal synchronization. /// - /// The SQLite database engine syncs at the most critical moments, but less frequently - /// than in FULL mode. While there is a very small chance of corruption in - /// `journal_mode=DELETE` on older filesystems during a power failure, WAL - /// mode is safe from corruption with synchronous=NORMAL. Modern filesystems - /// likely make DELETE mode safe too. However, WAL mode in synchronous=NORMAL - /// loses some durability, as a transaction committed in WAL mode might roll back - /// after a power loss or system crash. Transactions are still durable across application - /// crashes regardless of the synchronous setting or journal mode. This setting is a - /// good choice for most applications running in WAL mode. + /// SQLite performs syncs only at critical points. In WAL mode, this guarantees consistency but + /// not full durability: the most recent transactions might be lost after a power failure. In + /// rollback journal mode, there is a very small chance of corruption on older filesystems. + /// Recommended for most use cases where performance is preferred over strict durability. case normal = 1 - /// Full synchronous mode. + /// Enables full synchronization. /// - /// Uses the xSync method of the VFS to ensure that all content is safely written - /// to the disk surface prior to continuing. This ensures that an operating system - /// crash or power failure will not corrupt the database. FULL synchronous is very - /// safe but also slower. It is the most commonly used synchronous setting when - /// not in WAL mode. + /// SQLite calls the VFS `xSync` method to ensure that all data is written to disk before + /// continuing. Prevents corruption even after a system crash or power loss. Default mode for + /// rollback journals and fully ACID-compliant in WAL mode. Provides strong consistency and + /// isolation; durability may depend on filesystem behavior. case full = 2 - /// Extra synchronous mode. + /// Enables extra synchronization for maximum durability. /// - /// Similar to FULL mode, but ensures the directory containing the rollback journal - /// is synced after the journal is unlinked, providing additional durability in case of - /// power loss shortly after a commit. + /// Extends `FULL` by also syncing the directory that contained the rollback journal after it + /// is removed, ensuring durability even if power is lost immediately after a commit. Guarantees + /// full ACID compliance in both rollback and WAL modes. Recommended for systems where + /// durability is more important than performance. case extra = 3 } diff --git a/Sources/DataLiteCore/Enums/TransactionType.swift b/Sources/DataLiteCore/Enums/TransactionType.swift index 092f86c..1579d6d 100644 --- a/Sources/DataLiteCore/Enums/TransactionType.swift +++ b/Sources/DataLiteCore/Enums/TransactionType.swift @@ -1,39 +1,35 @@ 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 -/// during a transaction. The default transaction behavior is DEFERRED. For more detailed information -/// about SQLite transactions, refer to the [SQLite documentation](https://www.sqlite.org/lang_transaction.html). +/// A transaction defines how the database manages concurrency and locking. The transaction type +/// determines when a write lock is acquired and how other connections can access the database +/// during the transaction. +/// +/// - SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html) 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, - /// the `BEGIN DEFERRED` statement merely sets a flag on the database connection to prevent - /// the automatic commit that normally occurs when the last statement finishes. If the first - /// statement after `BEGIN DEFERRED` is a `SELECT`, a read transaction begins. If it is a write - /// statement, a write transaction starts. Subsequent write operations may upgrade the transaction - /// to a write transaction if possible, or return `SQLITE_BUSY`. The transaction persists until - /// an explicit `COMMIT` or `ROLLBACK` or until a rollback is provoked by an error or an `ON CONFLICT ROLLBACK` clause. + /// With `BEGIN DEFERRED`, no locks are acquired immediately. If the first statement is a read + /// (`SELECT`), a read transaction begins. If it is a write statement, a write transaction + /// begins instead. Deferred transactions allow greater concurrency and are the default mode. case deferred = "DEFERRED" - /// An immediate transaction. + /// Starts a write transaction immediately. /// - /// An immediate transaction starts a new write immediately, without waiting for the first - /// write statement. The `BEGIN IMMEDIATE` statement may fail with `SQLITE_BUSY` if another - /// write transaction is active on a different database connection. + /// With `BEGIN IMMEDIATE`, a reserved lock is acquired right away to ensure that no other + /// connection can start a conflicting write. The statement may fail with `SQLITE_BUSY` if + /// another write transaction is already active. case immediate = "IMMEDIATE" - /// An exclusive transaction. + /// Starts an exclusive write transaction. /// - /// Similar to `IMMEDIATE`, an exclusive transaction starts a write immediately. However, - /// in non-WAL modes, `EXCLUSIVE` prevents other database connections from reading the database - /// while the transaction is in progress. In WAL mode, `EXCLUSIVE` behaves the same as `IMMEDIATE`. + /// With `BEGIN EXCLUSIVE`, a write lock is acquired immediately. In rollback journal mode, it + /// also prevents other connections from reading the database while the transaction is active. + /// In WAL mode, it behaves the same as `.immediate`. case exclusive = "EXCLUSIVE" /// A textual representation of the transaction type. - /// - /// Returns the raw value of the transaction type (e.g., "DEFERRED", "IMMEDIATE", "EXCLUSIVE"). public var description: String { rawValue } diff --git a/Sources/DataLiteCore/Extensions/BinaryFloatingPoint.swift b/Sources/DataLiteCore/Extensions/BinaryFloatingPoint.swift index 977ce6c..9f38bb9 100644 --- a/Sources/DataLiteCore/Extensions/BinaryFloatingPoint.swift +++ b/Sources/DataLiteCore/Extensions/BinaryFloatingPoint.swift @@ -1,25 +1,27 @@ import Foundation -public extension SQLiteRawBindable where Self: BinaryFloatingPoint { - /// Provides the `SQLiteRawValue` representation for floating-point types. +public extension SQLiteBindable where Self: BinaryFloatingPoint { + /// 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. - var sqliteRawValue: SQLiteRawValue { + /// - Returns: An ``SQLiteValue`` of type `.real` containing the numeric value. + var sqliteValue: SQLiteValue { .real(.init(self)) } } -public extension SQLiteRawRepresentable where Self: BinaryFloatingPoint { - /// Initializes an instance of the conforming type from an `SQLiteRawValue`. +public extension SQLiteRepresentable where Self: BinaryFloatingPoint { + /// Creates a floating-point value from an SQLite representation. /// - /// This initializer handles `SQLiteRawValue` of type `.real`, converting it to the floating-point value. - /// It also handles `SQLiteRawValue` of type `.int`, converting it to the floating-point value. + /// This initializer supports both ``SQLiteValue/real(_:)`` and ``SQLiteValue/int(_:)`` cases, + /// converting the stored number to the corresponding floating-point type. /// - /// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. - init?(_ sqliteRawValue: SQLiteRawValue) { - switch sqliteRawValue { + /// - Parameter value: The SQLite value to convert from. + /// - Returns: A new instance if the conversion succeeds, or `nil` if the value is incompatible. + init?(_ value: SQLiteValue) { + switch value { case .int(let value): self.init(Double(value)) case .real(let value): @@ -30,5 +32,5 @@ public extension SQLiteRawRepresentable where Self: BinaryFloatingPoint { } } -extension Float: SQLiteRawRepresentable {} -extension Double: SQLiteRawRepresentable {} +extension Float: SQLiteRepresentable {} +extension Double: SQLiteRepresentable {} diff --git a/Sources/DataLiteCore/Extensions/BinaryInteger.swift b/Sources/DataLiteCore/Extensions/BinaryInteger.swift index 2362cee..1bccbcf 100644 --- a/Sources/DataLiteCore/Extensions/BinaryInteger.swift +++ b/Sources/DataLiteCore/Extensions/BinaryInteger.swift @@ -1,27 +1,28 @@ import Foundation -public extension SQLiteRawBindable where Self: BinaryInteger { - /// Provides the `SQLiteRawValue` representation for integer types. +public extension SQLiteBindable where Self: BinaryInteger { + /// 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. - var sqliteRawValue: SQLiteRawValue { + /// - Returns: An ``SQLiteValue`` of type `.int` containing the integer value. + var sqliteValue: SQLiteValue { .int(Int64(self)) } } -public extension SQLiteRawRepresentable where Self: BinaryInteger { - /// Initializes an instance of the conforming type from an `SQLiteRawValue`. +public extension SQLiteRepresentable where Self: BinaryInteger { + /// Creates an integer value from an SQLite representation. /// - /// This initializer handles `SQLiteRawValue` of type `.int`, converting it to the integer value. - /// It uses the `init(exactly:)` initializer to ensure that the value fits within the range of the - /// integer type. If the value cannot be exactly represented by the integer type, the initializer - /// will return `nil`. + /// This initializer supports the ``SQLiteValue/int(_:)`` case and uses `init(exactly:)` to + /// ensure that the value fits within the bounds of the integer type. If the value cannot be + /// exactly represented, the initializer returns `nil`. /// - /// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. - init?(_ sqliteRawValue: SQLiteRawValue) { - switch sqliteRawValue { + /// - Parameter value: The SQLite value to convert from. + /// - Returns: A new instance if the conversion succeeds, or `nil` otherwise. + init?(_ value: SQLiteValue) { + switch value { case .int(let value): self.init(exactly: value) default: @@ -30,14 +31,14 @@ public extension SQLiteRawRepresentable where Self: BinaryInteger { } } -extension Int: SQLiteRawRepresentable {} -extension Int8: SQLiteRawRepresentable {} -extension Int16: SQLiteRawRepresentable {} -extension Int32: SQLiteRawRepresentable {} -extension Int64: SQLiteRawRepresentable {} +extension Int: SQLiteRepresentable {} +extension Int8: SQLiteRepresentable {} +extension Int16: SQLiteRepresentable {} +extension Int32: SQLiteRepresentable {} +extension Int64: SQLiteRepresentable {} -extension UInt: SQLiteRawRepresentable {} -extension UInt8: SQLiteRawRepresentable {} -extension UInt16: SQLiteRawRepresentable {} -extension UInt32: SQLiteRawRepresentable {} -extension UInt64: SQLiteRawRepresentable {} +extension UInt: SQLiteRepresentable {} +extension UInt8: SQLiteRepresentable {} +extension UInt16: SQLiteRepresentable {} +extension UInt32: SQLiteRepresentable {} +extension UInt64: SQLiteRepresentable {} diff --git a/Sources/DataLiteCore/Extensions/Bool.swift b/Sources/DataLiteCore/Extensions/Bool.swift index c0aaf2a..b60cf61 100644 --- a/Sources/DataLiteCore/Extensions/Bool.swift +++ b/Sources/DataLiteCore/Extensions/Bool.swift @@ -1,28 +1,27 @@ import Foundation -extension Bool: SQLiteRawRepresentable { - /// Provides the `SQLiteRawValue` representation for boolean types. +extension Bool: SQLiteRepresentable { + /// Converts a Boolean value to its SQLite representation. /// - /// This implementation converts the boolean value to an `SQLiteRawValue` of type `.int`. - /// - `true` is represented as `1`. - /// - `false` is represented as `0`. + /// Boolean values are stored in SQLite as integers (`INTEGER` type). The value `true` is + /// represented as `1`, and `false` as `0`. /// - /// - Returns: An `SQLiteRawValue` of type `.int`, containing `1` for `true` and `0` for `false`. - public var sqliteRawValue: SQLiteRawValue { + /// - Returns: An ``SQLiteValue`` of type `.int`, containing `1` for `true` + /// and `0` for `false`. + public var sqliteValue: SQLiteValue { .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. - /// - `1` is converted to `true`. - /// - `0` is converted to `false`. + /// This initializer supports the ``SQLiteValue/int(_:)`` case and converts the integer value to + /// a Boolean. `1` is interpreted as `true`, `0` as `false`. If the integer is not `0` or `1`, + /// the initializer returns `nil`. /// - /// If the integer value is not `0` or `1`, the initializer returns `nil`. - /// - /// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. - public init?(_ sqliteRawValue: SQLiteRawValue) { - switch sqliteRawValue { + /// - Parameter value: The SQLite value to convert from. + /// - Returns: A Boolean value if the conversion succeeds, or `nil` otherwise. + public init?(_ value: SQLiteValue) { + switch value { case .int(let value) where value == 0 || value == 1: self = value == 1 default: diff --git a/Sources/DataLiteCore/Extensions/Data.swift b/Sources/DataLiteCore/Extensions/Data.swift index 8494415..8429aef 100644 --- a/Sources/DataLiteCore/Extensions/Data.swift +++ b/Sources/DataLiteCore/Extensions/Data.swift @@ -1,22 +1,25 @@ import Foundation -extension Data: SQLiteRawRepresentable { - /// Provides the `SQLiteRawValue` representation for `Data` types. +extension Data: SQLiteRepresentable { + /// 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. - public var sqliteRawValue: SQLiteRawValue { + /// - Returns: An ``SQLiteValue`` of type `.blob` containing the binary data. + public var sqliteValue: SQLiteValue { .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. - public init?(_ sqliteRawValue: SQLiteRawValue) { - switch sqliteRawValue { + /// - Parameter value: The SQLite value to convert from. + /// - Returns: A `Data` instance if the conversion succeeds, or `nil` otherwise. + public init?(_ value: SQLiteValue) { + switch value { case .blob(let data): self = data default: diff --git a/Sources/DataLiteCore/Extensions/Date.swift b/Sources/DataLiteCore/Extensions/Date.swift index f3700d0..469e05c 100644 --- a/Sources/DataLiteCore/Extensions/Date.swift +++ b/Sources/DataLiteCore/Extensions/Date.swift @@ -1,37 +1,38 @@ import Foundation -extension Date: SQLiteRawRepresentable { - /// Provides the `SQLiteRawValue` representation for `Date` types. +extension Date: SQLiteRepresentable { + /// Converts a `Date` value to its SQLite representation. /// - /// This implementation converts the `Date` value to an `SQLiteRawValue` of type `.text`. - /// The date is formatted as an ISO 8601 string. + /// Dates are stored in SQLite as text using the ISO 8601 format. This property converts the + /// 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. - public var sqliteRawValue: SQLiteRawValue { + /// - Returns: An ``SQLiteValue`` of type `.text`, containing the ISO 8601 string. + public var sqliteValue: SQLiteValue { let formatter = ISO8601DateFormatter() let dateString = formatter.string(from: self) 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. - /// It also supports `.int` and `.real` types representing time intervals since 1970. + /// This initializer supports the following ``SQLiteValue`` cases: + /// - ``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. - public init?(_ sqliteRawValue: SQLiteRawValue) { - switch sqliteRawValue { + /// - Parameter value: The SQLite value to convert from. + /// - Returns: A `Date` instance if the conversion succeeds, or `nil` otherwise. + public init?(_ value: SQLiteValue) { + switch value { case .int(let value): self.init(timeIntervalSince1970: TimeInterval(value)) case .real(let value): self.init(timeIntervalSince1970: value) case .text(let value): let formatter = ISO8601DateFormatter() - if let date = formatter.date(from: value) { - self = date - } else { - return nil - } + guard let date = formatter.date(from: value) else { return nil } + self = date default: return nil } diff --git a/Sources/DataLiteCore/Extensions/RawRepresentable.swift b/Sources/DataLiteCore/Extensions/RawRepresentable.swift index ecdfbbc..f91af97 100644 --- a/Sources/DataLiteCore/Extensions/RawRepresentable.swift +++ b/Sources/DataLiteCore/Extensions/RawRepresentable.swift @@ -1,28 +1,28 @@ import Foundation -public extension SQLiteRawBindable where Self: RawRepresentable, RawValue: SQLiteRawBindable { - /// Provides the `SQLiteRawValue` representation for `RawRepresentable` types. +public extension SQLiteBindable where Self: RawRepresentable, RawValue: SQLiteBindable { + /// Converts a `RawRepresentable` value to its SQLite representation. /// - /// This implementation converts the `RawRepresentable` type's `rawValue` to its corresponding - /// `SQLiteRawValue` representation. The `rawValue` itself must conform to `SQLiteRawBindable`. + /// The `rawValue` of the conforming type must itself conform to ``SQLiteBindable``. This + /// property delegates the conversion to the underlying ``rawValue``. /// - /// - Returns: An `SQLiteRawValue` representation of the `RawRepresentable` type. - var sqliteRawValue: SQLiteRawValue { - rawValue.sqliteRawValue + /// - Returns: The ``SQLiteValue`` representation of the underlying ``rawValue``. + var sqliteValue: SQLiteValue { + rawValue.sqliteValue } } -public extension SQLiteRawRepresentable where Self: RawRepresentable, RawValue: SQLiteRawRepresentable { - /// Initializes an instance of the conforming type from an `SQLiteRawValue`. +public extension SQLiteRepresentable where Self: RawRepresentable, RawValue: SQLiteRepresentable { + /// Creates a `RawRepresentable` value from an SQLite representation. /// - /// This initializer converts the `SQLiteRawValue` to the `RawRepresentable` type's `rawValue`. - /// It first attempts to create a `RawValue` from the `SQLiteRawValue`, then uses that to initialize - /// the `RawRepresentable` instance. If the `SQLiteRawValue` cannot be converted to the `RawValue`, the - /// initializer returns `nil`. + /// This initializer first attempts to create the underlying ``RawValue`` from the provided + /// ``SQLiteValue``. If successful, it uses that raw value to initialize the `RawRepresentable` + /// type. If the conversion fails, the initializer returns `nil`. /// - /// - Parameter sqliteRawValue: The raw SQLite value used to initialize the instance. - init?(_ sqliteRawValue: SQLiteRawValue) { - if let value = RawValue(sqliteRawValue) { + /// - Parameter value: The SQLite value to convert from. + /// - Returns: A new instance if the conversion succeeds, or `nil` otherwise. + init?(_ value: SQLiteValue) { + if let value = RawValue(value) { self.init(rawValue: value) } else { return nil diff --git a/Sources/DataLiteCore/Extensions/String.swift b/Sources/DataLiteCore/Extensions/String.swift index c911184..1d07f14 100644 --- a/Sources/DataLiteCore/Extensions/String.swift +++ b/Sources/DataLiteCore/Extensions/String.swift @@ -1,22 +1,25 @@ import Foundation -extension String: SQLiteRawRepresentable { - /// Provides the `SQLiteRawValue` representation for `String` type. +extension String: SQLiteRepresentable { + /// 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. - public var sqliteRawValue: SQLiteRawValue { + /// - Returns: An ``SQLiteValue`` of type `.text` containing the string value. + public var sqliteValue: SQLiteValue { .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. - public init?(_ sqliteRawValue: SQLiteRawValue) { - switch sqliteRawValue { + /// - Parameter value: The SQLite value to convert from. + /// - Returns: A `String` instance if the conversion succeeds, or `nil` otherwise. + public init?(_ value: SQLiteValue) { + switch value { case .text(let value): self = value default: diff --git a/Sources/DataLiteCore/Extensions/UUID.swift b/Sources/DataLiteCore/Extensions/UUID.swift index 6ae6710..a316bc8 100644 --- a/Sources/DataLiteCore/Extensions/UUID.swift +++ b/Sources/DataLiteCore/Extensions/UUID.swift @@ -1,22 +1,26 @@ import Foundation -extension UUID: SQLiteRawRepresentable { - /// Provides the `SQLiteRawValue` representation for `UUID`. +extension UUID: SQLiteRepresentable { + /// 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. - public var sqliteRawValue: SQLiteRawValue { + /// - Returns: An ``SQLiteValue`` of type `.text` containing the UUID string. + public var sqliteValue: SQLiteValue { .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. - public init?(_ sqliteRawValue: SQLiteRawValue) { - switch sqliteRawValue { + /// - Parameter value: The SQLite value to convert from. + /// - Returns: A `UUID` instance if the string is valid, or `nil` otherwise. + public init?(_ value: SQLiteValue) { + switch value { case .text(let value): self.init(uuidString: value) default: diff --git a/Sources/DataLiteCore/Protocols/ArgumentsProtocol.swift b/Sources/DataLiteCore/Protocols/ArgumentsProtocol.swift new file mode 100644 index 0000000..3079ea4 --- /dev/null +++ b/Sources/DataLiteCore/Protocols/ArgumentsProtocol.swift @@ -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(index: Index) -> T? { get } +} + +public extension ArgumentsProtocol { + subscript(index: Index) -> T? { + T.init(self[index]) + } +} diff --git a/Sources/DataLiteCore/Protocols/ConnectionDelegate.swift b/Sources/DataLiteCore/Protocols/ConnectionDelegate.swift index fcf58f8..9be6930 100644 --- a/Sources/DataLiteCore/Protocols/ConnectionDelegate.swift +++ b/Sources/DataLiteCore/Protocols/ConnectionDelegate.swift @@ -1,170 +1,51 @@ 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 -/// events that occur within a ``Connection``, including SQL statement tracing, database update -/// actions, and transaction commits or rollbacks. Implementing this protocol provides a way -/// to monitor and respond to database interactions in a structured manner. +/// Conforming types can monitor row-level updates and transaction lifecycle events. All methods are +/// optional — default implementations do nothing. /// -/// ### 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 -/// conforming types to only implement the methods they are interested in without the need to -/// provide an implementation for each method. +/// - Important: Delegate methods are invoked synchronously on SQLite’s internal execution thread. +/// Implementations must be lightweight and non-blocking to avoid slowing down SQL operations. /// /// ## Topics /// /// ### Instance Methods /// -/// - ``ConnectionDelegate/connection(_:trace:)`` /// - ``ConnectionDelegate/connection(_:didUpdate:)`` /// - ``ConnectionDelegate/connectionWillCommit(_:)`` /// - ``ConnectionDelegate/connectionDidRollback(_:)`` 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 - /// 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. + /// Enables reacting to data changes, for example to refresh caches or UI. /// /// - Parameters: - /// - connection: The ``Connection`` instance that is executing the SQL statement. - /// - sql: A tuple containing the unexpanded and expanded forms of the SQL statement being traced. - /// - `unexpandedSQL`: The original SQL statement as it was written by the developer. - /// - `expandedSQL`: The SQL statement with all parameters substituted in, which shows - /// exactly what is being sent to SQLite. - /// - /// ### Example - /// - /// You can implement this method to log or analyze SQL statements: - /// - /// ```swift - /// func connection( - /// _ connection: Connection, - /// trace sql: (unexpandedSQL: String, expandedSQL: String) - /// ) { - /// print("Tracing SQL: \(sql.unexpandedSQL)") - /// } - /// ``` - /// - /// - Important: If the implementation of this method performs any heavy operations, it could - /// potentially slow down the execution of the SQL statement. It is recommended to keep the - /// implementation lightweight to avoid impacting performance. - func connection(_ connection: Connection, trace sql: (unexpandedSQL: String, expandedSQL: String)) + /// - connection: The connection where the update occurred. + /// - action: Describes the affected database, table, and row. + func connection(_ connection: ConnectionProtocol, didUpdate action: SQLiteAction) - /// 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, - /// 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. + /// Throwing an error aborts the commit and causes a rollback. /// - /// - Parameters: - /// - connection: The `Connection` instance where the update action occurred. - /// - action: The type of update action that occurred, represented by the ``SQLiteAction`` enum. - /// - /// ### Example - /// - /// You can implement this method to respond to specific update actions: - /// - /// ```swift - /// func connection(_ connection: Connection, didUpdate action: SQLiteAction) { - /// switch action { - /// case .insert(let db, let table, let rowID): - /// print("Inserted row \(rowID) into \(table) in database \(db).") - /// case .update(let db, let table, let rowID): - /// print("Updated row \(rowID) in \(table) in database \(db).") - /// case .delete(let db, let table, let rowID): - /// print("Deleted row \(rowID) from \(table) in database \(db).") - /// } - /// } - /// ``` - /// - /// - Note: Implementing this method can help you maintain consistency and perform any - /// necessary actions (such as UI updates or logging) in response to database changes. - func connection(_ connection: Connection, didUpdate action: SQLiteAction) + /// - Parameter connection: The connection about to commit. + /// - Throws: An error to cancel and roll back the transaction. + func connectionWillCommit(_ connection: ConnectionProtocol) throws - /// 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 - /// 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. + /// Use to perform cleanup or maintain consistency after a failure. /// - /// - Parameter connection: The `Connection` instance where the transaction was committed. - /// - /// - Throws: May throw an error to abort the commit process, which will cause the transaction - /// to be rolled back. - /// - /// ### Example - /// You can implement this method to perform actions after a successful commit: - /// - /// ```swift - /// func 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) + /// - Parameter connection: The connection that rolled back. + func connectionDidRollback(_ connection: ConnectionProtocol) } public extension ConnectionDelegate { - /// Default implementation of the `connection(_:trace:)` method. - /// - /// This default implementation does nothing. - /// - /// - Parameters: - /// - connection: The `Connection` instance that is executing the SQL statement. - /// - sql: A tuple containing the unexpanded and expanded forms of the SQL statement being traced. - func connection(_ connection: Connection, trace sql: (unexpandedSQL: String, expandedSQL: String)) {} - - /// Default implementation of the `connection(_:didUpdate:)` method. - /// - /// This default implementation does nothing. - /// - /// - Parameters: - /// - connection: The `Connection` instance where the update action occurred. - /// - action: The type of update action that occurred. - func connection(_ connection: Connection, didUpdate action: SQLiteAction) {} - - /// Default implementation of the `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) {} + func connection(_ connection: ConnectionProtocol, didUpdate action: SQLiteAction) {} + func connectionWillCommit(_ connection: ConnectionProtocol) throws {} + func connectionDidRollback(_ connection: ConnectionProtocol) {} } diff --git a/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift b/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift index a324c5a..8ddd415 100644 --- a/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift +++ b/Sources/DataLiteCore/Protocols/ConnectionProtocol.swift @@ -1,27 +1,20 @@ 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 -/// to an SQLite database, including connection state, configuration via PRAGMA, -/// executing SQL statements and scripts, transaction control, and encryption support. -/// -/// It also includes support for delegation to handle connection-related events. -/// -/// ## See Also -/// -/// - ``Connection`` +/// The `ConnectionProtocol` defines the essential API for managing a database connection, +/// including configuration, statement preparation, transactions, encryption, and delegation. +/// Conforming types are responsible for maintaining the connection’s lifecycle and settings. /// /// ## Topics /// -/// ### Connection State +/// ### Managing Connection State /// /// - ``isAutocommit`` /// - ``isReadonly`` /// - ``busyTimeout`` /// -/// ### PRAGMA Accessors +/// ### Accessing PRAGMA Values /// /// - ``applicationID`` /// - ``foreignKeys`` @@ -29,276 +22,376 @@ import DataLiteC /// - ``synchronous`` /// - ``userVersion`` /// -/// ### Delegation -/// -/// - ``addDelegate(_:)`` -/// - ``removeDelegate(_:)`` -/// -/// ### SQLite Lifecycle +/// ### Managing SQLite Lifecycle /// /// - ``initialize()`` /// - ``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:)`` /// - ``remove(function:)`` /// -/// ### Statement Preparation +/// ### Preparing SQL Statements /// +/// - ``prepare(sql:)`` /// - ``prepare(sql:options:)`` /// -/// ### Script Execution +/// ### Executing SQL Commands /// -/// - ``execute(sql:)`` /// - ``execute(raw:)`` +/// - ``execute(sql:)`` /// -/// ### PRAGMA Execution +/// ### Controlling PRAGMA Settings /// /// - ``get(pragma:)`` /// - ``set(pragma:value:)`` /// -/// ### Transactions +/// ### Managing Transactions /// /// - ``beginTransaction(_:)`` /// - ``commitTransaction()`` /// - ``rollbackTransaction()`` -/// -/// ### Encryption Keys -/// -/// - ``Connection/Key`` -/// - ``apply(_:name:)`` -/// - ``rekey(_:name:)`` public protocol ConnectionProtocol: AnyObject { // 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 - /// explicit transactions are active. Executing `BEGIN` disables autocommit mode, - /// and executing `COMMIT` or `ROLLBACK` re-enables it. + /// Autocommit is enabled by default and remains active when no explicit transaction is open. + /// Executing `BEGIN` disables autocommit, while `COMMIT` or `ROLLBACK` re-enables it. /// - /// - Returns: `true` if the connection is in autocommit mode; otherwise, `false`. - /// - SeeAlso: [sqlite3_get_autocommit()](https://sqlite.org/c3ref/get_autocommit.html) + /// - Returns: `true` if autocommit mode is active; otherwise, `false`. + /// - SeeAlso: [Test For Auto-Commit Mode](https://sqlite.org/c3ref/get_autocommit.html) var isAutocommit: Bool { get } - /// 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. - /// It returns `true` if the database was opened with read-only access, - /// and `false` if it allows read-write access. + /// Returns `true` if the main database allows only read operations, or `false` if it permits + /// both reading and writing. /// - /// - Returns: `true` if the main database is read-only; otherwise, `false`. - /// - SeeAlso: [sqlite3_db_readonly()](https://www.sqlite.org/c3ref/db_readonly.html) + /// - Returns: `true` if the connection is read-only; otherwise, `false`. + /// - SeeAlso: [Determine if a database is read-only](https://sqlite.org/c3ref/db_readonly.html) var isReadonly: Bool { get } - /// 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 - /// before returning a `SQLITE_BUSY` error. A value of zero disables the timeout and causes - /// operations to fail immediately if the database is locked. + /// Defines how long SQLite waits for a locked database to become available before returning + /// a `SQLITE_BUSY` error. A value of zero disables the timeout, causing operations to fail + /// immediately if the database is locked. /// - /// - SeeAlso: [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 } // 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. - /// It is stored at a fixed offset within the database file header and can be read or modified - /// using the `application_id` pragma. + /// Used to distinguish database files created by different applications or file formats. This + /// value is a 32-bit integer written to the database header and can be queried or modified + /// through the `PRAGMA application_id` command. /// - /// - SeeAlso: [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 } - /// 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 - /// by the database connection. When set to `true`, constraints are enforced; - /// when `false`, they are ignored. + /// When enabled, SQLite enforces foreign key constraints on all tables. This behavior can be + /// controlled with `PRAGMA foreign_keys`. /// - /// - 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 } /// The journal mode used by the database connection. /// - /// The journal mode determines how SQLite manages rollback journals, - /// impacting durability, concurrency, and performance. + /// Determines how SQLite maintains the rollback journal for transactions. /// - /// Setting this property updates the journal mode using the corresponding SQLite PRAGMA. - /// - /// - SeeAlso: [PRAGMA journal_mode](https://www.sqlite.org/pragma.html#pragma_journal_mode) + /// - SeeAlso: [Journal Mode](https://sqlite.org/pragma.html#pragma_journal_mode) 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 - /// physically written to disk, influencing durability and performance. + /// Controls how aggressively SQLite syncs data to disk for durability versus performance. /// - /// Setting this property updates the synchronous mode using the - /// corresponding SQLite PRAGMA. - /// - /// - SeeAlso: [PRAGMA synchronous](https://www.sqlite.org/pragma.html#pragma_synchronous) + /// - SeeAlso: [Synchronous](https://sqlite.org/pragma.html#pragma_synchronous) 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 - /// is typically used by applications to track the schema version - /// or migration state of the database. + /// This value is stored in the database header and can be used by applications to track schema + /// migrations or format changes. /// - /// Setting this property updates the corresponding SQLite PRAGMA. - /// - /// - SeeAlso: [PRAGMA user_version](https://www.sqlite.org/pragma.html#pragma_user_version) + /// - SeeAlso: [User Version](https://sqlite.org/pragma.html#pragma_user_version) 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 /// Initializes the SQLite library. /// - /// This method sets up the global state required by SQLite. It must be called before using - /// any other SQLite interface, unless SQLite is initialized automatically. + /// Sets up the global state required by SQLite, including operating-system–specific + /// 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 - /// the process, or the first time after a call to ``shutdown()``. All other calls are no-ops. + /// Only the first invocation during the process lifetime, or the first after + /// ``shutdown()``, performs real initialization. All subsequent calls are no-ops. /// - /// - Throws: ``Connection/Error`` if the initialization fails. - /// - SeeAlso: [sqlite3_initialize()](https://www.sqlite.org/c3ref/initialize.html) - static func initialize() throws(Connection.Error) - + /// - Note: Workstation applications normally do not need to call this function explicitly, + /// as it is invoked automatically by interfaces such as `sqlite3_open()`. It is mainly + /// intended for embedded systems and controlled initialization scenarios. + /// + /// - Throws: ``SQLiteError`` if initialization fails. + /// - SeeAlso: [Initialize The SQLite Library](https://sqlite.org/c3ref/initialize.html) + static func initialize() throws(SQLiteError) + /// Shuts down the SQLite library. /// - /// This method releases global resources used by SQLite and reverses the effects of a successful - /// call to ``initialize()``. It must be called exactly once for each successful call to - /// ``initialize()``, and only after all database connections are closed. + /// Releases all global resources allocated by SQLite and undoes the effects of a + /// successful call to ``initialize()``. This function should be called exactly once + /// for each effective initialization and only after all database connections are closed. /// - /// - Throws: ``Connection/Error`` if the shutdown process fails. - /// - SeeAlso: [sqlite3_shutdown()](https://www.sqlite.org/c3ref/initialize.html) - static func shutdown() throws(Connection.Error) + /// Only the first invocation since the last call to ``initialize()`` performs + /// deinitialization. All other calls are harmless no-ops. + /// + /// - Note: Workstation applications normally do not need to call this function explicitly, + /// as cleanup happens automatically at process termination. It is mainly used in + /// embedded systems where precise resource control is required. + /// + /// - Important: This function is **not** threadsafe and must be called from a single thread. + /// - Throws: ``SQLiteError`` if the shutdown process fails. + /// - SeeAlso: [Initialize The SQLite Library](https://sqlite.org/c3ref/initialize.html) + static func shutdown() throws(SQLiteError) + + // MARK: - Encryption + + /// Applies an encryption key to a database connection. + /// + /// If the database is newly created, this call initializes encryption and makes it encrypted. + /// If the database already exists, this call decrypts its contents for access using the + /// provided key. An existing unencrypted database cannot be encrypted using this method. + /// + /// This function must be called immediately after the connection is opened and before invoking + /// any other operation on the same connection. + /// + /// - Parameters: + /// - key: The encryption key to apply. + /// - name: The database name, or `nil` for the main database. + /// - Throws: ``SQLiteError`` if the key is invalid or the decryption process fails. + /// - SeeAlso: [Setting The Key](https://www.zetetic.net/sqlcipher/sqlcipher-api/#key) + func apply(_ key: Connection.Key, name: String?) throws(SQLiteError) + + /// Changes the encryption key for an open database. + /// + /// Re-encrypts the database file with a new key while preserving its existing data. The + /// connection must already be open and unlocked with a valid key applied through + /// ``apply(_:name:)``. This operation replaces the current encryption key but does not modify + /// the database contents. + /// + /// This function can only be used with an encrypted database. It has no effect on unencrypted + /// databases. + /// + /// - Parameters: + /// - key: The new encryption key to apply. + /// - name: The database name, or `nil` for the main database. + /// - Throws: ``SQLiteError`` if rekeying fails or encryption is not supported. + /// - SeeAlso: [Changing The Key](https://www.zetetic.net/sqlcipher/sqlcipher-api/#Changing_Key) + func rekey(_ key: Connection.Key, name: String?) throws(SQLiteError) + + // MARK: - Delegation + + /// Adds a delegate to receive connection-level events. + /// + /// Registers an object conforming to ``ConnectionDelegate`` to receive notifications such as + /// update actions and transaction events. + /// + /// - Parameter delegate: The delegate to add. + func add(delegate: ConnectionDelegate) + + /// Removes a previously added delegate. + /// + /// Unregisters an object that was previously added with ``add(delegate:)`` so it no longer + /// receives update and transaction events. + /// + /// - Parameter delegate: The delegate to remove. + func remove(delegate: ConnectionDelegate) + + /// Adds a delegate to receive SQL trace callbacks. + /// + /// Registers an object conforming to ``ConnectionTraceDelegate`` to observe SQL statements as + /// they are executed by the connection. + /// + /// - Parameter delegate: The trace delegate to add. + func add(trace delegate: ConnectionTraceDelegate) + + /// Removes a previously added trace delegate. + /// + /// Unregisters an object that was previously added with ``add(trace:)`` so it no longer + /// receives SQL trace callbacks. + /// + /// - Parameter delegate: The trace delegate to remove. + func remove(trace delegate: ConnectionTraceDelegate) // MARK: - Custom SQL Functions - /// Registers a custom 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. - /// - Throws: ``Connection/Error`` if the function registration fails. - func add(function: Function.Type) throws(Connection.Error) + /// - Parameter function: The custom function type to register. + /// - Throws: ``SQLiteError`` if registration fails. + 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. - /// - Throws: ``Connection/Error`` if the function removal fails. - func remove(function: Function.Type) throws(Connection.Error) + /// The specified function type must match the one used during registration. After removal, + /// the function will no longer be available for use in SQL statements. + /// + /// - Parameter function: The custom function type to unregister. + /// - Throws: ``SQLiteError`` if the function could not be unregistered. + func remove(function: Function.Type) throws(SQLiteError) // MARK: - Statement Preparation /// Prepares an SQL statement for execution. /// - /// Compiles the provided SQL query into a ``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: - /// - query: The SQL query string to prepare. - /// - options: Options that affect statement preparation. - /// - Returns: A prepared ``Statement`` ready for execution. - /// - Throws: ``Connection/Error`` if statement preparation fails. - /// - SeeAlso: [sqlite3_prepare_v3()](https://www.sqlite.org/c3ref/prepare.html) - func prepare(sql query: String, options: Statement.Options) throws(Connection.Error) -> Statement - - // MARK: - Script Execution - - /// Executes a sequence of SQL statements. + /// - query: The SQL query to prepare. + /// - options: Additional compilation options. + /// - Returns: A compiled statement ready for execution. + /// - Throws: ``SQLiteError`` if the statement could not be prepared. /// - /// Processes the given SQL script by executing each individual statement in order. - /// - /// - Parameter script: A collection of SQL statements to execute. - /// - Throws: ``Connection/Error`` if any statement execution fails. - func execute(sql script: SQLScript) throws(Connection.Error) + /// - SeeAlso: [Compiling An SQL Statement](https://sqlite.org/c3ref/prepare.html) + func prepare( + sql query: String, options: Statement.Options + ) throws(SQLiteError) -> StatementProtocol - /// Executes a raw SQL string. - /// - /// Executes the provided raw SQL string as a single operation. - /// - /// - Parameter sql: The raw SQL string to execute. - /// - Throws: ``Connection/Error`` if the execution fails. - func execute(raw sql: String) throws(Connection.Error) + // MARK: - SQL Execution - // MARK: - PRAGMA Execution - - /// Retrieves the value of a PRAGMA setting from the database. + /// Executes one or more SQL statements in a single step. /// - /// - Parameter pragma: The PRAGMA setting to retrieve. - /// - Returns: The current value of the PRAGMA, or `nil` if the value is not available. - /// - Throws: ``Connection/Error`` if the operation fails. - func get(pragma: Pragma) throws(Connection.Error) -> T? + /// The provided SQL string may contain one or more statements separated by semicolons. + /// Each statement is compiled and executed sequentially within the current connection. + /// This method is suitable for operations that do not produce result sets, such as + /// `CREATE TABLE`, `INSERT`, `UPDATE`, or `PRAGMA`. + /// + /// Execution stops at the first error, and the corresponding ``SQLiteError`` is thrown. + /// + /// - Parameter 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(pragma: Pragma) throws(SQLiteError) -> T? + + /// Sets a database PRAGMA value. + /// + /// Assigns the specified value to the given PRAGMA. This can be used to change runtime + /// configuration parameters, such as `foreign_keys`, `journal_mode`, or `synchronous`. /// /// - Parameters: - /// - pragma: The PRAGMA setting to modify. - /// - value: The new value to assign to the PRAGMA. - /// - Returns: The resulting value after the assignment, or `nil` if unavailable. - /// - Throws: ``Connection/Error`` if the operation fails. - @discardableResult - func set(pragma: Pragma, value: T) throws(Connection.Error) -> T? + /// - pragma: The PRAGMA to set. + /// - value: The value to assign to the PRAGMA. + /// - Throws: ``SQLiteError`` if the assignment fails. + /// + /// - SeeAlso: [PRAGMA Statements](https://sqlite.org/pragma.html) + func set(pragma: Pragma, value: T) throws(SQLiteError) // 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). - /// - Throws: ``Connection/Error`` if starting the transaction fails. - /// - SeeAlso: [BEGIN TRANSACTION](https://www.sqlite.org/lang_transaction.html) - func beginTransaction(_ type: TransactionType) throws(Connection.Error) - - /// Commits the current database transaction. + /// Starts an explicit transaction using the given ``TransactionType``. If a transaction is + /// already active, this method throws an error. /// - /// - Throws: ``Connection/Error`` if committing the transaction fails. - /// - SeeAlso: [COMMIT](https://www.sqlite.org/lang_transaction.html) - func commitTransaction() throws(Connection.Error) - - /// Rolls back the current database transaction. + /// - Parameter type: The transaction type to begin. + /// - Throws: ``SQLiteError`` if the transaction could not be started. /// - /// - Throws: ``Connection/Error`` if rolling back the transaction fails. - /// - SeeAlso: [ROLLBACK](https://www.sqlite.org/lang_transaction.html) - func rollbackTransaction() throws(Connection.Error) + /// - SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html) + func beginTransaction(_ type: TransactionType) throws(SQLiteError) - // MARK: - Encryption Keys - - /// Applies an encryption key to the database connection. + /// Commits the current transaction. /// - /// - Parameters: - /// - key: The encryption key to apply. - /// - name: An optional name identifying the database to apply the key to. - /// - Throws: ``Connection/Error`` if applying the key fails. - func apply(_ key: Connection.Key, name: String?) throws(Connection.Error) - - /// Changes the encryption key for the database connection. + /// Makes all changes made during the transaction permanent. If no transaction is active, this + /// method has no effect. /// - /// - Parameters: - /// - key: The new encryption key to set. - /// - name: An optional name identifying the database to rekey. - /// - Throws: ``Connection/Error`` if rekeying fails. - func rekey(_ key: Connection.Key, name: String?) throws(Connection.Error) + /// - Throws: ``SQLiteError`` if the commit operation fails. + /// + /// - SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html) + func commitTransaction() throws(SQLiteError) + + /// Rolls back the current transaction. + /// + /// Reverts all changes made during the transaction. If no transaction is active, this method + /// has no effect. + /// + /// - Throws: ``SQLiteError`` if the rollback operation fails. + /// + /// - SeeAlso: [Transaction](https://sqlite.org/lang_transaction.html) + func rollbackTransaction() throws(SQLiteError) } -// MARK: - PRAGMA Accessors +// MARK: - Default Implementation public extension ConnectionProtocol { + var busyTimeout: Int32 { + get { try! get(pragma: .busyTimeout) ?? 0 } + set { try! set(pragma: .busyTimeout, value: newValue) } + } + var applicationID: Int32 { get { try! get(pragma: .applicationID) ?? 0 } set { try! set(pragma: .applicationID, value: newValue) } @@ -323,71 +416,40 @@ public extension ConnectionProtocol { get { try! get(pragma: .userVersion) ?? 0 } set { try! set(pragma: .userVersion, value: newValue) } } -} - -// MARK: - SQLite Lifecycle - -public extension ConnectionProtocol { - static func initialize() throws(Connection.Error) { - let status = sqlite3_initialize() - if status != SQLITE_OK { - throw Connection.Error(code: status, message: "") - } + + func prepare(sql query: String) throws(SQLiteError) -> StatementProtocol { + try prepare(sql: query, options: []) } - static func shutdown() throws(Connection.Error) { - let status = sqlite3_shutdown() - if status != SQLITE_OK { - throw Connection.Error(code: status, message: "") - } - } -} - -// MARK: - Script Execution - -public extension ConnectionProtocol { - func execute(sql script: SQLScript) throws(Connection.Error) { + func execute(sql script: SQLScript) throws(SQLiteError) { for query in script { - let stmt = try prepare(sql: query, options: []) + let stmt = try prepare(sql: query) while try stmt.step() {} } } -} - -// MARK: - PRAGMA Execution - -public extension ConnectionProtocol { - func get(pragma: Pragma) throws(Connection.Error) -> T? { - let stmt = try prepare(sql: "PRAGMA \(pragma)", options: []) + + func get(pragma: Pragma) throws(SQLiteError) -> T? { + let stmt = try prepare(sql: "PRAGMA \(pragma)") switch try stmt.step() { - case true: return stmt.columnValue(at: 0) + case true: return stmt.columnValue(at: 0) case false: return nil } } - @discardableResult - func set(pragma: Pragma, value: T) throws(Connection.Error) -> T? { + func set(pragma: Pragma, value: T) throws(SQLiteError) { let query = "PRAGMA \(pragma) = \(value.sqliteLiteral)" - let stmt = try prepare(sql: query, options: []) - switch try stmt.step() { - case true: return stmt.columnValue(at: 0) - case false: return nil - } + try prepare(sql: query).step() } -} - -// MARK: - Transactions - -public extension ConnectionProtocol { - func beginTransaction(_ type: TransactionType = .deferred) throws(Connection.Error) { + + func beginTransaction(_ type: TransactionType = .deferred) throws(SQLiteError) { try prepare(sql: "BEGIN \(type) TRANSACTION", options: []).step() } - func commitTransaction() throws(Connection.Error) { + func commitTransaction() throws(SQLiteError) { try prepare(sql: "COMMIT TRANSACTION", options: []).step() } - func rollbackTransaction() throws(Connection.Error) { + func rollbackTransaction() throws(SQLiteError) { try prepare(sql: "ROLLBACK TRANSACTION", options: []).step() } } diff --git a/Sources/DataLiteCore/Protocols/ConnectionTraceDelegate.swift b/Sources/DataLiteCore/Protocols/ConnectionTraceDelegate.swift new file mode 100644 index 0000000..e800667 --- /dev/null +++ b/Sources/DataLiteCore/Protocols/ConnectionTraceDelegate.swift @@ -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 SQLite’s 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) +} diff --git a/Sources/DataLiteCore/Protocols/SQLiteBindable.swift b/Sources/DataLiteCore/Protocols/SQLiteBindable.swift new file mode 100644 index 0000000..1448779 --- /dev/null +++ b/Sources/DataLiteCore/Protocols/SQLiteBindable.swift @@ -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 + } +} diff --git a/Sources/DataLiteCore/Protocols/SQLiteLiteralable.swift b/Sources/DataLiteCore/Protocols/SQLiteLiteralable.swift deleted file mode 100644 index 342bac2..0000000 --- a/Sources/DataLiteCore/Protocols/SQLiteLiteralable.swift +++ /dev/null @@ -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 } -} diff --git a/Sources/DataLiteCore/Protocols/SQLiteRawBindable.swift b/Sources/DataLiteCore/Protocols/SQLiteRawBindable.swift deleted file mode 100644 index e50c3cf..0000000 --- a/Sources/DataLiteCore/Protocols/SQLiteRawBindable.swift +++ /dev/null @@ -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 - } -} diff --git a/Sources/DataLiteCore/Protocols/SQLiteRawRepresentable.swift b/Sources/DataLiteCore/Protocols/SQLiteRawRepresentable.swift deleted file mode 100644 index 5ba7d6b..0000000 --- a/Sources/DataLiteCore/Protocols/SQLiteRawRepresentable.swift +++ /dev/null @@ -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) -} diff --git a/Sources/DataLiteCore/Protocols/SQLiteRepresentable.swift b/Sources/DataLiteCore/Protocols/SQLiteRepresentable.swift new file mode 100644 index 0000000..caa861c --- /dev/null +++ b/Sources/DataLiteCore/Protocols/SQLiteRepresentable.swift @@ -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) +} diff --git a/Sources/DataLiteCore/Protocols/StatementProtocol.swift b/Sources/DataLiteCore/Protocols/StatementProtocol.swift new file mode 100644 index 0000000..a1321d8 --- /dev/null +++ b/Sources/DataLiteCore/Protocols/StatementProtocol.swift @@ -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(_ 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(_ 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(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(_ value: T?, at index: Int32) throws(SQLiteError) { + try bind(value?.sqliteValue ?? .null, at: index) + } + + func bind(_ 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(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.. - - /// 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) + /// The type that represents a value stored in a column. + public typealias Value = SQLiteValue // MARK: - Properties - /// An ordered dictionary that stores the columns and their associated values in the row. - /// - /// This private property holds the internal representation of the row's data as an - /// `OrderedDictionary`, maintaining the insertion order of columns. It is used to - /// facilitate access to the row's columns and values, ensuring that the original - /// order from the SQLite database is preserved. - private var elements: Elements + private var elements: OrderedDictionary - /// 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 - /// elements in the row are indexed starting from zero, this property consistently - /// returns zero, allowing for predictable iteration through the row's data. - /// - /// - Complexity: `O(1)` - public var startIndex: Index { - 0 - } - - /// The ending index of the row, which is equal to the number of columns. - /// - /// This property indicates the position one past the last element in the row. - /// It returns the count of columns in the row, allowing for proper iteration - /// through the row's data in a collection context. The `endIndex` is useful - /// for determining the bounds of the row's elements when traversing or accessing them. - /// - /// - Complexity: `O(1)` - public var endIndex: Index { - elements.count - } - - /// A Boolean value indicating whether the row is empty. - /// - /// This property returns `true` if the row contains no columns; otherwise, it returns `false`. - /// It provides a quick way to check if there are any data present in the row, which can be - /// useful for validation or conditional logic when working with database rows. - /// - /// - Complexity: `O(1)` - public var isEmpty: Bool { - elements.isEmpty - } - - /// The number of columns in the row. - /// - /// This property returns the total count of columns stored in the row. It reflects - /// the number of column-value pairs in the `elements` dictionary, providing a convenient - /// way to determine how much data is present in the row. This is useful for iteration - /// and conditional checks when working with database rows. - /// - /// - Complexity: `O(1)` - public var count: Int { - elements.count - } - - /// A textual description of the row, showing the columns and values. - /// - /// This property returns a string representation of the row, including all column names - /// and their associated values. The description is generated from the `elements` dictionary, - /// providing a clear and concise overview of the row's data, which can be helpful for debugging - /// and logging purposes. - public var description: String { - elements.description - } - - /// A list of column names in the row, preserving their insertion order. - /// - /// Useful for dynamically generating SQL queries (e.g. `INSERT INTO ... (columns)`). - /// - /// - Complexity: `O(1)` - public var columns: [String] { + /// The order of column names corresponds to the sequence defined in the executed SQL statement. + /// This order is preserved exactly as provided by SQLite, ensuring deterministic column + /// indexing across rows. + public var columns: [Column] { 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, ...)`) - /// to match the row's structure. - /// - /// - Complexity: `O(n)` + /// Each element is formed by prefixing the column name with a colon (`:`), matching the syntax + /// of SQLite named parameters (e.g., `:username`, `:id`). The order of tokens matches the order + /// of columns in the result set. public var namedParameters: [String] { elements.keys.map { ":\($0)" } } // MARK: - Inits - /// Initializes an empty row. - /// - /// This initializer creates a new instance of `SQLiteRow` with no columns or values. + /// Creates an empty row with no columns. public init() { elements = [:] } // 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. - /// It returns an `Element`, which is a tuple containing the column name and its associated - /// value. The index must be valid; otherwise, it will trigger a runtime error. + /// Use this subscript to read or modify the value of a particular column by name. If the column + /// does not exist, the getter returns `nil` and assigning a value to a new column name adds it + /// to the row. /// - /// - Parameter index: The index of the element to access. - /// - Returns: A tuple containing the column name and its associated value. - /// - /// - Complexity: `O(1)` - public subscript(index: Index) -> Element { - let element = elements.elements[index] - return (element.key, element.value) - } - - /// Accesses the value for the specified column. - /// - /// This subscript allows you to retrieve or set the value associated with a given column name. - /// It returns an optional `Value`, which is the value stored in the row for the specified column. - /// If the column does not exist, it returns `nil`. When setting a value, the column will be created - /// if it does not already exist. - /// - /// - Parameter column: The name of the column to access. - /// - Returns: The value associated with the specified column, or `nil` if the column does not exist. - /// - /// - Complexity: On average, the complexity is O(1) for lookups and amortized O(1) for updates. + /// - Parameter column: The name of the column. + /// - 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. public subscript(column: Column) -> Value? { get { elements[column] } set { elements[column] = newValue } @@ -194,32 +67,132 @@ public struct SQLiteRow: Collection, CustomStringConvertible, Equatable { // 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. - /// It increments the given index by one, allowing for iteration through the row's elements - /// in a collection context. If the provided index is the last valid index, this method - /// will return an index that may not be valid for the collection, so it should be used - /// in conjunction with bounds checking. + /// Use this method to check if a column exists without retrieving its value or iterating + /// through all columns. /// - /// - Parameter i: A valid index of the row. - /// - Returns: The index immediately after `i`. - /// - /// - Complexity: `O(1)` - public func index(after i: Index) -> Index { - i + 1 - } - - /// Checks if the row contains a value for the specified column. - /// - /// This method determines whether a column with the given name exists in the row. It is - /// useful for validating the presence of data before attempting to access it. - /// - /// - Parameter column: The name of the column to check for. - /// - Returns: `true` if the column exists; otherwise, `false`. - /// - /// - Complexity: On average, the complexity is `O(1)`. + /// - Parameter column: The name of the column to look for. + /// - Returns: `true` if the column exists, otherwise `false`. + /// - Complexity: Average O(1). public func contains(_ column: Column) -> Bool { 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.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 {} diff --git a/Tests/DataLiteCoreTests/Classes/Connection+LocationTests.swift b/Tests/DataLiteCoreTests/Classes/Connection+LocationTests.swift index 5c768dc..2865c04 100644 --- a/Tests/DataLiteCoreTests/Classes/Connection+LocationTests.swift +++ b/Tests/DataLiteCoreTests/Classes/Connection+LocationTests.swift @@ -17,15 +17,4 @@ struct ConnectionLocationTests { let temporaryLocation = Connection.Location.temporary #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)") - } - } } diff --git a/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift b/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift index 5788f50..83b0074 100644 --- a/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift +++ b/Tests/DataLiteCoreTests/Classes/ConnectionTests.swift @@ -101,7 +101,7 @@ struct ConnectionTests { """) #expect( - throws: Connection.Error( + throws: SQLiteError( code: SQLITE_BUSY, message: "database is locked" ), @@ -229,7 +229,7 @@ struct ConnectionTests { try connection.add(function: function) try connection.remove(function: function) #expect( - throws: Connection.Error( + throws: SQLiteError( code: SQLITE_ERROR, message: "no such function: \(name)" ), @@ -250,7 +250,7 @@ private extension ConnectionTests { [.deterministic, .innocuous] } - override class func invoke(args: Arguments) throws -> SQLiteRawRepresentable? { + override class func invoke(args: any ArgumentsProtocol) throws -> SQLiteRepresentable? { args[0].description } } @@ -264,13 +264,13 @@ private extension ConnectionTests { private var count: Int = 0 - override func step(args: Arguments) throws { + override func step(args: any ArgumentsProtocol) throws { if args[0] != .null { count += 1 } } - override func finalize() throws -> SQLiteRawRepresentable? { + override func finalize() throws -> SQLiteRepresentable? { count } } diff --git a/Tests/DataLiteCoreTests/Classes/Statement+OptionsTests.swift b/Tests/DataLiteCoreTests/Classes/Statement+OptionsTests.swift index 5e29b87..081e3b4 100644 --- a/Tests/DataLiteCoreTests/Classes/Statement+OptionsTests.swift +++ b/Tests/DataLiteCoreTests/Classes/Statement+OptionsTests.swift @@ -1,45 +1,41 @@ +import Foundation import Testing -import DataLiteCore import DataLiteC +import DataLiteCore struct StatementOptionsTests { - @Test func testOptionsInitialization() { - 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) + @Test func testPersistentOptions() { #expect(Statement.Options.persistent.rawValue == UInt32(SQLITE_PREPARE_PERSISTENT)) + } + + @Test func testNoVtabOptions() { #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)) + } } diff --git a/Tests/DataLiteCoreTests/Classes/StatementTests.swift b/Tests/DataLiteCoreTests/Classes/StatementTests.swift index af14e7c..c8e3860 100644 --- a/Tests/DataLiteCoreTests/Classes/StatementTests.swift +++ b/Tests/DataLiteCoreTests/Classes/StatementTests.swift @@ -1,135 +1,240 @@ -import XCTest +import Foundation +import Testing import DataLiteC + @testable import DataLiteCore -final class StatementTests: XCTestCase { - private let databasePath = FileManager.default.temporaryDirectory.appendingPathComponent("test.db").path - private var connection: OpaquePointer! +final class StatementTests { + let connection: OpaquePointer - override func setUpWithError() throws { - try super.setUpWithError() - - XCTAssertEqual( - sqlite3_open(databasePath, &connection), - SQLITE_OK, - "Failed to open database" + init() { + var connection: OpaquePointer! = nil + let opts = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE + sqlite3_open_v2(":memory:", &connection, opts, nil) + sqlite3_exec( + connection, + """ + CREATE TABLE t( + id INTEGER PRIMARY KEY, + n INTEGER, + r REAL, + s TEXT, + b BLOB + ); + """, nil, nil, nil ) - - XCTAssertEqual( - sqlite3_exec( - connection, - """ - CREATE TABLE users ( - id INTEGER PRIMARY KEY, - name TEXT, - age INTEGER - ); - """, - 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, - "Failed to create table" + performing: { + try Statement( + db: connection, + sql: "SELECT * FROM invalid", + options: [] + ) + } ) } - override func tearDownWithError() throws { - sqlite3_close(connection) - try FileManager.default.removeItem(atPath: databasePath) - try super.tearDownWithError() + @Test func testParameterCount() throws { + let sql = "SELECT * FROM t WHERE id = ? AND s = ?" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect(stmt.parameterCount() == 2) } - func testMixBindings() throws { - do { - let sql = "INSERT INTO users (name, age) VALUES (?, ?)" - let stmt = try Statement(db: connection, sql: sql, options: []) - try stmt.bind("Alice", at: 1) - try stmt.bind(88, at: 2) - XCTAssertFalse(try stmt.step()) - } - - do { - let sql = "SELECT * FROM users WHERE age = ? AND name = $name" - let stmt = try Statement(db: connection, sql: sql, options: []) - try stmt.bind(88, at: 1) - 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) - } + @Test func testZeroParameterCount() throws { + let sql = "SELECT * FROM t" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect(stmt.parameterCount() == 0) } - func testStatementInitialization() throws { - let sql = "INSERT INTO users (name, age) VALUES (?, ?)" - let statement = try Statement(db: connection, sql: sql, options: [.persistent]) - XCTAssertNotNil(statement, "Statement should not be nil") + @Test func testParameterIndexByName() throws { + let sql = "SELECT * FROM t WHERE id = :id AND s = :s" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect(stmt.parameterIndexBy(":id") == 1) + #expect(stmt.parameterIndexBy(":s") == 2) + #expect(stmt.parameterIndexBy(":invalid") == 0) } - func testBindAndExecute() throws { - let sql = "INSERT INTO users (name, age) VALUES (?, ?)" - let statement = try Statement(db: connection, sql: sql, options: [.persistent]) - try statement.bind("Alice", at: 1) - try statement.bind(30, at: 2) - XCTAssertEqual(statement.bindParameterCount(), 2) - 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) + @Test func testParameterNameByIndex() throws { + let sql = "SELECT * FROM t WHERE id = :id AND s = :s" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect(stmt.parameterNameBy(1) == ":id") + #expect(stmt.parameterNameBy(2) == ":s") + #expect(stmt.parameterNameBy(3) == nil) } - func testClearBindings() throws { - let sql = "INSERT INTO users (name, age) VALUES (?, ?)" - let statement = try Statement(db: connection, sql: sql, options: [.persistent]) - try statement.bind("Bob", at: 1) - try statement.bind(25, at: 2) - try statement.clearBindings() - XCTAssertFalse(try statement.step()) + @Test func testBindValueAtIndex() throws { + let sql = "SELECT * FROM t where id = ?" + let stmt = try Statement(db: connection, sql: sql, options: []) + try stmt.bind(.int(42), at: 1) + try stmt.bind(.real(42), at: 1) + try stmt.bind(.text("42"), at: 1) + 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 { - let sql = "INSERT INTO users (name, age) VALUES (?, ?)" - let statement = try Statement(db: connection, sql: sql, options: [.persistent]) - try statement.bind("Charlie", at: 1) - try statement.bind(40, at: 2) - try statement.step() - - // Reset the statement and try executing it again with new values - try statement.reset() - try statement.bind("Dave", at: 1) - 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) + @Test func testErrorBindValueAtIndex() throws { + let sql = "SELECT * FROM t where id = ?" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect( + throws: SQLiteError( + code: SQLITE_RANGE, + message: "column index out of range" + ), + performing: { + try stmt.bind(.null, at: 0) + } + ) } - func testColumnValues() throws { - let sql = "INSERT INTO users (name, age) VALUES (?, ?)" - let statement = try Statement(db: connection, sql: sql, options: [.persistent]) - try statement.bind("Eve", at: 1) - try statement.bind(28, at: 2) - try statement.step() + @Test func testBindValueByName() throws { + let sql = "SELECT * FROM t where id = :id" + let stmt = try Statement(db: connection, sql: sql, options: []) + try stmt.bind(.int(42), by: ":id") + try stmt.bind(.real(42), by: ":id") + 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") + } + + @Test func testErrorBindValueByName() throws { + let sql = "SELECT * FROM t where id = :id" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect( + throws: SQLiteError( + code: SQLITE_RANGE, + message: "column index out of range" + ), + performing: { + try stmt.bind(.null, by: ":invalid") + } + ) + } + + @Test func testStepOneRow() throws { + let sql = "SELECT 1 where 1" + let stmt = try Statement(db: connection, sql: sql, options: []) + #expect(try stmt.step()) + #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 + ) - // Perform a SELECT query and check column data types - let query = "SELECT * FROM users WHERE name = ?" - let queryStatement = try Statement(db: connection, sql: query, options: [.persistent]) - try queryStatement.bind("Eve", at: 1) + let sql = "SELECT * FROM t WHERE id = 10" + let stmt = try Statement(db: connection, sql: sql, options: []) - XCTAssertTrue(try queryStatement.step(), "Failed to execute SELECT query") - XCTAssertEqual(queryStatement.columnType(at: 1), .text) - XCTAssertEqual(queryStatement.columnType(at: 2), .int) - XCTAssertEqual(queryStatement.columnValue(at: 1), "Eve") - XCTAssertEqual(queryStatement.columnValue(at: 2), 28) + #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 + } + } } } diff --git a/Tests/DataLiteCoreTests/Enums/SQLiteRawTypeTests.swift b/Tests/DataLiteCoreTests/Enums/SQLiteRawTypeTests.swift deleted file mode 100644 index bad997a..0000000 --- a/Tests/DataLiteCoreTests/Enums/SQLiteRawTypeTests.swift +++ /dev/null @@ -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) - } -} diff --git a/Tests/DataLiteCoreTests/Enums/SQLiteRawValueTests.swift b/Tests/DataLiteCoreTests/Enums/SQLiteRawValueTests.swift deleted file mode 100644 index b7ee712..0000000 --- a/Tests/DataLiteCoreTests/Enums/SQLiteRawValueTests.swift +++ /dev/null @@ -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") - } -} diff --git a/Tests/DataLiteCoreTests/Enums/SQLiteValueTests.swift b/Tests/DataLiteCoreTests/Enums/SQLiteValueTests.swift new file mode 100644 index 0000000..edb7c76 --- /dev/null +++ b/Tests/DataLiteCoreTests/Enums/SQLiteValueTests.swift @@ -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) + } +} diff --git a/Tests/DataLiteCoreTests/Extensions/BinaryFloatingPointTests.swift b/Tests/DataLiteCoreTests/Extensions/BinaryFloatingPointTests.swift index 128e6b9..f793299 100644 --- a/Tests/DataLiteCoreTests/Extensions/BinaryFloatingPointTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/BinaryFloatingPointTests.swift @@ -4,46 +4,46 @@ import DataLiteCore struct BinaryFloatingPointTests { @Test func testFloatToSQLiteRawValue() { let floatValue: Float = 3.14 - let rawValue = floatValue.sqliteRawValue + let rawValue = floatValue.sqliteValue #expect(rawValue == .real(Double(floatValue))) } @Test func testDoubleToSQLiteRawValue() { let doubleValue: Double = 3.14 - let rawValue = doubleValue.sqliteRawValue + let rawValue = doubleValue.sqliteValue #expect(rawValue == .real(doubleValue)) } @Test func testFloatInitializationFromSQLiteRawValue() { - let realValue: SQLiteRawValue = .real(3.14) + let realValue: SQLiteValue = .real(3.14) let floatValue = Float(realValue) #expect(floatValue != nil) #expect(floatValue == 3.14) - let intValue: SQLiteRawValue = .int(42) + let intValue: SQLiteValue = .int(42) let floatFromInt = Float(intValue) #expect(floatFromInt != nil) #expect(floatFromInt == 42.0) } @Test func testDoubleInitializationFromSQLiteRawValue() { - let realValue: SQLiteRawValue = .real(3.14) + let realValue: SQLiteValue = .real(3.14) let doubleValue = Double(realValue) #expect(doubleValue != nil) #expect(doubleValue == 3.14) - let intValue: SQLiteRawValue = .int(42) + let intValue: SQLiteValue = .int(42) let doubleFromInt = Double(intValue) #expect(doubleFromInt != nil) #expect(doubleFromInt == 42.0) } @Test func testInitializationFailureFromInvalidSQLiteRawValue() { - let nullValue: SQLiteRawValue = .null + let nullValue: SQLiteValue = .null #expect(Float(nullValue) == nil) #expect(Double(nullValue) == nil) - let textValue: SQLiteRawValue = .text("Invalid") + let textValue: SQLiteValue = .text("Invalid") #expect(Float(textValue) == nil) #expect(Double(textValue) == nil) } diff --git a/Tests/DataLiteCoreTests/Extensions/BinaryIntegerTests.swift b/Tests/DataLiteCoreTests/Extensions/BinaryIntegerTests.swift index 3405b18..935851f 100644 --- a/Tests/DataLiteCoreTests/Extensions/BinaryIntegerTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/BinaryIntegerTests.swift @@ -3,38 +3,38 @@ import Foundation import DataLiteCore struct BinaryIntegerTests { - @Test func testIntegerToSQLiteRawValue() { - #expect(Int(42).sqliteRawValue == .int(42)) - #expect(Int8(42).sqliteRawValue == .int(42)) - #expect(Int16(42).sqliteRawValue == .int(42)) - #expect(Int32(42).sqliteRawValue == .int(42)) - #expect(Int64(42).sqliteRawValue == .int(42)) + @Test func testIntegerToSQLiteValue() { + #expect(Int(42).sqliteValue == .int(42)) + #expect(Int8(42).sqliteValue == .int(42)) + #expect(Int16(42).sqliteValue == .int(42)) + #expect(Int32(42).sqliteValue == .int(42)) + #expect(Int64(42).sqliteValue == .int(42)) - #expect(UInt(42).sqliteRawValue == .int(42)) - #expect(UInt8(42).sqliteRawValue == .int(42)) - #expect(UInt16(42).sqliteRawValue == .int(42)) - #expect(UInt32(42).sqliteRawValue == .int(42)) - #expect(UInt64(42).sqliteRawValue == .int(42)) + #expect(UInt(42).sqliteValue == .int(42)) + #expect(UInt8(42).sqliteValue == .int(42)) + #expect(UInt16(42).sqliteValue == .int(42)) + #expect(UInt32(42).sqliteValue == .int(42)) + #expect(UInt64(42).sqliteValue == .int(42)) } - @Test func testIntegerInitializationFromSQLiteRawValue() { - #expect(Int(SQLiteRawValue.int(42)) == 42) - #expect(Int8(SQLiteRawValue.int(42)) == 42) - #expect(Int16(SQLiteRawValue.int(42)) == 42) - #expect(Int32(SQLiteRawValue.int(42)) == 42) - #expect(Int64(SQLiteRawValue.int(42)) == 42) + @Test func testIntegerInitializationFromSQLiteValue() { + #expect(Int(SQLiteValue.int(42)) == 42) + #expect(Int8(SQLiteValue.int(42)) == 42) + #expect(Int16(SQLiteValue.int(42)) == 42) + #expect(Int32(SQLiteValue.int(42)) == 42) + #expect(Int64(SQLiteValue.int(42)) == 42) - #expect(UInt(SQLiteRawValue.int(42)) == 42) - #expect(UInt8(SQLiteRawValue.int(42)) == 42) - #expect(UInt16(SQLiteRawValue.int(42)) == 42) - #expect(UInt32(SQLiteRawValue.int(42)) == 42) - #expect(UInt64(SQLiteRawValue.int(42)) == 42) + #expect(UInt(SQLiteValue.int(42)) == 42) + #expect(UInt8(SQLiteValue.int(42)) == 42) + #expect(UInt16(SQLiteValue.int(42)) == 42) + #expect(UInt32(SQLiteValue.int(42)) == 42) + #expect(UInt64(SQLiteValue.int(42)) == 42) } @Test func testInvalidIntegerInitialization() { - #expect(Int(SQLiteRawValue.real(3.14)) == nil) - #expect(Int8(SQLiteRawValue.text("test")) == nil) - #expect(UInt32(SQLiteRawValue.blob(Data([0x01, 0x02]))) == nil) + #expect(Int(SQLiteValue.real(3.14)) == nil) + #expect(Int8(SQLiteValue.text("test")) == nil) + #expect(UInt32(SQLiteValue.blob(Data([0x01, 0x02]))) == nil) // Out-of-range conversion let largeValue = Int64.max diff --git a/Tests/DataLiteCoreTests/Extensions/BoolTests.swift b/Tests/DataLiteCoreTests/Extensions/BoolTests.swift index 7c53646..e77d055 100644 --- a/Tests/DataLiteCoreTests/Extensions/BoolTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/BoolTests.swift @@ -4,8 +4,8 @@ import DataLiteCore struct BoolTests { @Test func testBoolToSQLiteRawValue() { - #expect(true.sqliteRawValue == .int(1)) - #expect(false.sqliteRawValue == .int(0)) + #expect(true.sqliteValue == .int(1)) + #expect(false.sqliteValue == .int(0)) } @Test func testSQLiteRawValueToBool() { diff --git a/Tests/DataLiteCoreTests/Extensions/DataTests.swift b/Tests/DataLiteCoreTests/Extensions/DataTests.swift index 6f62616..14f6ad7 100644 --- a/Tests/DataLiteCoreTests/Extensions/DataTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/DataTests.swift @@ -5,12 +5,12 @@ import DataLiteCore struct DataSQLiteRawRepresentableTests { @Test func testDataToSQLiteRawValue() { let data = Data([0x01, 0x02, 0x03]) - #expect(data.sqliteRawValue == .blob(data)) + #expect(data.sqliteValue == .blob(data)) } @Test func testSQLiteRawValueToData() { let data = Data([0x01, 0x02, 0x03]) - let rawValue = SQLiteRawValue.blob(data) + let rawValue = SQLiteValue.blob(data) #expect(Data(rawValue) == data) diff --git a/Tests/DataLiteCoreTests/Extensions/DateTests.swift b/Tests/DataLiteCoreTests/Extensions/DateTests.swift index ef3476f..667027f 100644 --- a/Tests/DataLiteCoreTests/Extensions/DateTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/DateTests.swift @@ -8,7 +8,7 @@ struct DateSQLiteRawRepresentableTests { let formatter = ISO8601DateFormatter() let dateString = formatter.string(from: date) - #expect(date.sqliteRawValue == .text(dateString)) + #expect(date.sqliteValue == .text(dateString)) } @Test func testSQLiteRawValueToDate() { @@ -16,13 +16,13 @@ struct DateSQLiteRawRepresentableTests { let formatter = ISO8601DateFormatter() let dateString = formatter.string(from: date) - let rawText = SQLiteRawValue.text(dateString) + let rawText = SQLiteValue.text(dateString) #expect(Date(rawText) == date) - let rawInt = SQLiteRawValue.int(1609459200) + let rawInt = SQLiteValue.int(1609459200) #expect(Date(rawInt) == date) - let rawReal = SQLiteRawValue.real(1609459200) + let rawReal = SQLiteValue.real(1609459200) #expect(Date(rawReal) == date) #expect(Date(.blob(Data([0x01, 0x02, 0x03]))) == nil) diff --git a/Tests/DataLiteCoreTests/Extensions/RawRepresentableTests.swift b/Tests/DataLiteCoreTests/Extensions/RawRepresentableTests.swift index 95dd0c5..a01a2dd 100644 --- a/Tests/DataLiteCoreTests/Extensions/RawRepresentableTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/RawRepresentableTests.swift @@ -5,7 +5,7 @@ import DataLiteCore struct RawRepresentableTests { @Test func testRawRepresentableToSQLiteRawValue() { let color: Color = .green - #expect(color.sqliteRawValue == .int(1)) + #expect(color.sqliteValue == .int(1)) } @Test func testSQLiteRawValueToRawRepresentable() { @@ -19,7 +19,7 @@ struct RawRepresentableTests { } private extension RawRepresentableTests { - enum Color: Int, SQLiteRawRepresentable { + enum Color: Int, SQLiteRepresentable { case red case green case blue diff --git a/Tests/DataLiteCoreTests/Extensions/StringTests.swift b/Tests/DataLiteCoreTests/Extensions/StringTests.swift index 76dd30c..85c2a7f 100644 --- a/Tests/DataLiteCoreTests/Extensions/StringTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/StringTests.swift @@ -4,14 +4,14 @@ import DataLiteCore struct StringTests { @Test func testStringToSQLiteRawValue() { - #expect("Hello, SQLite!".sqliteRawValue == .text("Hello, SQLite!")) + #expect("Hello, SQLite!".sqliteValue == .text("Hello, SQLite!")) } @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(SQLiteRawValue.blob(Data([0x01, 0x02]))) == nil) - #expect(String(SQLiteRawValue.null) == nil) + #expect(String(SQLiteValue.int(42)) == nil) + #expect(String(SQLiteValue.blob(Data([0x01, 0x02]))) == nil) + #expect(String(SQLiteValue.null) == nil) } } diff --git a/Tests/DataLiteCoreTests/Extensions/UUIDTests.swift b/Tests/DataLiteCoreTests/Extensions/UUIDTests.swift index 43b184a..54bc0ba 100644 --- a/Tests/DataLiteCoreTests/Extensions/UUIDTests.swift +++ b/Tests/DataLiteCoreTests/Extensions/UUIDTests.swift @@ -5,11 +5,11 @@ import DataLiteCore struct UUIDTests { @Test func testUUIDToSQLiteRawValue() { 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() { - 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(.text("invalid-uuid-string")) == nil) diff --git a/Tests/DataLiteCoreTests/Protocols/SQLiteBindableTests.swift b/Tests/DataLiteCoreTests/Protocols/SQLiteBindableTests.swift new file mode 100644 index 0000000..cb6a6ce --- /dev/null +++ b/Tests/DataLiteCoreTests/Protocols/SQLiteBindableTests.swift @@ -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) + } +} diff --git a/Tests/DataLiteCoreTests/Classes/Connection+ErrorTests.swift b/Tests/DataLiteCoreTests/Structures/SQLiteErrorTests.swift similarity index 65% rename from Tests/DataLiteCoreTests/Classes/Connection+ErrorTests.swift rename to Tests/DataLiteCoreTests/Structures/SQLiteErrorTests.swift index 9556a5d..faa9231 100644 --- a/Tests/DataLiteCoreTests/Classes/Connection+ErrorTests.swift +++ b/Tests/DataLiteCoreTests/Structures/SQLiteErrorTests.swift @@ -3,26 +3,26 @@ import Testing import DataLiteC @testable import DataLiteCore -struct ConnectionErrorTests { +struct SQLiteErrorTests { @Test func testInitWithConnection() { var db: OpaquePointer? = nil defer { sqlite3_close(db) } sqlite3_open(":memory:", &db) sqlite3_exec(db, "INVALID SQL", nil, nil, nil) - let error = Connection.Error(db!) + let error = SQLiteError(db!) #expect(error.code == SQLITE_ERROR) #expect(error.message == "near \"INVALID\": syntax error") } @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.message == "Test Error Message") } @Test func testDescription() { - let error = Connection.Error(code: 1, message: "Test Error Message") - #expect(error.description == "Connection.Error code: 1 message: Test Error Message") + let error = SQLiteError(code: 1, message: "Test Error Message") + #expect(error.description == "SQLiteError code: 1 message: Test Error Message") } } diff --git a/Tests/DataLiteCoreTests/Structures/SQLiteRowTests.swift b/Tests/DataLiteCoreTests/Structures/SQLiteRowTests.swift index c04193c..5f8f4c5 100644 --- a/Tests/DataLiteCoreTests/Structures/SQLiteRowTests.swift +++ b/Tests/DataLiteCoreTests/Structures/SQLiteRowTests.swift @@ -26,7 +26,7 @@ final class SQLiteRowTests: XCTestCase { XCTAssertEqual(row["name"], .text("Alice")) XCTAssertNil(row["age"]) - row["age"] = SQLiteRawValue.int(30) + row["age"] = SQLiteValue.int(30) XCTAssertEqual(row["age"], .int(30)) }