159 lines
5.9 KiB
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)
|
|
}
|
|
}
|
|
}
|