use crate::{SqlError, Translation};
use quarb::reflect::QueryArbor;
use quarb::{AstAdapter, NodeId, Value};
pub fn export(quarb: &str) -> Result<Translation, SqlError> {
refuse_marker(quarb)?;
let arbor =
QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
refuse_groups(&arbor)?;
let mut ex = Exporter {
arbor,
notes: Vec::new(),
strict: false,
dialect: None,
from_table: String::new(),
join_on_cols: Vec::new(),
join_table: None,
from_sql: String::new(),
join_sql: None,
in_on: false,
aggregate: false,
};
let query = ex.query()?;
Ok(Translation {
query,
notes: ex.notes,
})
}
fn refuse_marker(quarb: &str) -> Result<(), SqlError> {
for marker in ["__LEFT__", "__AGG__"] {
if quarb.contains(marker) {
return Err(SqlError::Unsupported(format!(
"query text contains the reserved marker \"{marker}\""
)));
}
}
Ok(())
}
pub struct Pushdown {
pub sql: String,
pub order_table: Option<String>,
pub join_left: Option<(String, Vec<String>)>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Dialect {
Postgres,
MySql,
Sqlite,
Mssql,
Oracle,
}
pub fn pushdown(quarb: &str, dialect: Option<Dialect>) -> Option<Pushdown> {
pushdown_explained(quarb, dialect).ok()
}
pub fn pushdown_explained(quarb: &str, dialect: Option<Dialect>) -> Result<Pushdown, SqlError> {
refuse_marker(quarb)?;
let arbor =
QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
refuse_groups(&arbor)?;
let mut ex = Exporter {
arbor,
notes: Vec::new(),
strict: true,
dialect,
from_table: String::new(),
join_on_cols: Vec::new(),
join_table: None,
from_sql: String::new(),
join_sql: None,
in_on: false,
aggregate: false,
};
let sql = ex.query()?;
let order_table = if ex.aggregate {
None
} else {
Some(ex.from_table.clone())
};
let join_left = ex
.join_table
.clone()
.map(|t| (t, ex.join_on_cols.clone()));
Ok(Pushdown {
sql,
order_table,
join_left,
})
}
fn json_extract(dialect: Dialect, qual: Option<&str>, col: &str, path: &[String]) -> String {
let qcol = match qual {
Some(q) => format!("{q}.{col}"),
None => col.to_string(),
};
match dialect {
Dialect::Postgres => format!("({qcol}::jsonb #>> '{{{}}}')", path.join(",")),
Dialect::MySql => {
format!("JSON_UNQUOTE(JSON_EXTRACT({qcol}, '$.{}'))", path.join("."))
}
Dialect::Sqlite => format!("json_extract({qcol}, '$.{}')", path.join(".")),
Dialect::Mssql | Dialect::Oracle => format!("JSON_VALUE({qcol}, '$.{}')", path.join(".")),
}
}
const SQL_KEYWORDS: &[&str] = &[
"all",
"and",
"as",
"asc",
"by",
"case",
"cross",
"desc",
"distinct",
"else",
"end",
"except",
"exists",
"from",
"group",
"having",
"in",
"index",
"inner",
"intersect",
"into",
"is",
"join",
"left",
"like",
"limit",
"not",
"null",
"offset",
"on",
"or",
"order",
"outer",
"right",
"select",
"set",
"table",
"then",
"union",
"unique",
"update",
"using",
"values",
"when",
"where",
];
fn is_plain_ident(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
&& !SQL_KEYWORDS.contains(&name.to_ascii_lowercase().as_str())
}
#[derive(Default)]
struct Select {
select: Vec<String>,
distinct: bool,
from: String,
join: Option<(String, String)>, wheres: Vec<String>,
group_by: Option<String>,
having: Option<String>,
order_by: Option<(String, bool)>, limit: Option<String>,
}
impl Select {
fn render(&self) -> String {
let mut out = String::from("SELECT ");
if self.distinct {
out.push_str("DISTINCT ");
}
if self.select.is_empty() {
out.push('*');
} else {
out.push_str(&self.select.join(", "));
}
out.push_str(&format!(" FROM {}", self.from));
if let Some((t, on)) = &self.join {
out.push_str(&format!(" JOIN {t} ON {on}"));
}
if !self.wheres.is_empty() {
out.push_str(&format!(" WHERE {}", self.wheres.join(" AND ")));
}
if let Some(g) = &self.group_by {
out.push_str(&format!(" GROUP BY {g}"));
}
if let Some(h) = &self.having {
out.push_str(&format!(" HAVING {h}"));
}
if let Some((e, desc)) = &self.order_by {
out.push_str(&format!(" ORDER BY {e}"));
if *desc {
out.push_str(" DESC");
}
}
if let Some(n) = &self.limit {
out.push_str(&format!(" LIMIT {n}"));
}
out
}
}
pub struct Partial {
pub table: String,
pub where_sql: String,
}
pub fn partial_pushdown(quarb: &str) -> Option<Partial> {
partial_pushdown_explained(quarb).ok()
}
pub fn partial_pushdown_explained(quarb: &str) -> Result<Partial, SqlError> {
refuse_marker(quarb)?;
let arbor =
QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
refuse_groups(&arbor)?;
let mut ex = Exporter {
arbor,
notes: Vec::new(),
strict: true,
dialect: None,
from_table: String::new(),
join_on_cols: Vec::new(),
join_table: None,
from_sql: String::new(),
join_sql: None,
in_on: false,
aggregate: false,
};
ex.partial()
}
fn refuse_groups(arbor: &QueryArbor) -> Result<(), SqlError> {
let mut stack = vec![arbor.root()];
while let Some(n) = stack.pop() {
if arbor.name(n).as_deref() == Some("group") {
return Err(SqlError::Unsupported(
"path patterns (groups and quantifiers)".into(),
));
}
stack.extend(arbor.children(n));
}
Ok(())
}
struct Exporter {
arbor: QueryArbor,
notes: Vec<String>,
strict: bool,
dialect: Option<Dialect>,
from_table: String,
join_on_cols: Vec<String>,
join_table: Option<String>,
from_sql: String,
join_sql: Option<String>,
in_on: bool,
aggregate: bool,
}
impl Exporter {
fn kids(&self, n: NodeId, kind: &str) -> Vec<NodeId> {
self.arbor
.children(n)
.into_iter()
.filter(|&c| self.arbor.name(c).as_deref() == Some(kind))
.collect()
}
fn kid(&self, n: NodeId, kind: &str) -> Option<NodeId> {
self.kids(n, kind).into_iter().next()
}
fn prop(&self, n: NodeId, key: &str) -> Option<Value> {
self.arbor.property(n, key)
}
fn prop_s(&self, n: NodeId, key: &str) -> String {
self.prop(n, key).map(|v| v.to_string()).unwrap_or_default()
}
fn kind(&self, n: NodeId) -> String {
self.arbor.name(n).unwrap_or_default()
}
fn is_null_literal(&self, n: NodeId) -> bool {
self.kind(n) == "literal" && self.prop_s(n, "type") == "null"
}
fn partial(&mut self) -> Result<Partial, SqlError> {
let root = self.arbor.root();
let q = self
.kid(root, "query")
.ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
if self.kid(q, "query").is_some() {
return Err(SqlError::Unsupported(
"partial pushdown: correlations address other tables".into(),
));
}
let branches = self.kids(q, "branch");
if branches.len() != 1 {
return Err(SqlError::Unsupported(
"partial pushdown: a branch union".into(),
));
}
let (table, preds) = self.table_branch(branches[0])?;
let all = self.walk_all(root);
let mut table_mentions = 0;
for n in &all {
match self.kind(*n).as_str() {
"step" => {
let axis = self.prop_s(*n, "axis");
if matches!(axis.as_str(), "->" | "<-" | "--" | "-->" | "<--") {
return Err(SqlError::Unsupported(
"partial pushdown: crosslink/resolution axes could reach \
the filtered table"
.into(),
));
}
if self.prop_s(*n, "matcher") == table {
table_mentions += 1;
}
}
"projection" if self.prop_s(*n, "kind") != "property" => {
return Err(SqlError::Unsupported(
"partial pushdown: metadata would observe the filtering \
(;;;n-rows, :::index)"
.into(),
));
}
_ => {}
}
}
if table_mentions != 1 {
return Err(SqlError::Unsupported(
"partial pushdown: the table is reached more than once".into(),
));
}
let mut conds = Vec::new();
for p in preds {
if self.prop_s(p, "kind") != "expr" {
break;
}
conds.push(self.predicate_cond(p, None)?);
}
if conds.is_empty() {
return Err(SqlError::Unsupported(
"partial pushdown: no leading expression predicates to push".into(),
));
}
Ok(Partial {
table,
where_sql: conds.join(" AND "),
})
}
fn walk_all(&self, n: NodeId) -> Vec<NodeId> {
let mut out = vec![n];
let mut i = 0;
while i < out.len() {
out.extend(self.arbor.children(out[i]));
i += 1;
}
out
}
fn query(&mut self) -> Result<String, SqlError> {
let root = self.arbor.root();
let q = self
.kid(root, "query")
.ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
let mut sel = Select::default();
let corrs = self.kids(q, "query");
let branches = self.kids(q, "branch");
if branches.len() != 1 {
return Err(SqlError::Unsupported(
"a branch union (SQL has UNION, but result shapes differ; export one branch)"
.into(),
));
}
if corrs.len() > 1 {
return Err(SqlError::Unsupported(
"more than one correlation context".into(),
));
}
if let Some(corr) = corrs.first() {
let (ltable, lpreds) = self.table_branch(branches[0])?;
let (rtable, rpreds) = self.table_branch(self.kids(*corr, "branch")[0])?;
let lsql = self.sql_ident(<able, "table name")?;
let rsql = self.sql_ident(&rtable, "table name")?;
self.from_sql = lsql.clone();
self.join_sql = Some(rsql.clone());
let driver_conds: Vec<NodeId> = lpreds;
let mut on = Vec::new();
let mut wheres = Vec::new();
for p in rpreds {
self.split_join_pred(p, &rsql, &mut on, &mut wheres)?;
}
if on.is_empty() {
return Err(SqlError::Unsupported(
"a correlation without a '$$' equality (no JOIN condition)".into(),
));
}
for p in driver_conds {
let cond = self.predicate_cond(p, Some(&lsql.clone()))?;
wheres.push(cond);
}
self.notes.push(
"JOIN: Quarb's binding is existential (one row per FROM row, \
bound to its first witness); SQL multiplies rows when \
several joined rows match"
.to_string(),
);
sel.from = lsql.clone();
self.from_table = ltable;
self.join_table = Some(rtable);
sel.join = Some((rsql.clone(), on.join(" AND ")));
sel.wheres = wheres;
if let Some(proj) = self.kid(branches[0], "projection") {
let col = self.projection_col(proj)?;
let col = self.sql_ident(&col, "column name")?;
sel.select.push(format!("{lsql}.{col}"));
}
if self.kid(self.kids(*corr, "branch")[0], "projection").is_some() {
return Err(SqlError::Unsupported(
"a projection on the joined expression (project the witness \
in the pipeline: '$*1::col')"
.into(),
));
}
self.pipeline(q, &mut sel, Some((&lsql, &rsql)))?;
} else {
let (table, preds) = self.table_branch(branches[0])?;
sel.from = self.sql_ident(&table, "table name")?;
self.from_table = table;
for p in preds {
let cond = self.predicate_cond(p, None)?;
sel.wheres.push(cond);
}
if let Some(proj) = self.kid(branches[0], "projection") {
let col = self.projection_col(proj)?;
sel.select.push(self.sql_ident(&col, "column name")?);
}
self.pipeline(q, &mut sel, None)?;
}
Ok(sel.render())
}
fn table_branch(&mut self, b: NodeId) -> Result<(String, Vec<NodeId>), SqlError> {
let steps = self.kids(b, "step");
if steps.len() != 2 {
return Err(SqlError::Unsupported(
"navigation beyond /table/* (SQL sees tables and rows)".into(),
));
}
let (t, rows) = (steps[0], steps[1]);
if self.prop_s(t, "axis") != "/"
|| self.prop_s(t, "matcher-kind") != "name"
|| self.prop_s(rows, "axis") != "/"
|| self.prop_s(rows, "matcher-kind") != "any"
{
return Err(SqlError::Unsupported(
"navigation beyond /table/* (SQL sees tables and rows)".into(),
));
}
if self.kid(t, "predicate").is_some() {
return Err(SqlError::Unsupported("a predicate on the table hop".into()));
}
Ok((self.prop_s(t, "matcher"), self.kids(rows, "predicate")))
}
fn predicate_cond(&mut self, p: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
if self.prop_s(p, "kind") != "expr" {
return Err(SqlError::Unsupported(
"a positional predicate on rows (SQL rows are unordered; ORDER BY + LIMIT)".into(),
));
}
let parts: Vec<String> = self
.arbor
.children(p)
.into_iter()
.map(|c| self.pred_expr(c, qual))
.collect::<Result<_, _>>()?;
Ok(parts.join(" AND "))
}
fn pred_expr(&mut self, e: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
match self.kind(e).as_str() {
"and" | "or" => {
let op = self.kind(e).to_uppercase();
let kids: Vec<String> = self
.arbor
.children(e)
.into_iter()
.map(|c| self.pred_expr(c, qual))
.collect::<Result<_, _>>()?;
Ok(format!("({})", kids.join(&format!(" {op} "))))
}
"not" => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: 'not(...)' drops the NULL rows Quarb keeps \
(SQL NOT propagates UNKNOWN)"
.into(),
));
}
let inner: Vec<String> = self
.arbor
.children(e)
.into_iter()
.map(|c| self.pred_expr(c, qual))
.collect::<Result<_, _>>()?;
Ok(format!("NOT ({})", inner.join(" AND ")))
}
"parens" => {
let inner: Vec<String> = self
.arbor
.children(e)
.into_iter()
.map(|c| self.pred_expr(c, qual))
.collect::<Result<_, _>>()?;
Ok(format!("({})", inner.join(" AND ")))
}
"compare" => {
let op = self.prop_s(e, "op");
let kids = self.arbor.children(e);
if matches!(op.as_str(), "=" | "!=")
&& (self.is_null_literal(kids[0]) || self.is_null_literal(kids[1]))
{
let other = if self.is_null_literal(kids[0]) {
kids[1]
} else {
kids[0]
};
let col = self.operand(other, qual)?;
return Ok(if op == "=" {
format!("{col} IS NULL")
} else {
format!("{col} IS NOT NULL")
});
}
if op == "="
&& let Some(dialect) = self.dialect
{
for (pi, li) in [(0usize, 1usize), (1, 0)] {
if self.is_text_literal(kids[li])
&& let Some((col, path)) = self.json_path(kids[pi])
{
let lit = self.operand(kids[li], qual)?;
let extract = json_extract(dialect, qual, &col, &path);
return Ok(format!("{extract} = {lit}"));
}
}
}
let l = self.operand(kids[0], qual)?;
let r = self.operand(kids[1], qual)?;
Ok(match op.as_str() {
"=" => format!("{l} = {r}"),
"!=" => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: '!=' drops the NULL rows Quarb keeps \
(SQL '<>' is UNKNOWN for NULL; use '!= null' \
for IS NOT NULL)"
.into(),
));
}
self.notes.push(
"'!=' → '<>': Quarb keeps rows whose column is NULL; \
SQL's '<>' drops them (use '!= null' for IS NOT NULL)"
.to_string(),
);
format!("{l} <> {r}")
}
"<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
"*=" => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: LIKE case folding differs per engine".into(),
));
}
if !self.is_text_literal(kids[1]) {
return Err(SqlError::Unsupported(
"'*=' with a non-literal pattern".into(),
));
}
self.notes.push(
"*= → LIKE: Quarb's substring test is case-sensitive; LIKE \
folds case on SQLite/MySQL but not PostgreSQL"
.to_string(),
);
let raw = self
.prop(kids[1], "value")
.map(|v| v.to_string())
.unwrap_or_default();
let pat = raw
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_")
.replace('\'', "''");
format!("{l} LIKE '%{pat}%' ESCAPE '\\'")
}
"=~" | "!~" => {
return Err(SqlError::Unsupported(
"regex matching (REGEXP dialects disagree; use *= or spell \
the SQL by hand)"
.into(),
));
}
other => {
return Err(SqlError::Unsupported(format!("the '{other}' comparison")));
}
})
}
_ => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: truthiness diverges (0 and '' are falsy in Quarb)".into(),
));
}
self.notes.push(
"truthiness: '[::c]' exports as IS NOT NULL, but Quarb also treats \
0 and '' as falsy"
.to_string(),
);
Ok(format!("{} IS NOT NULL", self.operand(e, qual)?))
}
}
}
fn operand(&mut self, o: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
match self.kind(o).as_str() {
"literal" => {
let v = self.prop(o, "value").unwrap_or(Value::Null);
match self.prop_s(o, "type").as_str() {
"text" => {
let s = v.to_string();
if self.strict && (s.contains('\'') || s.contains('\\')) {
return Err(SqlError::Unsupported(
"pushdown: a text literal with a quote or backslash \
has no escaping portable across SQL dialects"
.into(),
));
}
Ok(format!("'{}'", s.replace('\'', "''")))
}
"null" => Ok("NULL".to_string()),
_ => Ok(v.to_string()),
}
}
"path" => {
if self.kid(o, "step").is_some() {
let s = self.kids(o, "step")[0];
if self.prop_s(s, "axis") == "-->" {
return Err(SqlError::Unsupported(
"a '~>' resolution chain: the foreign-key targets live in \
the schema, not the query — spell the join with '<=>' to \
export it"
.into(),
));
}
return Err(SqlError::Unsupported(
"navigation inside a predicate (SQL rows are flat)".into(),
));
}
let p = self
.kid(o, "projection")
.ok_or_else(|| SqlError::Unsupported("an empty path operand".into()))?;
let col = self.projection_col(p)?;
let col = self.sql_ident(&col, "column name")?;
Ok(match qual {
Some(q) => format!("{q}.{col}"),
None => col,
})
}
"context" => {
let p = self
.kid(o, "projection")
.ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
let col = self.projection_col(p)?;
let col = self.sql_ident(&col, "column name")?;
if self.in_on {
return Err(SqlError::Unsupported(
"a '$*' reference inside the ON (the driver is '$$')".into(),
));
}
let Some(join) = self.join_sql.clone() else {
return Err(SqlError::Unsupported(
"a '$*' reference outside a correlation join".into(),
));
};
match self.prop(o, "index") {
Some(Value::Int(1)) => Ok(format!("{join}.{col}")),
None => Err(SqlError::Unsupported("a bare '$*' reference".into())),
Some(v) => Err(SqlError::Unsupported(format!(
"pushdown: $*{v} beyond a two-branch correlation"
))),
}
}
"outer" => {
if !self.in_on {
return Err(SqlError::Unsupported(
"a '$$' reference outside the join's ON".into(),
));
}
let kids = self.arbor.children(o);
let inner = *kids
.first()
.ok_or_else(|| SqlError::Unsupported("an empty '$$' reference".into()))?;
if self.kind(inner) != "path" || self.kid(inner, "step").is_some() {
return Err(SqlError::Unsupported(
"a '$$' reference beyond a plain column ($$::col)".into(),
));
}
let p = self
.kid(inner, "projection")
.ok_or_else(|| SqlError::Unsupported("a bare '$$' reference".into()))?;
let col = self.projection_col(p)?;
let col = self.sql_ident(&col, "column name")?;
Ok(format!("{}.{col}", self.from_sql.clone()))
}
"arith" => {
let op = self.prop_s(o, "op");
let kids = self.arbor.children(o);
let l = self.operand(kids[0], qual)?;
let r = self.operand(kids[1], qual)?;
Ok(match op.as_str() {
"+" | "-" | "*" => format!("({l} {op} {r})"),
"div" => format!("({l} / {r})"),
"mod" => format!("({l} % {r})"),
other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
})
}
other => Err(SqlError::Unsupported(format!(
"the '{other}' operand (registers, topics, and captures are Quarb-side state)"
))),
}
}
fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
match self.prop_s(p, "kind").as_str() {
"property" => match self.prop(p, "key") {
Some(k) => Ok(k.to_string()),
None => Err(SqlError::Unsupported(
"the bare '::' projection (name the column)".into(),
)),
},
other => Err(SqlError::Unsupported(format!(
"the {other} metadata projection"
))),
}
}
fn is_text_literal(&self, o: NodeId) -> bool {
self.kind(o) == "literal" && self.prop_s(o, "type") == "text"
}
fn json_path(&self, o: NodeId) -> Option<(String, Vec<String>)> {
if self.kind(o) != "path" {
return None;
}
let steps = self.kids(o, "step");
if steps.len() < 2 {
return None;
}
let proj = self.kid(o, "projection")?;
if self.prop_s(proj, "kind") != "property" || self.prop(proj, "key").is_some() {
return None;
}
let mut names = Vec::with_capacity(steps.len());
for s in &steps {
if self.prop_s(*s, "axis") != "/"
|| self.prop_s(*s, "matcher-kind") != "name"
|| self.kid(*s, "predicate").is_some()
{
return None;
}
let name = self.prop_s(*s, "matcher");
let plain = !name.is_empty()
&& name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
if !plain {
return None;
}
names.push(name);
}
let col = names.remove(0);
Some((col, names))
}
fn alias(&mut self, name: &str) -> Result<String, SqlError> {
if is_plain_ident(name) {
return Ok(name.to_string());
}
if self.strict {
return Err(SqlError::Unsupported(format!(
"pushdown: field name {name:?} needs SQL quoting \
(dialects disagree); rename the field"
)));
}
self.notes
.push(format!("field name {name:?} double-quoted (ANSI)"));
Ok(format!("\"{}\"", name.replace('"', "\"\"")))
}
fn sql_ident(&mut self, name: &str, what: &str) -> Result<String, SqlError> {
if is_plain_ident(name) {
return Ok(name.to_string());
}
if self.strict {
return Err(SqlError::Unsupported(format!(
"pushdown: {what} {name:?} is not a plain SQL identifier"
)));
}
self.notes
.push(format!("{what} {name:?} double-quoted (ANSI)"));
Ok(format!("\"{}\"", name.replace('"', "\"\"")))
}
fn split_join_pred(
&mut self,
p: NodeId,
rtable: &str,
on: &mut Vec<String>,
wheres: &mut Vec<String>,
) -> Result<(), SqlError> {
if self.prop_s(p, "kind") != "expr" {
return Err(SqlError::Unsupported(
"a positional predicate on the joined side".into(),
));
}
fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
if ex.kind(e) == "and" {
for c in ex.arbor.children(e) {
conjuncts(ex, c, out);
}
} else {
out.push(e);
}
}
let mut parts = Vec::new();
for c in self.arbor.children(p) {
conjuncts(self, c, &mut parts);
}
for e in parts {
let uses_driver = self.subtree_has(e, "outer");
self.in_on = true;
let cond = self.pred_expr(e, Some(rtable));
self.in_on = false;
let cond = cond?;
if uses_driver && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
self.collect_join_cols(e)?;
on.push(cond);
} else {
wheres.push(cond);
}
}
Ok(())
}
fn collect_join_cols(&mut self, e: NodeId) -> Result<(), SqlError> {
if self.kind(e) == "outer" {
return Ok(());
}
if self.kind(e) == "path"
&& self.kid(e, "step").is_none()
&& let Some(p) = self.kid(e, "projection")
{
let col = self.projection_col(p)?;
self.join_on_cols.push(col);
}
for c in self.arbor.children(e) {
self.collect_join_cols(c)?;
}
Ok(())
}
fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
if self.kind(n) == kind {
return true;
}
self.arbor
.children(n)
.into_iter()
.any(|c| self.subtree_has(c, kind))
}
fn pipeline(
&mut self,
q: NodeId,
sel: &mut Select,
join: Option<(&str, &str)>,
) -> Result<(), SqlError> {
let Some(pipe) = self.kid(q, "pipeline") else {
return Ok(());
};
let stages: Vec<NodeId> = self.arbor.children(pipe);
let mut i = 0;
let mut pending_col: Option<String> = None;
let mut grouped: Option<(String, String, String)> = None;
while i < stages.len() {
let s = stages[i];
match self.kind(s).as_str() {
"expr" => {
let kids = self.arbor.children(s);
pending_col = Some(self.operand(kids[0], join.map(|(l, _)| l))?);
}
"func" => {
let name = self.prop_s(s, "name");
match name.as_str() {
"rec" | "record" => {
sel.select = self.record_fields(s, join)?;
}
"count" | "sum" | "mean" | "avg" | "min" | "max" => {
let (key, _, _) = grouped.as_ref().ok_or_else(|| {
SqlError::Unsupported(format!(
"'| {name}' outside a group (use '@| {name}')"
))
})?;
let col = if name == "count" {
pending_col = None;
None
} else {
pending_col.take()
};
let agg = sql_agg(&name, col)?;
grouped = Some((key.clone(), agg.clone(), agg));
}
other => {
return Err(SqlError::Unsupported(format!(
"the '{other}' pipeline function"
)));
}
}
}
"push" => {
if let Some((k, agg, _)) = grouped.take() {
let alias = self.prop_s(s, "name");
grouped = Some((k, agg, alias));
} else {
return Err(SqlError::Unsupported(
"a register push (Quarb-side state)".into(),
));
}
}
"filter" => {
if grouped.is_some() {
let cond = self.having_cond(s)?;
sel.having = Some(cond);
} else {
return Err(SqlError::Unsupported(
"a mid-pipeline filter (put it in the row predicate)".into(),
));
}
}
"recall" => {
if self.prop_s(s, "ref") != "%." {
return Err(SqlError::Unsupported(
"a register recall (Quarb-side state)".into(),
));
}
let (key, agg, alias) = grouped.clone().ok_or_else(|| {
SqlError::Unsupported("'%.', with nothing grouped".into())
})?;
let alias = self.alias(&alias)?;
sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
if let Some(h) = sel.having.take() {
sel.having = Some(h.replace("__AGG__", &agg));
}
}
"agg" => {
let name = self.prop_s(s, "name");
match name.as_str() {
"count" | "sum" | "mean" | "avg" | "min" | "max" => {
let col = if name == "count" {
pending_col = None;
None
} else {
pending_col.take()
};
sel.select = vec![sql_agg(&name, col)?];
self.aggregate = true;
}
"group" => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: GROUP BY result order is unordered in SQL".into(),
));
}
self.notes.push(
"GROUP BY: SQL keeps a NULL-key group; Quarb's group \
drops null keys"
.to_string(),
);
let key = self.group_key(s, join)?;
sel.group_by = Some(key.clone());
grouped = Some((key, String::new(), String::new()));
}
"sort_by" => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: ORDER BY collations differ per engine".into(),
));
}
if sel.limit.is_some() {
return Err(SqlError::Unsupported(
"a sort after a positional selection (SQL orders \
before LIMIT)"
.into(),
));
}
if sel.order_by.is_some() {
return Err(SqlError::Unsupported(
"a second sort (SQL has a single ORDER BY)".into(),
));
}
let kids = self.arbor.children(s);
let e = self.operand(kids[0], join.map(|(l, _)| l))?;
sel.order_by = Some((e, false));
}
"reverse" => match &mut sel.order_by {
Some((_, desc)) => *desc = true,
None => {
return Err(SqlError::Unsupported(
"reverse without an ORDER BY (rows are unordered)".into(),
));
}
},
"top" => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: ORDER BY collations differ per engine".into(),
));
}
if sel.limit.is_some() {
return Err(SqlError::Unsupported(
"'top' after a positional selection (SQL orders \
before LIMIT)"
.into(),
));
}
if sel.order_by.is_some() {
return Err(SqlError::Unsupported(
"a second sort (SQL has a single ORDER BY)".into(),
));
}
let kids = self.arbor.children(s);
let n = self.prop_s(kids[0], "value");
let e = self.operand(kids[1], join.map(|(l, _)| l))?;
sel.order_by = Some((e, true));
sel.limit = Some(n);
}
"unique" => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: DISTINCT result order is unordered in SQL".into(),
));
}
if sel.limit.is_some() {
return Err(SqlError::Unsupported(
"'unique' after a positional selection (SQL applies \
DISTINCT before LIMIT)"
.into(),
));
}
if let Some(c) = pending_col.take() {
sel.select = vec![c];
}
sel.distinct = true;
}
other => {
return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
}
}
}
"select" => {
if self.strict {
return Err(SqlError::Unsupported(
"pushdown: LIMIT without a guaranteed order".into(),
));
}
if sel.limit.is_some() {
return Err(SqlError::Unsupported(
"a second positional selection".into(),
));
}
let p = self.arbor.children(s)[0];
match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
("range", Some(Value::Int(n)))
if self.prop(p, "from").is_none() && n > 0 =>
{
sel.limit = Some(n.to_string());
}
_ => {
return Err(SqlError::Unsupported(
"positional selection beyond '@| [..n]'".into(),
));
}
}
}
other => {
return Err(SqlError::Unsupported(format!(
"the '{other}' stage (windows, subcontexts, and registers are \
Quarb-side state)"
)));
}
}
i += 1;
}
if let Some(c) = pending_col
&& sel.select.is_empty()
{
sel.select = vec![c];
}
Ok(())
}
fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
let kids = self.arbor.children(s);
let key = kids
.iter()
.rev()
.find(|&&k| self.kind(k) != "literal")
.ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
self.operand(*key, join.map(|(l, _)| l))
}
fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
let kids = self.arbor.children(s);
if kids.len() != 1 || self.kind(kids[0]) != "compare" {
return Err(SqlError::Unsupported(
"HAVING translates for a single comparison".into(),
));
}
let e = kids[0];
let op = self.prop_s(e, "op");
let cmp_kids = self.arbor.children(e);
let l = match self.kind(cmp_kids[0]).as_str() {
"topic" | "recall" => "__AGG__".to_string(),
_ => {
return Err(SqlError::Unsupported(
"HAVING compares the aggregate ($_ or its register)".into(),
));
}
};
let r = self.operand(cmp_kids[1], None)?;
Ok(format!("{l} {op} {r}"))
}
fn record_fields(
&mut self,
s: NodeId,
join: Option<(&str, &str)>,
) -> Result<Vec<String>, SqlError> {
let kids = self.arbor.children(s);
let mut fields = Vec::new();
let mut i = 0;
while i < kids.len() {
let k = kids[i];
if self.kind(k) == "literal" {
let name = self.prop_s(k, "value");
let value = self.operand(kids[i + 1], join.map(|(l, _)| l))?;
let name = self.alias(&name)?;
fields.push(format!("{value} AS {name}"));
i += 2;
} else {
let value = self.operand(k, join.map(|(l, _)| l))?;
fields.push(value);
i += 1;
}
}
Ok(fields)
}
}
fn sql_agg(name: &str, col: Option<String>) -> Result<String, SqlError> {
let f = match name {
"count" => "COUNT",
"sum" => "SUM",
"mean" | "avg" => "AVG",
"min" => "MIN",
"max" => "MAX",
_ => unreachable!("checked by caller"),
};
match col {
Some(c) => Ok(format!("{f}({c})")),
None if f == "COUNT" => Ok("COUNT(*)".to_string()),
None => Err(SqlError::Unsupported(format!(
"'{name}' over row nodes (project a column first: '| ::col @| {name}')"
))),
}
}
#[cfg(test)]
mod null_and_literal_tests {
use super::{Dialect, export, partial_pushdown, pushdown};
const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
#[test]
fn json_column_path_pushdown_per_dialect() {
let q = "/orders/*[/data/meta/tier:: = 'gold']::id";
let sql = |d| pushdown(q, Some(d)).unwrap().sql;
assert_eq!(
sql(Dialect::Postgres),
"SELECT id FROM orders WHERE (data::jsonb #>> '{meta,tier}') = 'gold'"
);
assert_eq!(
sql(Dialect::MySql),
"SELECT id FROM orders WHERE JSON_UNQUOTE(JSON_EXTRACT(data, '$.meta.tier')) = 'gold'"
);
assert_eq!(
sql(Dialect::Sqlite),
"SELECT id FROM orders WHERE json_extract(data, '$.meta.tier') = 'gold'"
);
assert_eq!(
sql(Dialect::Mssql),
"SELECT id FROM orders WHERE JSON_VALUE(data, '$.meta.tier') = 'gold'"
);
assert_eq!(
sql(Dialect::Oracle),
"SELECT id FROM orders WHERE JSON_VALUE(data, '$.meta.tier') = 'gold'"
);
assert!(pushdown(q, None).is_none());
}
#[test]
fn json_pushdown_only_string_equality() {
let d = Some(Dialect::Sqlite);
assert!(pushdown("/o/*[/data/n:: > 2]::id", d).is_none());
assert!(pushdown("/o/*[/data/tier:: != 'gold']::id", d).is_none());
assert!(pushdown("/o/*[/data/items/*/sku:: = 'A1']::id", d).is_none());
}
#[test]
fn null_literal_compares_use_is_null() {
assert_eq!(
export("/t/*[::x = null] | ::x").unwrap().query,
"SELECT x FROM t WHERE x IS NULL"
);
assert_eq!(
export("/t/*[::x != null] | ::x").unwrap().query,
"SELECT x FROM t WHERE x IS NOT NULL"
);
assert_eq!(
pushdown("/t/*[::x = null] | ::x", None).unwrap().sql,
"SELECT x FROM t WHERE x IS NULL"
);
assert_eq!(
pushdown("/t/*[::x != null] | ::x", None).unwrap().sql,
"SELECT x FROM t WHERE x IS NOT NULL"
);
}
#[test]
fn ne_and_not_refuse_pushdown_but_display_diverges() {
assert!(pushdown("/t/*[::x != 5] | ::x", None).is_none());
assert!(pushdown("/t/*[!::x = 5] | ::x", None).is_none());
assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}")).is_none());
assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}")).is_none());
let t = export("/t/*[::x != 5] | ::x").unwrap();
assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
assert!(t.notes.iter().any(|n| n.contains("NULL")));
}
#[test]
fn unescapable_text_literal_refuses_pushdown() {
assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path", None).is_none());
assert!(pushdown("/t/*[::name = \"it's\"] | ::name", None).is_none());
assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}")).is_none());
assert_eq!(
pushdown("/t/*[::name = \"rare\"] | ::name", None).unwrap().sql,
"SELECT name FROM t WHERE name = 'rare'"
);
}
}