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 and view's spec, via
52/// PRAGMAs. Views are containers like tables — a single-column view
53/// (the `SELECT DISTINCT` dimension idiom) is named by that column,
54/// the way a single-column primary key names a table's rows.
55/// `refs` is the declared-references document: each `(field,
56/// container)` pair synthesizes a foreign-key edge from every
57/// matching column (bare `column`, or `table.column` to scope) to
58/// the target container's key, unless the schema already declares
59/// one there.
60fn specs(conn: &Connection, refs: &[(String, String)]) -> Result<Vec<TableSpec>, SqliteError> {
61    let named: Vec<(String, bool)> = conn
62        .prepare(
63            "SELECT name, type = 'view' FROM sqlite_master \
64             WHERE type IN ('table', 'view') \
65             AND name NOT LIKE 'sqlite_%' ORDER BY name",
66        )?
67        .query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?
68        .collect::<Result<_, _>>()?;
69
70    let mut out = Vec::new();
71    for (name, is_view) in named {
72        // Columns and the primary key (a single-column pk names the
73        // rows; a composite or absent pk falls back to rowid).
74        let mut columns = Vec::new();
75        let mut pk_cols: Vec<(i64, usize)> = Vec::new();
76        {
77            let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote_ident(&name)))?;
78            let mut rows = stmt.query([])?;
79            while let Some(r) = rows.next()? {
80                let col: String = r.get(1)?;
81                let pk: i64 = r.get(5)?;
82                if pk > 0 {
83                    pk_cols.push((pk, columns.len()));
84                }
85                columns.push(col);
86            }
87        }
88        pk_cols.sort();
89        // A single-column primary key names the rows. A view has no
90        // primary key ever; when it projects exactly one column —
91        // the `SELECT DISTINCT` dimension idiom — that column is
92        // the row's identity, so it names the rows (and keys `-->`
93        // resolution into the view).
94        let pk = (pk_cols.len() == 1)
95            .then(|| pk_cols[0].1)
96            .or_else(|| (is_view && columns.len() == 1).then_some(0));
97
98        // Declared foreign keys (an omitted target column means the
99        // target's primary key).
100        let mut fks = Vec::new();
101        {
102            let mut stmt =
103                conn.prepare(&format!("PRAGMA foreign_key_list({})", quote_ident(&name)))?;
104            let mut rows = stmt.query([])?;
105            while let Some(r) = rows.next()? {
106                let target: String = r.get(2)?;
107                let from: String = r.get(3)?;
108                let to: Option<String> = r.get(4)?;
109                if let Some(idx) = columns.iter().position(|c| *c == from) {
110                    fks.push((idx, target, to.unwrap_or_default()));
111                }
112            }
113        }
114        // Declared client-side references: synthesize an edge to the
115        // target container's key for every matching column the
116        // schema does not already cover.
117        for (field, container) in refs {
118            let col = match field.split_once('.') {
119                Some((table, col)) if table == name => col,
120                Some(_) => continue,
121                None => field.as_str(),
122            };
123            if let Some(idx) = columns.iter().position(|c| c == col)
124                && !fks.iter().any(|(i, _, _)| *i == idx)
125                // A container's key column pointing at the container
126                // itself is the identity, not an edge.
127                && !(*container == name && pk == Some(idx))
128            {
129                fks.push((idx, container.clone(), String::new()));
130            }
131        }
132        out.push(TableSpec {
133            name,
134            columns,
135            pk,
136            fks,
137        });
138    }
139    Ok(out)
140}
141
142/// Stream one table's rows, optionally filtered (partial
143/// pushdown: the engine re-applies the pushed predicates).
144fn fetch_rows_where(
145    conn: &Connection,
146    spec: &TableSpec,
147    where_sql: Option<&str>,
148) -> Result<Vec<RowSpec>, SqliteError> {
149    let cols = spec
150        .columns
151        .iter()
152        .map(|c| quote_ident(c))
153        .collect::<Vec<_>>()
154        .join(", ");
155    let table = quote_ident(&spec.name);
156    let filter = match where_sql {
157        Some(w) => format!(" WHERE {w}"),
158        None => String::new(),
159    };
160    // Prefer SQLite's rowid: stable row identity, and the row's edge
161    // name for tables without a single-column primary key. WITHOUT
162    // ROWID tables have no rowid column, so `SELECT rowid` fails to
163    // prepare; fall back to a positional ordinal (below), ordered by
164    // the key for determinism.
165    let with_rowid = format!("SELECT rowid, {cols} FROM {table}{filter} ORDER BY rowid");
166    if let Ok(mut stmt) = conn.prepare(&with_rowid) {
167        let mut rows = stmt.query([])?;
168        let mut out = Vec::new();
169        while let Some(r) = rows.next()? {
170            let rowid: i64 = r.get(0)?;
171            let values: Vec<Value> = (0..spec.columns.len())
172                .map(|i| to_value(r.get_ref(i + 1).expect("column in range")))
173                .collect();
174            out.push(RowSpec { rowid, values });
175        }
176        return Ok(out);
177    }
178    // WITHOUT ROWID: no rowid column to select or order by. Order by
179    // the single-column primary key (which names the rows) when
180    // present, else by every column so the ordinal keys are stable.
181    let order = match spec.pk {
182        Some(i) => quote_ident(&spec.columns[i]),
183        None => cols.clone(),
184    };
185    let mut stmt = conn.prepare(&format!(
186        "SELECT {cols} FROM {table}{filter} ORDER BY {order}"
187    ))?;
188    let mut rows = stmt.query([])?;
189    let mut out = Vec::new();
190    let mut rowid = 0i64;
191    while let Some(r) = rows.next()? {
192        rowid += 1;
193        let values: Vec<Value> = (0..spec.columns.len())
194            .map(|i| to_value(r.get_ref(i).expect("column in range")))
195            .collect();
196        out.push(RowSpec { rowid, values });
197    }
198    Ok(out)
199}
200
201impl SqliteAdapter {
202    /// Open a database file (read-only): the catalog now, each
203    /// table's rows on first touch (the connection stays owned by
204    /// the adapter).
205    pub fn open(path: &std::path::Path) -> Result<Self, SqliteError> {
206        Self::open_impl(path, None, &[])
207    }
208
209    /// [`open`], with a declared-references document: each `(field,
210    /// container)` pair supplies the edge the schema does not
211    /// declare (see [`quarb_relational::parse_refs`]).
212    pub fn open_with_refs(
213        path: &std::path::Path,
214        refs: &[(String, String)],
215    ) -> Result<Self, SqliteError> {
216        Self::open_impl(path, None, refs)
217    }
218
219    /// [`open`], with one table's fetch filtered by a WHERE clause
220    /// (partial pushdown; the engine re-applies the predicates).
221    pub fn open_filtered(
222        path: &std::path::Path,
223        table: &str,
224        where_sql: &str,
225    ) -> Result<Self, SqliteError> {
226        Self::open_impl(path, Some((table.to_string(), where_sql.to_string())), &[])
227    }
228
229    /// [`open_filtered`], with a declared-references document.
230    pub fn open_filtered_with_refs(
231        path: &std::path::Path,
232        table: &str,
233        where_sql: &str,
234        refs: &[(String, String)],
235    ) -> Result<Self, SqliteError> {
236        Self::open_impl(path, Some((table.to_string(), where_sql.to_string())), refs)
237    }
238
239    fn open_impl(
240        path: &std::path::Path,
241        filter: Option<(String, String)>,
242        refs: &[(String, String)],
243    ) -> Result<Self, SqliteError> {
244        let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
245        let specs = specs(&conn, refs)?;
246        let model = RelationalModel::lazy(
247            specs,
248            Box::new(move |_, spec| {
249                let w = filter
250                    .as_ref()
251                    .filter(|(t, _)| *t == spec.name)
252                    .map(|(_, w)| w.as_str());
253                fetch_rows_where(&conn, spec, w).map_err(|e| e.to_string())
254            }),
255        );
256        Ok(SqliteAdapter { model })
257    }
258
259    /// Open a database held entirely in memory — the bytes of a
260    /// `.db` file that never touched a filesystem (the browser's
261    /// uploaded files). Deserializes read-only, then materializes
262    /// eagerly, as [`SqliteAdapter::load`] does.
263    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SqliteError> {
264        let mut conn = Connection::open_in_memory()?;
265        conn.deserialize_read_exact(rusqlite::MAIN_DB, bytes, bytes.len(), true)?;
266        Self::load(&conn)
267    }
268
269    /// Materialize every user table of an open connection, eagerly
270    /// (in-memory databases, tests).
271    pub fn load(conn: &Connection) -> Result<Self, SqliteError> {
272        Self::load_with_refs(conn, &[])
273    }
274
275    /// [`load`], with a declared-references document.
276    pub fn load_with_refs(
277        conn: &Connection,
278        refs: &[(String, String)],
279    ) -> Result<Self, SqliteError> {
280        let mut input = Vec::new();
281        for spec in specs(conn, refs)? {
282            let rows = fetch_rows_where(conn, &spec, None)?;
283            input.push((spec, rows));
284        }
285        Ok(SqliteAdapter {
286            model: RelationalModel::build(input),
287        })
288    }
289
290    /// A human-readable locator: `/table/key` for rows.
291    pub fn locator(&self, node: NodeId) -> String {
292        self.model.locator(node)
293    }
294}
295
296/// Execute pushed-down SQL directly (read-only): the column names
297/// and rows, ordered by `order_table`'s key when one is given (the
298/// pushdown contract: row order must match the adapter's document
299/// order, which is that key).
300/// Whether `cols` covers the primary key or a non-partial UNIQUE
301/// index of `table` — the soundness condition for executing a
302/// witness-JOIN pushdown (see `raw_query`).
303fn unique_key(conn: &Connection, table: &str, cols: &[String]) -> Result<bool, SqliteError> {
304    use std::collections::HashSet;
305    let want: HashSet<&str> = cols.iter().map(|s| s.as_str()).collect();
306    if want.is_empty() {
307        return Ok(false);
308    }
309    // Primary key (pk ordinal > 0 in table_info).
310    let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote_ident(table)))?;
311    let mut pk: HashSet<String> = HashSet::new();
312    let mut rows = stmt.query([])?;
313    while let Some(r) = rows.next()? {
314        let name: String = r.get(1)?;
315        let ord: i64 = r.get(5)?;
316        if ord > 0 {
317            pk.insert(name);
318        }
319    }
320    if !pk.is_empty() && pk.iter().all(|c| want.contains(c.as_str())) {
321        return Ok(true);
322    }
323    // Non-partial UNIQUE indexes.
324    let mut stmt = conn.prepare(&format!("PRAGMA index_list({})", quote_ident(table)))?;
325    let mut uniques: Vec<String> = Vec::new();
326    let mut rows = stmt.query([])?;
327    while let Some(r) = rows.next()? {
328        let name: String = r.get(1)?;
329        let is_unique: i64 = r.get(2)?;
330        let partial: i64 = r.get(4).unwrap_or(0);
331        if is_unique == 1 && partial == 0 {
332            uniques.push(name);
333        }
334    }
335    for idx in uniques {
336        let mut stmt = conn.prepare(&format!("PRAGMA index_info({})", quote_ident(&idx)))?;
337        let mut cols_of: Vec<String> = Vec::new();
338        let mut rows = stmt.query([])?;
339        while let Some(r) = rows.next()? {
340            cols_of.push(r.get(2)?);
341        }
342        if !cols_of.is_empty() && cols_of.iter().all(|c| want.contains(c.as_str())) {
343            return Ok(true);
344        }
345    }
346    Ok(false)
347}
348
349pub fn raw_query(
350    path: &std::path::Path,
351    sql: &str,
352    order_table: Option<&str>,
353    join_left: Option<(&str, &[String])>,
354) -> Result<(Vec<String>, Vec<Vec<Value>>), SqliteError> {
355    let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
356    // A witness-JOIN plan is only sound when the ON binds the left
357    // table by a unique key (else SQL multiplies rows where
358    // Quarb's existential binding does not). Verify against the
359    // catalog; refusing sends the caller back to the scan.
360    if let Some((table, cols)) = join_left
361        && !unique_key(&conn, table, cols)?
362    {
363        return Err(SqliteError::Plan(format!(
364            "join ON does not bind {table} by a unique key; \
365             the SQL JOIN would multiply rows"
366        )));
367    }
368    let sql = match order_table {
369        Some(t) => {
370            let key = specs(&conn, &[])?
371                .into_iter()
372                .find(|s| s.name == t)
373                .and_then(|s| s.pk.map(|i| s.columns[i].clone()))
374                .unwrap_or_else(|| "rowid".to_string());
375            format!("{sql} ORDER BY {t}.{key}")
376        }
377        None => sql.to_string(),
378    };
379    let mut stmt = conn.prepare(&sql)?;
380    let cols: Vec<String> = stmt.column_names().iter().map(|c| c.to_string()).collect();
381    let n = cols.len();
382    let mut rows = stmt.query([])?;
383    let mut out = Vec::new();
384    while let Some(r) = rows.next()? {
385        out.push(
386            (0..n)
387                .map(|i| to_value(r.get_ref(i).expect("column in range")))
388                .collect(),
389        );
390    }
391    Ok((cols, out))
392}
393
394impl AstAdapter for SqliteAdapter {
395    fn root(&self) -> NodeId {
396        self.model.root()
397    }
398    fn children(&self, node: NodeId) -> Vec<NodeId> {
399        self.model.children(node)
400    }
401    fn name(&self, node: NodeId) -> Option<String> {
402        self.model.name(node)
403    }
404    fn parent(&self, node: NodeId) -> Option<NodeId> {
405        self.model.parent(node)
406    }
407    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
408        self.model.property(node, name)
409    }
410    fn default_value(&self, node: NodeId) -> Option<Value> {
411        self.model.default_value(node)
412    }
413    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
414        self.model.metadata(node, key)
415    }
416    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
417        self.model.resolve(node, property, hint)
418    }
419    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
420        self.model.links(node)
421    }
422    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
423        self.model.backlinks(node)
424    }
425}