Skip to main content

quarb_sqlite/
lib.rs

1//! SQLite adapter for the Quarb query engine.
2//!
3//! A thin catalog driver over the shared relational model
4//! (`quarb-relational`): it introspects the schema via PRAGMAs
5//! (`table_info`, `foreign_key_list`), streams every user table,
6//! maps SQLite's storage classes to values (`NULL` → null,
7//! `INTEGER`/`REAL` → numbers, text as itself, blobs as a size
8//! placeholder), and hands the result to [`RelationalModel`].
9//!
10//! The arbor mapping, the foreign-key reference machinery (`~>`
11//! chains, `->`/`<-` crosslinks, `<~` reverse resolution, the
12//! table-naming hint), and the metadata surface are documented on
13//! the shared model.
14
15use quarb::{AstAdapter, NodeId, Value};
16use quarb_relational::{RelationalModel, RowSpec, TableSpec};
17use rusqlite::Connection;
18use rusqlite::types::ValueRef;
19
20/// An error loading a database.
21#[derive(Debug, thiserror::Error)]
22pub enum SqliteError {
23    #[error("sqlite: {0}")]
24    Sqlite(#[from] rusqlite::Error),
25    #[error("pushdown plan: {0}")]
26    Plan(String),
27}
28
29/// A SQLite database, materialized as an arbor.
30pub struct SqliteAdapter {
31    model: RelationalModel,
32}
33
34fn to_value(v: ValueRef<'_>) -> Value {
35    match v {
36        ValueRef::Null => Value::Null,
37        ValueRef::Integer(n) => Value::Int(n),
38        ValueRef::Real(f) => Value::Float(f),
39        ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
40        ValueRef::Blob(b) => Value::Str(format!("<blob {} bytes>", b.len())),
41    }
42}
43
44/// Quote an identifier (table or column name) for interpolation into
45/// SQL: wrap in double quotes, doubling any embedded quote, so
46/// reserved words and special characters survive.
47fn quote_ident(name: &str) -> String {
48    format!("\"{}\"", name.replace('"', "\"\""))
49}
50
51/// Introspect the schema: every user table's spec, via PRAGMAs.
52fn specs(conn: &Connection) -> Result<Vec<TableSpec>, SqliteError> {
53    let names: Vec<String> = conn
54        .prepare(
55            "SELECT name FROM sqlite_master WHERE type = 'table' \
56             AND name NOT LIKE 'sqlite_%' ORDER BY name",
57        )?
58        .query_map([], |r| r.get(0))?
59        .collect::<Result<_, _>>()?;
60
61    let mut out = Vec::new();
62    for name in names {
63        // Columns and the primary key (a single-column pk names the
64        // rows; a composite or absent pk falls back to rowid).
65        let mut columns = Vec::new();
66        let mut pk_cols: Vec<(i64, usize)> = Vec::new();
67        {
68            let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote_ident(&name)))?;
69            let mut rows = stmt.query([])?;
70            while let Some(r) = rows.next()? {
71                let col: String = r.get(1)?;
72                let pk: i64 = r.get(5)?;
73                if pk > 0 {
74                    pk_cols.push((pk, columns.len()));
75                }
76                columns.push(col);
77            }
78        }
79        pk_cols.sort();
80        let pk = (pk_cols.len() == 1).then(|| pk_cols[0].1);
81
82        // Declared foreign keys (an omitted target column means the
83        // target's primary key).
84        let mut fks = Vec::new();
85        {
86            let mut stmt =
87                conn.prepare(&format!("PRAGMA foreign_key_list({})", quote_ident(&name)))?;
88            let mut rows = stmt.query([])?;
89            while let Some(r) = rows.next()? {
90                let target: String = r.get(2)?;
91                let from: String = r.get(3)?;
92                let to: Option<String> = r.get(4)?;
93                if let Some(idx) = columns.iter().position(|c| *c == from) {
94                    fks.push((idx, target, to.unwrap_or_default()));
95                }
96            }
97        }
98        out.push(TableSpec {
99            name,
100            columns,
101            pk,
102            fks,
103        });
104    }
105    Ok(out)
106}
107
108/// Stream one table's rows, optionally filtered (partial
109/// pushdown: the engine re-applies the pushed predicates).
110fn fetch_rows_where(
111    conn: &Connection,
112    spec: &TableSpec,
113    where_sql: Option<&str>,
114) -> Result<Vec<RowSpec>, SqliteError> {
115    let cols = spec
116        .columns
117        .iter()
118        .map(|c| quote_ident(c))
119        .collect::<Vec<_>>()
120        .join(", ");
121    let table = quote_ident(&spec.name);
122    let filter = match where_sql {
123        Some(w) => format!(" WHERE {w}"),
124        None => String::new(),
125    };
126    // Prefer SQLite's rowid: stable row identity, and the row's edge
127    // name for tables without a single-column primary key. WITHOUT
128    // ROWID tables have no rowid column, so `SELECT rowid` fails to
129    // prepare; fall back to a positional ordinal (below), ordered by
130    // the key for determinism.
131    let with_rowid = format!("SELECT rowid, {cols} FROM {table}{filter} ORDER BY rowid");
132    if let Ok(mut stmt) = conn.prepare(&with_rowid) {
133        let mut rows = stmt.query([])?;
134        let mut out = Vec::new();
135        while let Some(r) = rows.next()? {
136            let rowid: i64 = r.get(0)?;
137            let values: Vec<Value> = (0..spec.columns.len())
138                .map(|i| to_value(r.get_ref(i + 1).expect("column in range")))
139                .collect();
140            out.push(RowSpec { rowid, values });
141        }
142        return Ok(out);
143    }
144    // WITHOUT ROWID: no rowid column to select or order by. Order by
145    // the single-column primary key (which names the rows) when
146    // present, else by every column so the ordinal keys are stable.
147    let order = match spec.pk {
148        Some(i) => quote_ident(&spec.columns[i]),
149        None => cols.clone(),
150    };
151    let mut stmt = conn.prepare(&format!(
152        "SELECT {cols} FROM {table}{filter} ORDER BY {order}"
153    ))?;
154    let mut rows = stmt.query([])?;
155    let mut out = Vec::new();
156    let mut rowid = 0i64;
157    while let Some(r) = rows.next()? {
158        rowid += 1;
159        let values: Vec<Value> = (0..spec.columns.len())
160            .map(|i| to_value(r.get_ref(i).expect("column in range")))
161            .collect();
162        out.push(RowSpec { rowid, values });
163    }
164    Ok(out)
165}
166
167impl SqliteAdapter {
168    /// Open a database file (read-only): the catalog now, each
169    /// table's rows on first touch (the connection stays owned by
170    /// the adapter).
171    pub fn open(path: &std::path::Path) -> Result<Self, SqliteError> {
172        Self::open_impl(path, None)
173    }
174
175    /// [`open`], with one table's fetch filtered by a WHERE clause
176    /// (partial pushdown; the engine re-applies the predicates).
177    pub fn open_filtered(
178        path: &std::path::Path,
179        table: &str,
180        where_sql: &str,
181    ) -> Result<Self, SqliteError> {
182        Self::open_impl(path, Some((table.to_string(), where_sql.to_string())))
183    }
184
185    fn open_impl(
186        path: &std::path::Path,
187        filter: Option<(String, String)>,
188    ) -> Result<Self, SqliteError> {
189        let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
190        let specs = specs(&conn)?;
191        let model = RelationalModel::lazy(
192            specs,
193            Box::new(move |_, spec| {
194                let w = filter
195                    .as_ref()
196                    .filter(|(t, _)| *t == spec.name)
197                    .map(|(_, w)| w.as_str());
198                fetch_rows_where(&conn, spec, w).map_err(|e| e.to_string())
199            }),
200        );
201        Ok(SqliteAdapter { model })
202    }
203
204    /// Open a database held entirely in memory — the bytes of a
205    /// `.db` file that never touched a filesystem (the browser's
206    /// uploaded files). Deserializes read-only, then materializes
207    /// eagerly, as [`SqliteAdapter::load`] does.
208    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SqliteError> {
209        let mut conn = Connection::open_in_memory()?;
210        conn.deserialize_read_exact(rusqlite::MAIN_DB, bytes, bytes.len(), true)?;
211        Self::load(&conn)
212    }
213
214    /// Materialize every user table of an open connection, eagerly
215    /// (in-memory databases, tests).
216    pub fn load(conn: &Connection) -> Result<Self, SqliteError> {
217        let mut input = Vec::new();
218        for spec in specs(conn)? {
219            let rows = fetch_rows_where(conn, &spec, None)?;
220            input.push((spec, rows));
221        }
222        Ok(SqliteAdapter {
223            model: RelationalModel::build(input),
224        })
225    }
226
227    /// A human-readable locator: `/table/key` for rows.
228    pub fn locator(&self, node: NodeId) -> String {
229        self.model.locator(node)
230    }
231}
232
233/// Execute pushed-down SQL directly (read-only): the column names
234/// and rows, ordered by `order_table`'s key when one is given (the
235/// pushdown contract: row order must match the adapter's document
236/// order, which is that key).
237/// Whether `cols` covers the primary key or a non-partial UNIQUE
238/// index of `table` — the soundness condition for executing a
239/// witness-JOIN pushdown (see `raw_query`).
240fn unique_key(conn: &Connection, table: &str, cols: &[String]) -> Result<bool, SqliteError> {
241    use std::collections::HashSet;
242    let want: HashSet<&str> = cols.iter().map(|s| s.as_str()).collect();
243    if want.is_empty() {
244        return Ok(false);
245    }
246    // Primary key (pk ordinal > 0 in table_info).
247    let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote_ident(table)))?;
248    let mut pk: HashSet<String> = HashSet::new();
249    let mut rows = stmt.query([])?;
250    while let Some(r) = rows.next()? {
251        let name: String = r.get(1)?;
252        let ord: i64 = r.get(5)?;
253        if ord > 0 {
254            pk.insert(name);
255        }
256    }
257    if !pk.is_empty() && pk.iter().all(|c| want.contains(c.as_str())) {
258        return Ok(true);
259    }
260    // Non-partial UNIQUE indexes.
261    let mut stmt = conn.prepare(&format!("PRAGMA index_list({})", quote_ident(table)))?;
262    let mut uniques: Vec<String> = Vec::new();
263    let mut rows = stmt.query([])?;
264    while let Some(r) = rows.next()? {
265        let name: String = r.get(1)?;
266        let is_unique: i64 = r.get(2)?;
267        let partial: i64 = r.get(4).unwrap_or(0);
268        if is_unique == 1 && partial == 0 {
269            uniques.push(name);
270        }
271    }
272    for idx in uniques {
273        let mut stmt = conn.prepare(&format!("PRAGMA index_info({})", quote_ident(&idx)))?;
274        let mut cols_of: Vec<String> = Vec::new();
275        let mut rows = stmt.query([])?;
276        while let Some(r) = rows.next()? {
277            cols_of.push(r.get(2)?);
278        }
279        if !cols_of.is_empty() && cols_of.iter().all(|c| want.contains(c.as_str())) {
280            return Ok(true);
281        }
282    }
283    Ok(false)
284}
285
286pub fn raw_query(
287    path: &std::path::Path,
288    sql: &str,
289    order_table: Option<&str>,
290    join_left: Option<(&str, &[String])>,
291) -> Result<(Vec<String>, Vec<Vec<Value>>), SqliteError> {
292    let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
293    // A witness-JOIN plan is only sound when the ON binds the left
294    // table by a unique key (else SQL multiplies rows where
295    // Quarb's existential binding does not). Verify against the
296    // catalog; refusing sends the caller back to the scan.
297    if let Some((table, cols)) = join_left
298        && !unique_key(&conn, table, cols)?
299    {
300        return Err(SqliteError::Plan(format!(
301            "join ON does not bind {table} by a unique key; \
302             the SQL JOIN would multiply rows"
303        )));
304    }
305    let sql = match order_table {
306        Some(t) => {
307            let key = specs(&conn)?
308                .into_iter()
309                .find(|s| s.name == t)
310                .and_then(|s| s.pk.map(|i| s.columns[i].clone()))
311                .unwrap_or_else(|| "rowid".to_string());
312            format!("{sql} ORDER BY {t}.{key}")
313        }
314        None => sql.to_string(),
315    };
316    let mut stmt = conn.prepare(&sql)?;
317    let cols: Vec<String> = stmt.column_names().iter().map(|c| c.to_string()).collect();
318    let n = cols.len();
319    let mut rows = stmt.query([])?;
320    let mut out = Vec::new();
321    while let Some(r) = rows.next()? {
322        out.push(
323            (0..n)
324                .map(|i| to_value(r.get_ref(i).expect("column in range")))
325                .collect(),
326        );
327    }
328    Ok((cols, out))
329}
330
331impl AstAdapter for SqliteAdapter {
332    fn root(&self) -> NodeId {
333        self.model.root()
334    }
335    fn children(&self, node: NodeId) -> Vec<NodeId> {
336        self.model.children(node)
337    }
338    fn name(&self, node: NodeId) -> Option<String> {
339        self.model.name(node)
340    }
341    fn parent(&self, node: NodeId) -> Option<NodeId> {
342        self.model.parent(node)
343    }
344    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
345        self.model.property(node, name)
346    }
347    fn default_value(&self, node: NodeId) -> Option<Value> {
348        self.model.default_value(node)
349    }
350    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
351        self.model.metadata(node, key)
352    }
353    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
354        self.model.resolve(node, property, hint)
355    }
356    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
357        self.model.links(node)
358    }
359    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
360        self.model.backlinks(node)
361    }
362}