Skip to main content

supercode_interchange/world/codec/
sqlite.rs

1//! SQLite primitives (`sqlite.mjs`): readers open read-only — the contract
2//! every supercode reader keeps over a live store — and writers create fresh
3//! files. Rows are read as JSON objects so the codecs see the same shapes the
4//! Node package saw.
5
6use std::path::Path;
7
8use rusqlite::types::ValueRef;
9use rusqlite::{Connection, OpenFlags};
10use serde_json::{Map, Value};
11
12use crate::Result;
13
14fn other(message: String) -> crate::Error {
15    crate::Error::Other(message)
16}
17
18fn open_read_only(path: &Path) -> Result<Connection> {
19    Connection::open_with_flags(
20        path,
21        OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
22    )
23    .map_err(|e| other(format!("{}: {e}", path.display())))
24}
25
26fn cell(value: ValueRef<'_>) -> Value {
27    match value {
28        ValueRef::Null => Value::Null,
29        ValueRef::Integer(i) => Value::from(i),
30        ValueRef::Real(f) => serde_json::Number::from_f64(f)
31            .map(Value::Number)
32            .unwrap_or(Value::Null),
33        ValueRef::Text(t) => Value::String(String::from_utf8_lossy(t).into_owned()),
34        ValueRef::Blob(b) => Value::String(String::from_utf8_lossy(b).into_owned()),
35    }
36}
37
38/// Whether `table` exists in the store at `path` (false when the file does not exist).
39pub fn table_exists(path: &Path, table: &str) -> bool {
40    if !path.is_file() {
41        return false;
42    }
43    let Ok(conn) = open_read_only(path) else {
44        return false;
45    };
46    conn.query_row(
47        "select name from sqlite_master where type='table' and name=?1",
48        [table],
49        |_| Ok(()),
50    )
51    .is_ok()
52}
53
54/// Every row of `sql` as a JSON object keyed by column name; `None` when the file does not exist.
55pub fn read_rows(
56    path: &Path,
57    sql: &str,
58    params: &[&dyn rusqlite::ToSql],
59) -> Result<Option<Vec<Map<String, Value>>>> {
60    if !path.is_file() {
61        return Ok(None);
62    }
63    let conn = open_read_only(path)?;
64    let mut statement = conn
65        .prepare(sql)
66        .map_err(|e| other(format!("{}: {e}", path.display())))?;
67    let names: Vec<String> = statement
68        .column_names()
69        .iter()
70        .map(|s| s.to_string())
71        .collect();
72    let rows = statement
73        .query_map(params, |row| {
74            let mut object = Map::new();
75            for (index, name) in names.iter().enumerate() {
76                object.insert(name.clone(), cell(row.get_ref(index)?));
77            }
78            Ok(object)
79        })
80        .map_err(|e| other(format!("{}: {e}", path.display())))?
81        .collect::<rusqlite::Result<Vec<_>>>()
82        .map_err(|e| other(format!("{}: {e}", path.display())))?;
83    Ok(Some(rows))
84}
85
86/// A parameter value for `write_table`.
87#[derive(Debug, Clone)]
88pub enum Param {
89    /// SQL NULL.
90    Null,
91    /// An integer.
92    Int(i64),
93    /// A real.
94    Real(f64),
95    /// Text.
96    Text(String),
97}
98
99impl From<&Value> for Param {
100    fn from(value: &Value) -> Self {
101        match value {
102            Value::Null => Self::Null,
103            Value::Bool(b) => Self::Int(i64::from(*b)),
104            Value::Number(n) => n
105                .as_i64()
106                .map(Self::Int)
107                .or_else(|| n.as_f64().map(Self::Real))
108                .unwrap_or(Self::Null),
109            Value::String(s) => Self::Text(s.clone()),
110            other => Self::Text(other.to_string()),
111        }
112    }
113}
114
115impl rusqlite::ToSql for Param {
116    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
117        use rusqlite::types::{ToSqlOutput, Value as SqlValue};
118        Ok(ToSqlOutput::Owned(match self {
119            Self::Null => SqlValue::Null,
120            Self::Int(i) => SqlValue::Integer(*i),
121            Self::Real(f) => SqlValue::Real(*f),
122            Self::Text(t) => SqlValue::Text(t.clone()),
123        }))
124    }
125}
126
127/// Create (`ddl`) and fill (`insert`, one parameter list per row) a store in one transaction.
128pub fn write_table(path: &Path, ddl: &str, insert: &str, rows: &[Vec<Param>]) -> Result<()> {
129    let conn = Connection::open(path).map_err(|e| other(format!("{}: {e}", path.display())))?;
130    if !ddl.trim().is_empty() {
131        conn.execute_batch(ddl)
132            .map_err(|e| other(format!("{}: {e}", path.display())))?;
133    }
134    conn.execute_batch("begin")
135        .map_err(|e| other(e.to_string()))?;
136    {
137        let mut statement = conn
138            .prepare(insert)
139            .map_err(|e| other(format!("{}: {e}", path.display())))?;
140        for row in rows {
141            let params: Vec<&dyn rusqlite::ToSql> =
142                row.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
143            statement
144                .execute(params.as_slice())
145                .map_err(|e| other(format!("{}: {e}", path.display())))?;
146        }
147    }
148    conn.execute_batch("commit")
149        .map_err(|e| other(e.to_string()))?;
150    Ok(())
151}