walletkit-db 0.11.1

Internal SQLite wrapper crate for WalletKit storage.
//! Safe wrapper around a `SQLite` prepared statement.
//!
//! This file contains **no `unsafe` code**. All FFI interaction is delegated to
//! [`ffi::RawStmt`] which encapsulates the raw pointers and C type conversions.

use super::error::DbResult;
use super::ffi::{self, RawStmt};
use super::value::Value;

/// Result of a single `sqlite3_step` call.
pub enum StepResult<'stmt, 'conn> {
    /// A result row is available.
    Row(Row<'stmt, 'conn>),
    /// The statement has finished executing.
    Done,
}

/// A guard that represents the current row for a statement.
///
/// Values read through this guard are valid for the current row only.
/// Calling `step`, `reset`, or dropping/finalizing the statement invalidates
/// `SQLite`'s internal row pointers.
pub struct Row<'stmt, 'conn> {
    stmt: &'stmt Statement<'conn>,
}

impl Row<'_, '_> {
    /// Reads a column as `i64`.
    ///
    /// # Panics
    ///
    /// Panics if `idx` exceeds `i32::MAX`.
    #[must_use]
    pub fn column_i64(&self, idx: usize) -> i64 {
        self.stmt
            .raw
            .column_i64(i32::try_from(idx).expect("column index overflow"))
    }

    /// Reads a column as a blob. Returns an empty `Vec` for NULL.
    ///
    /// # Panics
    ///
    /// Panics if `idx` exceeds `i32::MAX`.
    #[must_use]
    pub fn column_blob(&self, idx: usize) -> Vec<u8> {
        self.stmt
            .raw
            .column_blob(i32::try_from(idx).expect("column index overflow"))
    }

    /// Reads a column as a UTF-8 string. Returns an empty string for NULL.
    ///
    /// # Panics
    ///
    /// Panics if `idx` exceeds `i32::MAX`.
    #[must_use]
    pub fn column_text(&self, idx: usize) -> String {
        self.stmt
            .raw
            .column_text(i32::try_from(idx).expect("column index overflow"))
    }

    /// Returns `true` if the column is SQL NULL.
    ///
    /// # Panics
    ///
    /// Panics if `idx` exceeds `i32::MAX`.
    #[allow(dead_code)]
    #[must_use]
    pub fn is_column_null(&self, idx: usize) -> bool {
        self.stmt
            .raw
            .column_type(i32::try_from(idx).expect("column index overflow"))
            == ffi::SQLITE_NULL
    }
}

/// A prepared `SQLite` statement.
///
/// Created via [`Connection::prepare`](super::Connection::prepare).
/// Tied to the lifetime of the connection that created it.
/// Finalized when dropped.
pub struct Statement<'conn> {
    raw: RawStmt<'conn>,
}

impl<'conn> Statement<'conn> {
    /// Wraps a raw statement handle.
    pub(super) const fn new(raw: RawStmt<'conn>) -> Self {
        Self { raw }
    }

    /// Binds a slice of [`Value`]s to the statement parameters (1-indexed).
    ///
    /// # Errors
    ///
    /// Returns `DbError` if any bind call fails.
    ///
    /// # Panics
    ///
    /// Panics if the number of values exceeds `i32::MAX`.
    pub fn bind_values(&mut self, values: &[Value]) -> DbResult<()> {
        for (i, val) in values.iter().enumerate() {
            let idx = i32::try_from(i + 1).expect("parameter index overflow");
            match val {
                Value::Integer(v) => self.raw.bind_i64(idx, *v)?,
                Value::Blob(v) => self.raw.bind_blob(idx, v)?,
                Value::Text(v) => self.raw.bind_text(idx, v)?,
                Value::Null => self.raw.bind_null(idx)?,
            }
        }
        Ok(())
    }

    /// Executes a single step.
    ///
    /// # Errors
    ///
    /// Returns `DbError` if the step fails.
    pub fn step<'stmt>(&'stmt mut self) -> DbResult<StepResult<'stmt, 'conn>> {
        let rc = self.raw.step()?;
        if rc == ffi::SQLITE_ROW {
            Ok(StepResult::Row(Row { stmt: self }))
        } else {
            Ok(StepResult::Done)
        }
    }
}