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    /// Materialize every user table of an open connection, eagerly
205    /// (in-memory databases, tests).
206    pub fn load(conn: &Connection) -> Result<Self, SqliteError> {
207        let mut input = Vec::new();
208        for spec in specs(conn)? {
209            let rows = fetch_rows_where(conn, &spec, None)?;
210            input.push((spec, rows));
211        }
212        Ok(SqliteAdapter {
213            model: RelationalModel::build(input),
214        })
215    }
216
217    /// A human-readable locator: `/table/key` for rows.
218    pub fn locator(&self, node: NodeId) -> String {
219        self.model.locator(node)
220    }
221}
222
223/// Execute pushed-down SQL directly (read-only): the column names
224/// and rows, ordered by `order_table`'s key when one is given (the
225/// pushdown contract: row order must match the adapter's document
226/// order, which is that key).
227/// Whether `cols` covers the primary key or a non-partial UNIQUE
228/// index of `table` — the soundness condition for executing a
229/// witness-JOIN pushdown (see `raw_query`).
230fn unique_key(conn: &Connection, table: &str, cols: &[String]) -> Result<bool, SqliteError> {
231    use std::collections::HashSet;
232    let want: HashSet<&str> = cols.iter().map(|s| s.as_str()).collect();
233    if want.is_empty() {
234        return Ok(false);
235    }
236    // Primary key (pk ordinal > 0 in table_info).
237    let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote_ident(table)))?;
238    let mut pk: HashSet<String> = HashSet::new();
239    let mut rows = stmt.query([])?;
240    while let Some(r) = rows.next()? {
241        let name: String = r.get(1)?;
242        let ord: i64 = r.get(5)?;
243        if ord > 0 {
244            pk.insert(name);
245        }
246    }
247    if !pk.is_empty() && pk.iter().all(|c| want.contains(c.as_str())) {
248        return Ok(true);
249    }
250    // Non-partial UNIQUE indexes.
251    let mut stmt = conn.prepare(&format!("PRAGMA index_list({})", quote_ident(table)))?;
252    let mut uniques: Vec<String> = Vec::new();
253    let mut rows = stmt.query([])?;
254    while let Some(r) = rows.next()? {
255        let name: String = r.get(1)?;
256        let is_unique: i64 = r.get(2)?;
257        let partial: i64 = r.get(4).unwrap_or(0);
258        if is_unique == 1 && partial == 0 {
259            uniques.push(name);
260        }
261    }
262    for idx in uniques {
263        let mut stmt = conn.prepare(&format!("PRAGMA index_info({})", quote_ident(&idx)))?;
264        let mut cols_of: Vec<String> = Vec::new();
265        let mut rows = stmt.query([])?;
266        while let Some(r) = rows.next()? {
267            cols_of.push(r.get(2)?);
268        }
269        if !cols_of.is_empty() && cols_of.iter().all(|c| want.contains(c.as_str())) {
270            return Ok(true);
271        }
272    }
273    Ok(false)
274}
275
276pub fn raw_query(
277    path: &std::path::Path,
278    sql: &str,
279    order_table: Option<&str>,
280    join_left: Option<(&str, &[String])>,
281) -> Result<(Vec<String>, Vec<Vec<Value>>), SqliteError> {
282    let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
283    // A witness-JOIN plan is only sound when the ON binds the left
284    // table by a unique key (else SQL multiplies rows where
285    // Quarb's existential binding does not). Verify against the
286    // catalog; refusing sends the caller back to the scan.
287    if let Some((table, cols)) = join_left
288        && !unique_key(&conn, table, cols)?
289    {
290        return Err(SqliteError::Plan(format!(
291            "join ON does not bind {table} by a unique key; \
292             the SQL JOIN would multiply rows"
293        )));
294    }
295    let sql = match order_table {
296        Some(t) => {
297            let key = specs(&conn)?
298                .into_iter()
299                .find(|s| s.name == t)
300                .and_then(|s| s.pk.map(|i| s.columns[i].clone()))
301                .unwrap_or_else(|| "rowid".to_string());
302            format!("{sql} ORDER BY {t}.{key}")
303        }
304        None => sql.to_string(),
305    };
306    let mut stmt = conn.prepare(&sql)?;
307    let cols: Vec<String> = stmt.column_names().iter().map(|c| c.to_string()).collect();
308    let n = cols.len();
309    let mut rows = stmt.query([])?;
310    let mut out = Vec::new();
311    while let Some(r) = rows.next()? {
312        out.push(
313            (0..n)
314                .map(|i| to_value(r.get_ref(i).expect("column in range")))
315                .collect(),
316        );
317    }
318    Ok((cols, out))
319}
320
321impl AstAdapter for SqliteAdapter {
322    fn root(&self) -> NodeId {
323        self.model.root()
324    }
325    fn children(&self, node: NodeId) -> Vec<NodeId> {
326        self.model.children(node)
327    }
328    fn name(&self, node: NodeId) -> Option<String> {
329        self.model.name(node)
330    }
331    fn parent(&self, node: NodeId) -> Option<NodeId> {
332        self.model.parent(node)
333    }
334    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
335        self.model.property(node, name)
336    }
337    fn default_value(&self, node: NodeId) -> Option<Value> {
338        self.model.default_value(node)
339    }
340    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
341        self.model.metadata(node, key)
342    }
343    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
344        self.model.resolve(node, property, hint)
345    }
346    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
347        self.model.links(node)
348    }
349    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
350        self.model.backlinks(node)
351    }
352}