luminate/Sources/LuminateStore/Database/SQLiteStatement.swift

159 lines
5.9 KiB
Swift

//
// SQLiteStatement.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// 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 <https://www.gnu.org/licenses/>.
//
// 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)
}
}
}