use quarb::{AstAdapter, NodeId, Value};
use quarb_relational::{RelationalModel, RowSpec, TableSpec};
use rusqlite::Connection;
use rusqlite::types::ValueRef;
#[derive(Debug, thiserror::Error)]
pub enum SqliteError {
#[error("sqlite: {0}")]
Sqlite(#[from] rusqlite::Error),
#[error("pushdown plan: {0}")]
Plan(String),
}
pub struct SqliteAdapter {
model: RelationalModel,
}
fn to_value(v: ValueRef<'_>) -> Value {
match v {
ValueRef::Null => Value::Null,
ValueRef::Integer(n) => Value::Int(n),
ValueRef::Real(f) => Value::Float(f),
ValueRef::Text(t) => Value::Str(String::from_utf8_lossy(t).into_owned()),
ValueRef::Blob(b) => Value::Str(format!("<blob {} bytes>", b.len())),
}
}
fn quote_ident(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
fn specs(conn: &Connection) -> Result<Vec<TableSpec>, SqliteError> {
let names: Vec<String> = conn
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' \
AND name NOT LIKE 'sqlite_%' ORDER BY name",
)?
.query_map([], |r| r.get(0))?
.collect::<Result<_, _>>()?;
let mut out = Vec::new();
for name in names {
let mut columns = Vec::new();
let mut pk_cols: Vec<(i64, usize)> = Vec::new();
{
let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote_ident(&name)))?;
let mut rows = stmt.query([])?;
while let Some(r) = rows.next()? {
let col: String = r.get(1)?;
let pk: i64 = r.get(5)?;
if pk > 0 {
pk_cols.push((pk, columns.len()));
}
columns.push(col);
}
}
pk_cols.sort();
let pk = (pk_cols.len() == 1).then(|| pk_cols[0].1);
let mut fks = Vec::new();
{
let mut stmt =
conn.prepare(&format!("PRAGMA foreign_key_list({})", quote_ident(&name)))?;
let mut rows = stmt.query([])?;
while let Some(r) = rows.next()? {
let target: String = r.get(2)?;
let from: String = r.get(3)?;
let to: Option<String> = r.get(4)?;
if let Some(idx) = columns.iter().position(|c| *c == from) {
fks.push((idx, target, to.unwrap_or_default()));
}
}
}
out.push(TableSpec {
name,
columns,
pk,
fks,
});
}
Ok(out)
}
fn fetch_rows_where(
conn: &Connection,
spec: &TableSpec,
where_sql: Option<&str>,
) -> Result<Vec<RowSpec>, SqliteError> {
let cols = spec
.columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ");
let table = quote_ident(&spec.name);
let filter = match where_sql {
Some(w) => format!(" WHERE {w}"),
None => String::new(),
};
let with_rowid = format!("SELECT rowid, {cols} FROM {table}{filter} ORDER BY rowid");
if let Ok(mut stmt) = conn.prepare(&with_rowid) {
let mut rows = stmt.query([])?;
let mut out = Vec::new();
while let Some(r) = rows.next()? {
let rowid: i64 = r.get(0)?;
let values: Vec<Value> = (0..spec.columns.len())
.map(|i| to_value(r.get_ref(i + 1).expect("column in range")))
.collect();
out.push(RowSpec { rowid, values });
}
return Ok(out);
}
let order = match spec.pk {
Some(i) => quote_ident(&spec.columns[i]),
None => cols.clone(),
};
let mut stmt = conn.prepare(&format!(
"SELECT {cols} FROM {table}{filter} ORDER BY {order}"
))?;
let mut rows = stmt.query([])?;
let mut out = Vec::new();
let mut rowid = 0i64;
while let Some(r) = rows.next()? {
rowid += 1;
let values: Vec<Value> = (0..spec.columns.len())
.map(|i| to_value(r.get_ref(i).expect("column in range")))
.collect();
out.push(RowSpec { rowid, values });
}
Ok(out)
}
impl SqliteAdapter {
pub fn open(path: &std::path::Path) -> Result<Self, SqliteError> {
Self::open_impl(path, None)
}
pub fn open_filtered(
path: &std::path::Path,
table: &str,
where_sql: &str,
) -> Result<Self, SqliteError> {
Self::open_impl(path, Some((table.to_string(), where_sql.to_string())))
}
fn open_impl(
path: &std::path::Path,
filter: Option<(String, String)>,
) -> Result<Self, SqliteError> {
let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
let specs = specs(&conn)?;
let model = RelationalModel::lazy(
specs,
Box::new(move |_, spec| {
let w = filter
.as_ref()
.filter(|(t, _)| *t == spec.name)
.map(|(_, w)| w.as_str());
fetch_rows_where(&conn, spec, w).map_err(|e| e.to_string())
}),
);
Ok(SqliteAdapter { model })
}
pub fn load(conn: &Connection) -> Result<Self, SqliteError> {
let mut input = Vec::new();
for spec in specs(conn)? {
let rows = fetch_rows_where(conn, &spec, None)?;
input.push((spec, rows));
}
Ok(SqliteAdapter {
model: RelationalModel::build(input),
})
}
pub fn locator(&self, node: NodeId) -> String {
self.model.locator(node)
}
}
fn unique_key(conn: &Connection, table: &str, cols: &[String]) -> Result<bool, SqliteError> {
use std::collections::HashSet;
let want: HashSet<&str> = cols.iter().map(|s| s.as_str()).collect();
if want.is_empty() {
return Ok(false);
}
let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote_ident(table)))?;
let mut pk: HashSet<String> = HashSet::new();
let mut rows = stmt.query([])?;
while let Some(r) = rows.next()? {
let name: String = r.get(1)?;
let ord: i64 = r.get(5)?;
if ord > 0 {
pk.insert(name);
}
}
if !pk.is_empty() && pk.iter().all(|c| want.contains(c.as_str())) {
return Ok(true);
}
let mut stmt = conn.prepare(&format!("PRAGMA index_list({})", quote_ident(table)))?;
let mut uniques: Vec<String> = Vec::new();
let mut rows = stmt.query([])?;
while let Some(r) = rows.next()? {
let name: String = r.get(1)?;
let is_unique: i64 = r.get(2)?;
let partial: i64 = r.get(4).unwrap_or(0);
if is_unique == 1 && partial == 0 {
uniques.push(name);
}
}
for idx in uniques {
let mut stmt = conn.prepare(&format!("PRAGMA index_info({})", quote_ident(&idx)))?;
let mut cols_of: Vec<String> = Vec::new();
let mut rows = stmt.query([])?;
while let Some(r) = rows.next()? {
cols_of.push(r.get(2)?);
}
if !cols_of.is_empty() && cols_of.iter().all(|c| want.contains(c.as_str())) {
return Ok(true);
}
}
Ok(false)
}
pub fn raw_query(
path: &std::path::Path,
sql: &str,
order_table: Option<&str>,
join_left: Option<(&str, &[String])>,
) -> Result<(Vec<String>, Vec<Vec<Value>>), SqliteError> {
let conn = Connection::open_with_flags(path, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?;
if let Some((table, cols)) = join_left
&& !unique_key(&conn, table, cols)?
{
return Err(SqliteError::Plan(format!(
"join ON does not bind {table} by a unique key; \
the SQL JOIN would multiply rows"
)));
}
let sql = match order_table {
Some(t) => {
let key = specs(&conn)?
.into_iter()
.find(|s| s.name == t)
.and_then(|s| s.pk.map(|i| s.columns[i].clone()))
.unwrap_or_else(|| "rowid".to_string());
format!("{sql} ORDER BY {t}.{key}")
}
None => sql.to_string(),
};
let mut stmt = conn.prepare(&sql)?;
let cols: Vec<String> = stmt.column_names().iter().map(|c| c.to_string()).collect();
let n = cols.len();
let mut rows = stmt.query([])?;
let mut out = Vec::new();
while let Some(r) = rows.next()? {
out.push(
(0..n)
.map(|i| to_value(r.get_ref(i).expect("column in range")))
.collect(),
);
}
Ok((cols, out))
}
impl AstAdapter for SqliteAdapter {
fn root(&self) -> NodeId {
self.model.root()
}
fn children(&self, node: NodeId) -> Vec<NodeId> {
self.model.children(node)
}
fn name(&self, node: NodeId) -> Option<String> {
self.model.name(node)
}
fn parent(&self, node: NodeId) -> Option<NodeId> {
self.model.parent(node)
}
fn property(&self, node: NodeId, name: &str) -> Option<Value> {
self.model.property(node, name)
}
fn default_value(&self, node: NodeId) -> Option<Value> {
self.model.default_value(node)
}
fn metadata(&self, node: NodeId, key: &str) -> Option<Value> {
self.model.metadata(node, key)
}
fn resolve(&self, node: NodeId, property: &str, hint: Option<&str>) -> Option<NodeId> {
self.model.resolve(node, property, hint)
}
fn links(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.model.links(node)
}
fn backlinks(&self, node: NodeId) -> Vec<(String, NodeId)> {
self.model.backlinks(node)
}
}