// // SQLiteStatement.swift // // Copyright 2026 Brendan Szymanski // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // // SPDX-License-Identifier: GPL-3.0-or-later // import CSQLite import Foundation import LuminateCore /// `SQLITE_TRANSIENT`. The C macro is a cast of `-1` and does not survive into Swift, so the bit /// pattern is reconstructed here; it tells SQLite to copy the bound bytes. private let sqliteTransient = unsafeBitCast(-1, to: sqlite3_destructor_type.self) /// A prepared SQLite statement with typed binding and column helpers. final class SQLiteStatement { private let database: SQLiteDatabase private let handle: OpaquePointer /// Prepares `sql` against `database`. /// /// - Parameters: /// - database: The open database that owns the statement. /// - sql: The SQL statement to prepare. /// - Throws: ``PreferenceStoreError/sqlite(message:)`` when preparation fails. init(database: SQLiteDatabase, sql: String) throws { self.database = database var statement: OpaquePointer? let result = sqlite3_prepare_v2(database.handle, sql, -1, &statement, nil) guard result == SQLITE_OK, let statement else { throw PreferenceStoreError.sqlite(message: database.errorMessage) } handle = statement } deinit { sqlite3_finalize(handle) } /// Binds a scalar preference value at a 1-based parameter index. /// /// - Parameters: /// - index: The SQLite parameter index. /// - value: The scalar value to bind. /// - Throws: ``PreferenceStoreError/sqlite(message:)`` when binding fails. func bind(_ index: Int32, _ value: StoredPreference) throws { let result: Int32 switch value { case .integer(let value): result = sqlite3_bind_int64(handle, index, value) case .real(let value): result = sqlite3_bind_double(handle, index, value) case .text(let value): result = bindText(index, value) case .blob(let value): if value.isEmpty { result = sqlite3_bind_zeroblob(handle, index, 0) } else { result = value.withUnsafeBytes { bytes in sqlite3_bind_blob(handle, index, bytes.baseAddress, Int32(value.count), sqliteTransient) } } } try check(result) } /// Binds a string at a 1-based parameter index. /// /// - Parameters: /// - index: The SQLite parameter index. /// - text: The string to bind. /// - Throws: ``PreferenceStoreError/sqlite(message:)`` when binding fails. func bind(_ index: Int32, text: String) throws { try check(bindText(index, text)) } /// Steps the statement once. /// /// - Returns: `true` when a row is available, or `false` when the statement is complete. /// - Throws: ``PreferenceStoreError/sqlite(message:)`` for any other SQLite result. func step() throws -> Bool { switch sqlite3_step(handle) { case SQLITE_ROW: return true case SQLITE_DONE: return false default: throw PreferenceStoreError.sqlite(message: database.errorMessage) } } /// Reads an integer result column. /// - Parameter index: The zero-based result column index. /// - Returns: The SQLite integer value. func integer(at index: Int32) -> Int32 { sqlite3_column_int(handle, index) } /// Reads a nullable string column. /// /// - Parameter index: The zero-based result column index. /// - Returns: The column text, or `nil` for SQL NULL. func text(at index: Int32) -> String? { guard let pointer = sqlite3_column_text(handle, index) else { return nil } let count = Int(sqlite3_column_bytes(handle, index)) return String(decoding: UnsafeBufferPointer(start: pointer, count: count), as: UTF8.self) } /// Reads the SQLite storage-class value from a result column. /// /// - Parameter index: The zero-based result column index. /// - Returns: The scalar value, or `nil` for SQL NULL or an unsupported storage class. func preference(at index: Int32) -> StoredPreference? { switch sqlite3_column_type(handle, index) { case SQLITE_INTEGER: return .integer(sqlite3_column_int64(handle, index)) case SQLITE_FLOAT: return .real(sqlite3_column_double(handle, index)) case SQLITE_TEXT: guard let value = text(at: index) else { return nil } return .text(value) case SQLITE_BLOB: let length = Int(sqlite3_column_bytes(handle, index)) guard let pointer = sqlite3_column_blob(handle, index), length > 0 else { return .blob(Data()) } return .blob(Data(bytes: pointer, count: length)) default: return nil } } private func bindText(_ index: Int32, _ value: String) -> Int32 { value.utf8CString.withUnsafeBufferPointer { buffer in sqlite3_bind_text(handle, index, buffer.baseAddress, Int32(buffer.count - 1), sqliteTransient) } } private func check(_ result: Int32) throws { guard result == SQLITE_OK else { throw PreferenceStoreError.sqlite(message: database.errorMessage) } } }