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}
26
27/// A SQLite database, materialized as an arbor.
28pub struct SqliteAdapter {
29    model: RelationalModel,
30}
31
32fn to_value(v: ValueRef<'_>) -> Value {
33    match v {
34        ValueRef::Null => Value::Null,
35        ValueRef::Integer(n) => Value::Int(n),
36        ValueRef::Real(f) => Value::Float(f),
37        ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
38        ValueRef::Blob(b) => Value::Str(format!("<blob {} bytes>", b.len())),
39    }
40}
41
42/// Quote an identifier (table or column name) for interpolation into
43/// SQL: wrap in double quotes, doubling any embedded quote, so
44/// reserved words and special characters survive.
45fn quote_ident(name: &str) -> String {
46    format!("\"{}\"", name.replace('"', "\"\""))
47}
48
49/// Introspect the schema: every user table's spec, via PRAGMAs.
50fn specs(conn: &Connection) -> Result<Vec<TableSpec>, SqliteError> {
51    let names: Vec<String> = conn
52        .prepare(
53            "SELECT name FROM sqlite_master WHERE type = 'table' \
54             AND name NOT LIKE 'sqlite_%' ORDER BY name",
55        )?
56        .query_map([], |r| r.get(0))?
57        .collect::<Result<_, _>>()?;
58
59    let mut out = Vec::new();
60    for name in names {
61        // Columns and the primary key (a single-column pk names the
62        // rows; a composite or absent pk falls back to rowid).
63        let mut columns = Vec::new();
64        let mut pk_cols: Vec<(i64, usize)> = Vec::new();
65        {
66            let mut stmt =
67                conn.prepare(&format!("PRAGMA table_info({})", quote_ident(&name)))?;
68            let mut rows = stmt.query([])?;
69            while let Some(r) = rows.next()? {
70                let col: String = r.get(1)?;
71                let pk: i64 = r.get(5)?;
72                if pk > 0 {
73                    pk_cols.push((pk, columns.len()));
74                }
75                columns.push(col);
76            }
77        }
78        pk_cols.sort();
79        let pk = (pk_cols.len() == 1).then(|| pk_cols[0].1);
80
81        // Declared foreign keys (an omitted target column means the
82        // target's primary key).
83        let mut fks = Vec::new();
84        {
85            let mut stmt =
86                conn.prepare(&format!("PRAGMA foreign_key_list({})", quote_ident(&name)))?;
87            let mut rows = stmt.query([])?;
88            while let Some(r) = rows.next()? {
89                let target: String = r.get(2)?;
90                let from: String = r.get(3)?;
91                let to: Option<String> = r.get(4)?;
92                if let Some(idx) = columns.iter().position(|c| *c == from) {
93                    fks.push((idx, target, to.unwrap_or_default()));
94                }
95            }
96        }
97        out.push(TableSpec {
98            name,
99            columns,
100            pk,
101            fks,
102        });
103    }
104    Ok(out)
105}
106
107/// Stream one table's rows, optionally filtered (partial
108/// pushdown: the engine re-applies the pushed predicates).
109fn fetch_rows_where(
110    conn: &Connection,
111    spec: &TableSpec,
112    where_sql: Option<&str>,
113) -> Result<Vec<RowSpec>, SqliteError> {
114    let cols = spec
115        .columns
116        .iter()
117        .map(|c| quote_ident(c))
118        .collect::<Vec<_>>()
119        .join(", ");
120    let table = quote_ident(&spec.name);
121    let filter = match where_sql {
122        Some(w) => format!(" WHERE {w}"),
123        None => String::new(),
124    };
125    // Prefer SQLite's rowid: stable row identity, and the row's edge
126    // name for tables without a single-column primary key. WITHOUT
127    // ROWID tables have no rowid column, so `SELECT rowid` fails to
128    // prepare; fall back to a positional ordinal (below), ordered by
129    // the key for determinism.
130    let with_rowid = format!("SELECT rowid, {cols} FROM {table}{filter} ORDER BY rowid");
131    if let Ok(mut stmt) = conn.prepare(&with_rowid) {
132        let mut rows = stmt.query([])?;
133        let mut out = Vec::new();
134        while let Some(r) = rows.next()? {
135            let rowid: i64 = r.get(0)?;
136            let values: Vec<Value> = (0..spec.columns.len())
137                .map(|i| to_value(r.get_ref(i + 1).expect("column in range")))
138                .collect();
139            out.push(RowSpec { rowid, values });
140        }
141        return Ok(out);
142    }
143    // WITHOUT ROWID: no rowid column to select or order by. Order by
144    // the single-column primary key (which names the rows) when
145    // present, else by every column so the ordinal keys are stable.
146    let order = match spec.pk {
147        Some(i) => quote_ident(&spec.columns[i]),
148        None => cols.clone(),
149    };
150    let mut stmt = conn.prepare(&format!("SELECT {cols} FROM {table}{filter} ORDER BY {order}"))?;
151    let mut rows = stmt.query([])?;
152    let mut out = Vec::new();
153    let mut rowid = 0i64;
154    while let Some(r) = rows.next()? {
155        rowid += 1;
156        let values: Vec<Value> = (0..spec.columns.len())
157            .map(|i| to_value(r.get_ref(i).expect("column in range")))
158            .collect();
159        out.push(RowSpec { rowid, values });
160    }
161    Ok(out)
162}
163
164impl SqliteAdapter {
165    /// Open a database file (read-only): the catalog now, each
166    /// table's rows on first touch (the connection stays owned by
167    /// the adapter).
168    pub fn open(path: &std::path::Path) -> Result<Self, SqliteError> {
169        Self::open_impl(path, None)
170    }
171
172    /// [`open`], with one table's fetch filtered by a WHERE clause
173    /// (partial pushdown; the engine re-applies the predicates).
174    pub fn open_filtered(
175        path: &std::path::Path,
176        table: &str,
177        where_sql: &str,
178    ) -> Result<Self, SqliteError> {
179        Self::open_impl(path, Some((table.to_string(), where_sql.to_string())))
180    }
181
182    fn open_impl(
183        path: &std::path::Path,
184        filter: Option<(String, String)>,
185    ) -> Result<Self, SqliteError> {
186        let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
187        let specs = specs(&conn)?;
188        let model = RelationalModel::lazy(
189            specs,
190            Box::new(move |_, spec| {
191                let w = filter
192                    .as_ref()
193                    .filter(|(t, _)| *t == spec.name)
194                    .map(|(_, w)| w.as_str());
195                fetch_rows_where(&conn, spec, w).map_err(|e| e.to_string())
196            }),
197        );
198        Ok(SqliteAdapter { model })
199    }
200
201    /// Materialize every user table of an open connection, eagerly
202    /// (in-memory databases, tests).
203    pub fn load(conn: &Connection) -> Result<Self, SqliteError> {
204        let mut input = Vec::new();
205        for spec in specs(conn)? {
206            let rows = fetch_rows_where(conn, &spec, None)?;
207            input.push((spec, rows));
208        }
209        Ok(SqliteAdapter {
210            model: RelationalModel::build(input),
211        })
212    }
213
214    /// A human-readable locator: `/table/key` for rows.
215    pub fn locator(&self, node: NodeId) -> String {
216        self.model.locator(node)
217    }
218}
219
220/// Execute pushed-down SQL directly (read-only): the column names
221/// and rows, ordered by `order_table`'s key when one is given (the
222/// pushdown contract: row order must match the adapter's document
223/// order, which is that key).
224pub fn raw_query(
225    path: &std::path::Path,
226    sql: &str,
227    order_table: Option<&str>,
228) -> Result<(Vec<String>, Vec<Vec<Value>>), SqliteError> {
229    let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
230    let sql = match order_table {
231        Some(t) => {
232            let key = specs(&conn)?
233                .into_iter()
234                .find(|s| s.name == t)
235                .and_then(|s| s.pk.map(|i| s.columns[i].clone()))
236                .unwrap_or_else(|| "rowid".to_string());
237            format!("{sql} ORDER BY {t}.{key}")
238        }
239        None => sql.to_string(),
240    };
241    let mut stmt = conn.prepare(&sql)?;
242    let cols: Vec<String> = stmt.column_names().iter().map(|c| c.to_string()).collect();
243    let n = cols.len();
244    let mut rows = stmt.query([])?;
245    let mut out = Vec::new();
246    while let Some(r) = rows.next()? {
247        out.push(
248            (0..n)
249                .map(|i| to_value(r.get_ref(i).expect("column in range")))
250                .collect(),
251        );
252    }
253    Ok((cols, out))
254}
255
256impl AstAdapter for SqliteAdapter {
257    fn root(&self) -> NodeId {
258        self.model.root()
259    }
260    fn children(&self, node: NodeId) -> Vec<NodeId> {
261        self.model.children(node)
262    }
263    fn name(&self, node: NodeId) -> Option<String> {
264        self.model.name(node)
265    }
266    fn parent(&self, node: NodeId) -> Option<NodeId> {
267        self.model.parent(node)
268    }
269    fn property(&self, node: NodeId, name: &str) -> Option<Value> {
270        self.model.property(node, name)
271    }
272    fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
273        self.model.metadata(node, key)
274    }
275    fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
276        self.model.resolve(node, property, hint)
277    }
278    fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
279        self.model.links(node)
280    }
281    fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
282        self.model.backlinks(node)
283    }
284}