import Foundation import DataLiteC /// A value representing a static destructor for SQLite. /// /// `SQLITE_STATIC` is used to indicate that the SQLite library should not free the associated /// memory when the statement is finalized. let SQLITE_STATIC = unsafeBitCast( OpaquePointer(bitPattern: 0), to: sqlite3_destructor_type.self ) /// A value representing a transient destructor for SQLite. /// /// `SQLITE_TRANSIENT` is used to indicate that the SQLite library should make a copy of the /// associated memory and free the original memory when the statement is finalized. let SQLITE_TRANSIENT = unsafeBitCast( OpaquePointer(bitPattern: -1), to: sqlite3_destructor_type.self ) /// A class representing a prepared SQL statement in SQLite. /// /// ## Overview /// /// This class provides functionality for preparing, binding parameters, and executing SQL /// statements using SQLite. It also supports retrieving results and resource management, ensuring /// the statement is finalized when no longer needed. /// /// ## Preparing an SQL Statement /// /// To create a prepared SQL statement, use the ``Connection/prepare(sql:options:)`` method of the /// ``Connection`` object. /// /// ```swift /// do { /// let statement = try connection.prepare( /// sql: "SELECT id, name FROM users WHERE age > ?", /// options: [.persistent, .normalize] /// ) /// } catch { /// print("Error: \(error)") /// } /// ``` /// /// ## Binding Parameters /// /// SQL queries can contain parameters whose values can be bound after the statement is prepared. /// This prevents SQL injection and makes the code more secure. /// /// ### Binding Parameters by Index /// /// When preparing an SQL query, you can use the question mark (`?`) as a placeholder for parameter /// values. Parameter indexing starts from one (1). It is important to keep this in mind for /// correctly binding values to parameters in the SQL query. The method ``bind(_:at:)-(T?,_)`` /// is used to bind values to parameters. /// /// ```swift /// do { /// let query = "INSERT INTO users (name, age) VALUES (?, ?)" /// let statement = try connection.prepare(sql: query) /// try statement.bind("John Doe", at: 1) /// try statement.bind(30, at: 2) /// } catch { /// print("Error binding parameters: \(error)") /// } /// ``` /// /// ### Binding Parameters by Explicit Index /// /// Parameters can be explicitly bound by indices `?1`, `?2`, and so on. This improves readability /// and simplifies working with queries containing many parameters. Explicit indices do not need to /// start from one, be sequential, or contiguous. /// /// ```swift /// do { /// let query = "INSERT INTO users (name, age) VALUES (?1, ?2)" /// let statement = try connection.prepare(sql: query) /// try statement.bind("Jane Doe", at: 1) /// try statement.bind(25, at: 2) /// } catch { /// print("Error binding parameters: \(error)") /// } /// ``` /// /// ### Binding Parameters by Name /// /// Parameters can also be bound by names. This increases code readability and simplifies managing /// complex queries. Use ``bind(parameterIndexBy:)`` to retrieve the index of a named parameter. /// /// ```swift /// do { /// let query = "INSERT INTO users (name, age) VALUES (:userName, :userAge)" /// let statement = try connection.prepare(sql: query) /// /// let indexName = statement.bind(parameterIndexBy: ":userName") /// let indexAge = statement.bind(parameterIndexBy: ":userAge") /// /// try statement.bind("Jane Doe", at: indexName) /// try statement.bind(25, at: indexAge) /// } catch { /// print("Error binding parameters: \(error)") /// } /// ``` /// /// ### Duplicating Parameters /// /// Parameters with explicit indices or names can be duplicated. This allows the same value to be /// bound to multiple places in the query. /// /// ```swift /// do { /// let query = """ /// INSERT INTO users (name, age) /// VALUES /// (:userName, :userAge), /// (:userName, :userAge) /// """ /// let statement = try connection.prepare(sql: query) /// /// let indexName = statement.bind(parameterIndexBy: ":userName") /// let indexAge = statement.bind(parameterIndexBy: ":userAge") /// /// try statement.bind("Jane Doe", at: indexName) /// try statement.bind(25, at: indexAge) /// } catch { /// print("Error binding parameters: \(error)") /// } /// ``` /// /// ### Mixing Indexed and Named Parameters /// /// You can mix positional (`?`, `?NNN`) and named (`:name`, `@name`, `$name`) parameters /// in a single SQL statement. This is supported by SQLite and allows you to use different parameter /// styles simultaneously. /// /// ```swift /// do { /// let query = """ /// SELECT * FROM users WHERE age = ? AND name = :name /// """ /// let statement = try connection.prepare(sql: query) /// let nameIndex = statement.bind(parameterIndexBy: ":name") /// /// try statement.bind(88, at: 1) /// try statement.bind("Alice", at: nameIndex) /// } catch { /// print("Error binding parameters: \(error)") /// } /// ``` /// /// - Important: Although mixing parameter styles is technically allowed, it is generally not recommended. /// For clarity and maintainability, you should consistently use either indexed or named parameters /// throughout a query. Mixing styles may lead to confusion or hard-to-diagnose bugs in more complex queries. /// /// ## Generating SQL Using SQLiteRow /// /// The ``SQLiteRow`` type can be used not only for retrieving query results, but also for dynamically /// generating SQL statements. Its ordered keys and parameter-friendly formatting make it especially /// convenient for constructing `INSERT`, `UPDATE`, and similar queries with named parameters. /// /// ### Inserting a Row /// /// To insert a new row into a table using values from a ``SQLiteRow``, you can use the /// ``SQLiteRow/columns`` and ``SQLiteRow/namedParameters`` properties. /// This ensures the correct number and order of columns and parameters. /// /// ```swift /// var row = SQLiteRow() /// row["name"] = .text("Alice") /// row["age"] = .int(30) /// row["email"] = .text("alice@example.com") /// /// let columns = row.columns.joined(separator: ", ") // name, age, email /// let values = row.namedParameters.joined(separator: ", ") // :name, :age, :email /// /// let sql = "INSERT INTO users (\(columns)) VALUES (\(values))" /// let statement = try connection.prepare(sql: sql) /// try statement.bind(row) /// ``` /// /// This approach eliminates the need to manually write parameter placeholders or maintain their order. /// It also ensures full compatibility with the ``bind(_:)-(SQLiteRow)`` method. /// /// ### Updating a Row /// /// To construct an `UPDATE` statement using a ``SQLiteRow``, you can dynamically /// map the column names to SQL assignments in the form `column = :column`. /// /// ```swift /// var row = SQLiteRow() /// row["id"] = .int(123) /// row["name"] = .text("Alice") /// row["age"] = .int(30) /// row["email"] = .text("alice@example.com") /// /// let assignments = zip(row.columns, row.namedParameters) /// .map { "\($0.0) = \($0.1)" } /// .joined(separator: ", ") /// /// let sql = "UPDATE users SET \(assignments) WHERE id = :id" /// let statement = try connection.prepare(sql: sql) /// try statement.bind(row) /// try statement.step() /// ``` /// /// - Important: Ensure the SQLiteRow includes any values used in conditions /// (e.g., `:id` in `WHERE`), or binding will fail. /// /// ## Executing an SQL Statement /// /// The SQL statement is executed using the ``step()`` method. It returns `true` if there is a /// result to process, and `false` when execution is complete. To retrieve the results of an SQL /// statement, use ``columnCount()``, ``columnType(at:)``, ``columnName(at:)``, /// ``columnValue(at:)->SQLiteRawValue``, and ``currentRow()``. /// /// ```swift /// do { /// let query = "SELECT id, name FROM users WHERE age > ?" /// let statement = try connection.prepare(sql: query) /// try statement.bind(18, at: 1) /// while try statement.step() { /// for index in 0..SQLiteRawValue`` /// - ``columnValue(at:)->T?`` /// - ``currentRow()`` /// /// ### Evaluating /// /// - ``step()`` /// - ``reset()`` /// - ``execute(rows:)`` /// - ``execute(args:)`` /// /// ### Hashing /// /// - ``hash(into:)`` public final class Statement: Equatable, Hashable { // MARK: - Private Properties /// The SQLite statement pointer associated with this `Statement` instance. private let statement: OpaquePointer /// The SQLite database connection pointer used to create this statement. private let connection: OpaquePointer // MARK: - Inits /// Initializes a new `Statement` instance with a given SQL query and options. /// /// This initializer prepares the SQL statement for execution and sets up any necessary /// options. It throws an ``Connection/Error`` if the SQL preparation fails. /// /// - Parameters: /// - connection: A pointer to the SQLite database connection to use. /// - query: The SQL query string to prepare. /// - options: The options to use when preparing the SQL statement. /// - Throws: ``Connection/Error`` if the SQL statement preparation fails. init(db connection: OpaquePointer, sql query: String, options: Options) throws(Connection.Error) { var statement: OpaquePointer! = nil let status = sqlite3_prepare_v3(connection, query, -1, options.rawValue, &statement, nil) if status == SQLITE_OK, let statement { self.statement = statement self.connection = connection } else { sqlite3_finalize(statement) throw Connection.Error(connection) } } /// Finalizes the SQL statement, releasing any associated resources. deinit { sqlite3_finalize(statement) } // MARK: - Binding Parameters /// Returns the count of parameters that can be bound to this statement. /// /// This method provides the number of parameters that can be bound in the SQL statement, /// allowing you to determine how many parameters need to be set. /// /// - Returns: The number of bindable parameters in the statement. public func bindParameterCount() -> Int32 { sqlite3_bind_parameter_count(statement) } /// Returns the index of a parameter by its name. /// /// This method is used to find the index of a parameter in the SQL statement given its name. /// This is useful for binding values to named parameters. /// /// - Parameters: /// - name: The name of the parameter. /// - Returns: The index of the parameter, or 0 if the parameter does not exist. public func bind(parameterIndexBy name: String) -> Int32 { sqlite3_bind_parameter_index(statement, name) } /// Returns the name of a parameter by its index. /// /// This method retrieves the name of a parameter based on its index in the SQL statement. This /// is useful for debugging or when parameter names are needed. /// /// - Parameters: /// - index: The index of the parameter (1-based). /// - Returns: The name of the parameter, or `nil` if the name could not be retrieved. public func bind(parameterNameBy index: Int32) -> String? { guard let cString = sqlite3_bind_parameter_name(statement, index) else { return nil } return String(cString: cString) } /// Binds a value to a parameter at a specified index. /// /// This method allows you to bind various types of values (integer, real, text, or blob) to a /// parameter in the SQL statement. The appropriate SQLite function is called based on the type /// of value being bound. /// /// - Parameters: /// - value: The value to bind to the parameter. /// - index: The index of the parameter to bind (1-based). /// - Throws: ``Connection/Error`` if the binding operation fails. public func bind(_ value: SQLiteRawValue, at index: Int32) throws { let status: Int32 switch value { case .int(let value): status = sqlite3_bind_int64(statement, index, value) case .real(let value): status = sqlite3_bind_double(statement, index, value) case .text(let value): status = sqlite3_bind_text(statement, index, value) case .blob(let value): status = sqlite3_bind_blob(statement, index, value) case .null: status = sqlite3_bind_null(statement, index) } if status != SQLITE_OK { throw Connection.Error(connection) } } /// Binds a value conforming to `RawBindable` to a parameter at a specified index. /// /// This method provides a generic way to bind values that conform to `RawBindable`, /// allowing for flexibility in the types of values that can be bound to SQL statements. /// /// - Parameters: /// - value: The value to bind to the parameter. /// - index: The index of the parameter to bind (1-based). /// - Throws: ``Connection/Error`` if the binding operation fails. public func bind(_ 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 { if sqlite3_clear_bindings(statement) != SQLITE_OK { throw Connection.Error(connection) } } // MARK: - Retrieving Results /// Returns the number of columns in the result set. /// /// This method provides the count of columns returned by the SQL statement result, which is /// useful for iterating over query results and processing data. /// /// - Returns: The number of columns in the result set. public func columnCount() -> Int32 { sqlite3_column_count(statement) } /// Returns the type of data stored in a column at a specified index. /// /// This method retrieves the type of data stored in a particular column of the result set, /// allowing you to handle different data types appropriately. /// /// - Parameters: /// - index: The index of the column (0-based). /// - Returns: The type of data in the column as `SQLiteRawType`. public func columnType(at index: Int32) -> SQLiteRawType { .init(rawValue: sqlite3_column_type(statement, index)) ?? .null } /// Returns the name of a column at a specified index. /// /// This method retrieves the name of a column, which is useful for debugging or when you need /// to work with column names directly. /// /// - Parameters: /// - index: The index of the column (0-based). /// - Returns: The name of the column as a `String`. public func columnName(at index: Int32) -> String { String(cString: sqlite3_column_name(statement, index)) } /// Retrieves the value from a column at a specified index. /// /// This method extracts the value from a column and returns it as an `SQLiteRawValue`, which /// can represent different data types like integer, real, text, or blob. /// /// - Parameters: /// - index: The index of the column (0-based). /// - Returns: The value from the column as `SQLiteRawValue`. public func columnValue(at index: Int32) -> SQLiteRawValue { switch columnType(at: index) { case .int: return .int(sqlite3_column_int64(statement, index)) case .real: return .real(sqlite3_column_double(statement, index)) case .text: return .text(sqlite3_column_text(statement, index)) case .blob: return .blob(sqlite3_column_blob(statement, index)) case .null: return .null } } /// Retrieves the value from a column at a specified index and converts it to a value /// conforming to `SQLiteRawRepresentable`. /// /// This method provides a way to convert column values into types that conform to /// ``SQLiteRawRepresentable``, allowing for easier integration with custom data models. /// /// - Parameters: /// - index: The index of the column (0-based). /// - Returns: The value from the column converted to `T`, or `nil` if conversion fails. public func columnValue(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 private func sqlite3_bind_text(_ stmt: OpaquePointer!, _ index: Int32, _ string: String) -> Int32 { sqlite3_bind_text(stmt, index, string, -1, SQLITE_TRANSIENT) } private func sqlite3_bind_blob(_ stmt: OpaquePointer!, _ index: Int32, _ data: Data) -> Int32 { data.withUnsafeBytes { sqlite3_bind_blob(stmt, index, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT) } } private func sqlite3_column_text(_ stmt: OpaquePointer!, _ iCol: Int32) -> String { String(cString: DataLiteC.sqlite3_column_text(stmt, iCol)) } private func sqlite3_column_blob(_ stmt: OpaquePointer!, _ iCol: Int32) -> Data { Data( bytes: sqlite3_column_blob(stmt, iCol), count: Int(sqlite3_column_bytes(stmt, iCol)) ) }