supercode-interchange 0.4.20

Canonical, provider-neutral session interchange primitives for Supercode
Documentation
//! SQLite primitives (`sqlite.mjs`): readers open read-only — the contract
//! every supercode reader keeps over a live store — and writers create fresh
//! files. Rows are read as JSON objects so the codecs see the same shapes the
//! Node package saw.

use std::path::Path;

use rusqlite::types::ValueRef;
use rusqlite::{Connection, OpenFlags};
use serde_json::{Map, Value};

use crate::Result;

fn other(message: String) -> crate::Error {
    crate::Error::Other(message)
}

fn open_read_only(path: &Path) -> Result<Connection> {
    Connection::open_with_flags(
        path,
        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
    )
    .map_err(|e| other(format!("{}: {e}", path.display())))
}

fn cell(value: ValueRef<'_>) -> Value {
    match value {
        ValueRef::Null => Value::Null,
        ValueRef::Integer(i) => Value::from(i),
        ValueRef::Real(f) => serde_json::Number::from_f64(f)
            .map(Value::Number)
            .unwrap_or(Value::Null),
        ValueRef::Text(t) => Value::String(String::from_utf8_lossy(t).into_owned()),
        ValueRef::Blob(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
    }
}

/// Whether `table` exists in the store at `path` (false when the file does not exist).
pub fn table_exists(path: &Path, table: &str) -> bool {
    if !path.is_file() {
        return false;
    }
    let Ok(conn) = open_read_only(path) else {
        return false;
    };
    conn.query_row(
        "select name from sqlite_master where type='table' and name=?1",
        [table],
        |_| Ok(()),
    )
    .is_ok()
}

/// Every row of `sql` as a JSON object keyed by column name; `None` when the file does not exist.
pub fn read_rows(
    path: &Path,
    sql: &str,
    params: &[&dyn rusqlite::ToSql],
) -> Result<Option<Vec<Map<String, Value>>>> {
    if !path.is_file() {
        return Ok(None);
    }
    let conn = open_read_only(path)?;
    let mut statement = conn
        .prepare(sql)
        .map_err(|e| other(format!("{}: {e}", path.display())))?;
    let names: Vec<String> = statement
        .column_names()
        .iter()
        .map(|s| s.to_string())
        .collect();
    let rows = statement
        .query_map(params, |row| {
            let mut object = Map::new();
            for (index, name) in names.iter().enumerate() {
                object.insert(name.clone(), cell(row.get_ref(index)?));
            }
            Ok(object)
        })
        .map_err(|e| other(format!("{}: {e}", path.display())))?
        .collect::<rusqlite::Result<Vec<_>>>()
        .map_err(|e| other(format!("{}: {e}", path.display())))?;
    Ok(Some(rows))
}

/// A parameter value for `write_table`.
#[derive(Debug, Clone)]
pub enum Param {
    /// SQL NULL.
    Null,
    /// An integer.
    Int(i64),
    /// A real.
    Real(f64),
    /// Text.
    Text(String),
}

impl From<&Value> for Param {
    fn from(value: &Value) -> Self {
        match value {
            Value::Null => Self::Null,
            Value::Bool(b) => Self::Int(i64::from(*b)),
            Value::Number(n) => n
                .as_i64()
                .map(Self::Int)
                .or_else(|| n.as_f64().map(Self::Real))
                .unwrap_or(Self::Null),
            Value::String(s) => Self::Text(s.clone()),
            other => Self::Text(other.to_string()),
        }
    }
}

impl rusqlite::ToSql for Param {
    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
        use rusqlite::types::{ToSqlOutput, Value as SqlValue};
        Ok(ToSqlOutput::Owned(match self {
            Self::Null => SqlValue::Null,
            Self::Int(i) => SqlValue::Integer(*i),
            Self::Real(f) => SqlValue::Real(*f),
            Self::Text(t) => SqlValue::Text(t.clone()),
        }))
    }
}

/// Create (`ddl`) and fill (`insert`, one parameter list per row) a store in one transaction.
pub fn write_table(path: &Path, ddl: &str, insert: &str, rows: &[Vec<Param>]) -> Result<()> {
    let conn = Connection::open(path).map_err(|e| other(format!("{}: {e}", path.display())))?;
    if !ddl.trim().is_empty() {
        conn.execute_batch(ddl)
            .map_err(|e| other(format!("{}: {e}", path.display())))?;
    }
    conn.execute_batch("begin")
        .map_err(|e| other(e.to_string()))?;
    {
        let mut statement = conn
            .prepare(insert)
            .map_err(|e| other(format!("{}: {e}", path.display())))?;
        for row in rows {
            let params: Vec<&dyn rusqlite::ToSql> =
                row.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
            statement
                .execute(params.as_slice())
                .map_err(|e| other(format!("{}: {e}", path.display())))?;
        }
    }
    conn.execute_batch("commit")
        .map_err(|e| other(e.to_string()))?;
    Ok(())
}