1use crate::{SqlError, Translation};
32use quarb::reflect::QueryArbor;
33use quarb::{AstAdapter, NodeId, Value};
34
35pub fn export(quarb: &str) -> Result<Translation, SqlError> {
37 refuse_marker(quarb)?;
38 let arbor =
39 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
40 refuse_groups(&arbor)?;
41 let mut ex = Exporter {
42 arbor,
43 notes: Vec::new(),
44 strict: false,
45 dialect: None,
46 from_table: String::new(),
47 join_on_cols: Vec::new(),
48 join_table: None,
49 from_sql: String::new(),
50 join_sql: None,
51 in_on: false,
52 aggregate: false,
53 };
54 let query = ex.query()?;
55 Ok(Translation {
56 query,
57 notes: ex.notes,
58 })
59}
60
61fn refuse_marker(quarb: &str) -> Result<(), SqlError> {
67 for marker in ["__LEFT__", "__AGG__"] {
68 if quarb.contains(marker) {
69 return Err(SqlError::Unsupported(format!(
70 "query text contains the reserved marker \"{marker}\""
71 )));
72 }
73 }
74 Ok(())
75}
76
77pub struct Pushdown {
83 pub sql: String,
84 pub order_table: Option<String>,
85 pub join_left: Option<(String, Vec<String>)>,
96}
97
98#[derive(Clone, Copy, PartialEq, Eq, Debug)]
104pub enum Dialect {
105 Postgres,
106 MySql,
107 Sqlite,
108 Mssql,
109 Oracle,
110}
111
112pub fn pushdown(quarb: &str, dialect: Option<Dialect>) -> Option<Pushdown> {
121 pushdown_explained(quarb, dialect).ok()
122}
123
124pub fn pushdown_explained(quarb: &str, dialect: Option<Dialect>) -> Result<Pushdown, SqlError> {
127 refuse_marker(quarb)?;
128 let arbor =
129 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
130 refuse_groups(&arbor)?;
131 let mut ex = Exporter {
132 arbor,
133 notes: Vec::new(),
134 strict: true,
135 dialect,
136 from_table: String::new(),
137 join_on_cols: Vec::new(),
138 join_table: None,
139 from_sql: String::new(),
140 join_sql: None,
141 in_on: false,
142 aggregate: false,
143 };
144 let sql = ex.query()?;
145 let order_table = if ex.aggregate {
146 None
147 } else {
148 Some(ex.from_table.clone())
151 };
152 let join_left = ex
153 .join_table
154 .clone()
155 .map(|t| (t, ex.join_on_cols.clone()));
156 Ok(Pushdown {
157 sql,
158 order_table,
159 join_left,
160 })
161}
162
163fn json_extract(dialect: Dialect, qual: Option<&str>, col: &str, path: &[String]) -> String {
171 let qcol = match qual {
172 Some(q) => format!("{q}.{col}"),
173 None => col.to_string(),
174 };
175 match dialect {
176 Dialect::Postgres => format!("({qcol}::jsonb #>> '{{{}}}')", path.join(",")),
181 Dialect::MySql => {
185 format!("JSON_UNQUOTE(JSON_EXTRACT({qcol}, '$.{}'))", path.join("."))
186 }
187 Dialect::Sqlite => format!("json_extract({qcol}, '$.{}')", path.join(".")),
188 Dialect::Mssql | Dialect::Oracle => format!("JSON_VALUE({qcol}, '$.{}')", path.join(".")),
191 }
192}
193
194const SQL_KEYWORDS: &[&str] = &[
199 "all",
200 "and",
201 "as",
202 "asc",
203 "by",
204 "case",
205 "cross",
206 "desc",
207 "distinct",
208 "else",
209 "end",
210 "except",
211 "exists",
212 "from",
213 "group",
214 "having",
215 "in",
216 "index",
217 "inner",
218 "intersect",
219 "into",
220 "is",
221 "join",
222 "left",
223 "like",
224 "limit",
225 "not",
226 "null",
227 "offset",
228 "on",
229 "or",
230 "order",
231 "outer",
232 "right",
233 "select",
234 "set",
235 "table",
236 "then",
237 "union",
238 "unique",
239 "update",
240 "using",
241 "values",
242 "when",
243 "where",
244];
245
246fn is_plain_ident(name: &str) -> bool {
250 !name.is_empty()
251 && name
252 .chars()
253 .next()
254 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
255 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
256 && !SQL_KEYWORDS.contains(&name.to_ascii_lowercase().as_str())
257}
258
259#[derive(Default)]
261struct Select {
262 select: Vec<String>,
263 distinct: bool,
264 from: String,
265 join: Option<(String, String)>, wheres: Vec<String>,
267 group_by: Option<String>,
268 having: Option<String>,
269 order_by: Option<(String, bool)>, limit: Option<String>,
271}
272
273impl Select {
274 fn render(&self) -> String {
275 let mut out = String::from("SELECT ");
276 if self.distinct {
277 out.push_str("DISTINCT ");
278 }
279 if self.select.is_empty() {
280 out.push('*');
281 } else {
282 out.push_str(&self.select.join(", "));
283 }
284 out.push_str(&format!(" FROM {}", self.from));
285 if let Some((t, on)) = &self.join {
286 out.push_str(&format!(" JOIN {t} ON {on}"));
287 }
288 if !self.wheres.is_empty() {
289 out.push_str(&format!(" WHERE {}", self.wheres.join(" AND ")));
290 }
291 if let Some(g) = &self.group_by {
292 out.push_str(&format!(" GROUP BY {g}"));
293 }
294 if let Some(h) = &self.having {
295 out.push_str(&format!(" HAVING {h}"));
296 }
297 if let Some((e, desc)) = &self.order_by {
298 out.push_str(&format!(" ORDER BY {e}"));
299 if *desc {
300 out.push_str(" DESC");
301 }
302 }
303 if let Some(n) = &self.limit {
304 out.push_str(&format!(" LIMIT {n}"));
305 }
306 out
307 }
308}
309
310pub struct Partial {
315 pub table: String,
316 pub where_sql: String,
317}
318
319pub fn partial_pushdown(quarb: &str) -> Option<Partial> {
333 partial_pushdown_explained(quarb).ok()
334}
335
336pub fn partial_pushdown_explained(quarb: &str) -> Result<Partial, SqlError> {
338 refuse_marker(quarb)?;
339 let arbor =
340 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
341 refuse_groups(&arbor)?;
342 let mut ex = Exporter {
343 arbor,
344 notes: Vec::new(),
345 strict: true,
346 dialect: None,
347 from_table: String::new(),
348 join_on_cols: Vec::new(),
349 join_table: None,
350 from_sql: String::new(),
351 join_sql: None,
352 in_on: false,
353 aggregate: false,
354 };
355 ex.partial()
356}
357
358fn refuse_groups(arbor: &QueryArbor) -> Result<(), SqlError> {
364 let mut stack = vec![arbor.root()];
365 while let Some(n) = stack.pop() {
366 if arbor.name(n).as_deref() == Some("group") {
367 return Err(SqlError::Unsupported(
368 "path patterns (groups and quantifiers)".into(),
369 ));
370 }
371 stack.extend(arbor.children(n));
372 }
373 Ok(())
374}
375
376struct Exporter {
377 arbor: QueryArbor,
378 notes: Vec<String>,
379 strict: bool,
383 dialect: Option<Dialect>,
386 from_table: String,
387 join_on_cols: Vec<String>,
388 join_table: Option<String>,
389 from_sql: String,
390 join_sql: Option<String>,
391 in_on: bool,
392 aggregate: bool,
393}
394
395impl Exporter {
396 fn kids(&self, n: NodeId, kind: &str) -> Vec<NodeId> {
397 self.arbor
398 .children(n)
399 .into_iter()
400 .filter(|&c| self.arbor.name(c).as_deref() == Some(kind))
401 .collect()
402 }
403
404 fn kid(&self, n: NodeId, kind: &str) -> Option<NodeId> {
405 self.kids(n, kind).into_iter().next()
406 }
407
408 fn prop(&self, n: NodeId, key: &str) -> Option<Value> {
409 self.arbor.property(n, key)
410 }
411
412 fn prop_s(&self, n: NodeId, key: &str) -> String {
413 self.prop(n, key).map(|v| v.to_string()).unwrap_or_default()
414 }
415
416 fn kind(&self, n: NodeId) -> String {
417 self.arbor.name(n).unwrap_or_default()
418 }
419
420 fn is_null_literal(&self, n: NodeId) -> bool {
422 self.kind(n) == "literal" && self.prop_s(n, "type") == "null"
423 }
424
425 fn partial(&mut self) -> Result<Partial, SqlError> {
427 let root = self.arbor.root();
428 let q = self
429 .kid(root, "query")
430 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
431 if self.kid(q, "query").is_some() {
432 return Err(SqlError::Unsupported(
433 "partial pushdown: correlations address other tables".into(),
434 ));
435 }
436 let branches = self.kids(q, "branch");
437 if branches.len() != 1 {
438 return Err(SqlError::Unsupported(
439 "partial pushdown: a branch union".into(),
440 ));
441 }
442 let (table, preds) = self.table_branch(branches[0])?;
443
444 let all = self.walk_all(root);
446 let mut table_mentions = 0;
447 for n in &all {
448 match self.kind(*n).as_str() {
449 "step" => {
450 let axis = self.prop_s(*n, "axis");
451 if matches!(axis.as_str(), "->" | "<-" | "~>" | "<~") {
452 return Err(SqlError::Unsupported(
453 "partial pushdown: crosslink/resolution axes could reach \
454 the filtered table"
455 .into(),
456 ));
457 }
458 if self.prop_s(*n, "matcher") == table {
459 table_mentions += 1;
460 }
461 }
462 "projection" if self.prop_s(*n, "kind") != "property" => {
463 return Err(SqlError::Unsupported(
464 "partial pushdown: metadata would observe the filtering \
465 (;;;n-rows, :::index)"
466 .into(),
467 ));
468 }
469 _ => {}
470 }
471 }
472 if table_mentions != 1 {
473 return Err(SqlError::Unsupported(
474 "partial pushdown: the table is reached more than once".into(),
475 ));
476 }
477
478 let mut conds = Vec::new();
481 for p in preds {
482 if self.prop_s(p, "kind") != "expr" {
483 break;
484 }
485 conds.push(self.predicate_cond(p, None)?);
486 }
487 if conds.is_empty() {
488 return Err(SqlError::Unsupported(
489 "partial pushdown: no leading expression predicates to push".into(),
490 ));
491 }
492 Ok(Partial {
493 table,
494 where_sql: conds.join(" AND "),
495 })
496 }
497
498 fn walk_all(&self, n: NodeId) -> Vec<NodeId> {
499 let mut out = vec![n];
500 let mut i = 0;
501 while i < out.len() {
502 out.extend(self.arbor.children(out[i]));
503 i += 1;
504 }
505 out
506 }
507
508 fn query(&mut self) -> Result<String, SqlError> {
509 let root = self.arbor.root();
510 let q = self
511 .kid(root, "query")
512 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
513 let mut sel = Select::default();
514
515 let corrs = self.kids(q, "query");
517 let branches = self.kids(q, "branch");
518 if branches.len() != 1 {
519 return Err(SqlError::Unsupported(
520 "a branch union (SQL has UNION, but result shapes differ; export one branch)"
521 .into(),
522 ));
523 }
524
525 if corrs.len() > 1 {
526 return Err(SqlError::Unsupported(
527 "more than one correlation context".into(),
528 ));
529 }
530 if let Some(corr) = corrs.first() {
531 let (ltable, lpreds) = self.table_branch(branches[0])?;
535 let (rtable, rpreds) = self.table_branch(self.kids(*corr, "branch")[0])?;
536 let lsql = self.sql_ident(<able, "table name")?;
540 let rsql = self.sql_ident(&rtable, "table name")?;
541 self.from_sql = lsql.clone();
542 self.join_sql = Some(rsql.clone());
543 let driver_conds: Vec<NodeId> = lpreds;
545 let mut on = Vec::new();
548 let mut wheres = Vec::new();
549 for p in rpreds {
550 self.split_join_pred(p, &rsql, &mut on, &mut wheres)?;
551 }
552 if on.is_empty() {
553 return Err(SqlError::Unsupported(
554 "a correlation without a '$$' equality (no JOIN condition)".into(),
555 ));
556 }
557 for p in driver_conds {
558 let cond = self.predicate_cond(p, Some(&lsql.clone()))?;
559 wheres.push(cond);
560 }
561 self.notes.push(
562 "JOIN: Quarb's binding is existential (one row per FROM row, \
563 bound to its first witness); SQL multiplies rows when \
564 several joined rows match"
565 .to_string(),
566 );
567 sel.from = lsql.clone();
568 self.from_table = ltable;
569 self.join_table = Some(rtable);
570 sel.join = Some((rsql.clone(), on.join(" AND ")));
571 sel.wheres = wheres;
572 if let Some(proj) = self.kid(branches[0], "projection") {
575 let col = self.projection_col(proj)?;
576 let col = self.sql_ident(&col, "column name")?;
577 sel.select.push(format!("{lsql}.{col}"));
578 }
579 if self.kid(self.kids(*corr, "branch")[0], "projection").is_some() {
580 return Err(SqlError::Unsupported(
581 "a projection on the joined expression (project the witness \
582 in the pipeline: '$*1::col')"
583 .into(),
584 ));
585 }
586 self.pipeline(q, &mut sel, Some((&lsql, &rsql)))?;
587 } else {
588 let (table, preds) = self.table_branch(branches[0])?;
589 sel.from = self.sql_ident(&table, "table name")?;
590 self.from_table = table;
591 for p in preds {
592 let cond = self.predicate_cond(p, None)?;
593 sel.wheres.push(cond);
594 }
595 if let Some(proj) = self.kid(branches[0], "projection") {
597 let col = self.projection_col(proj)?;
598 sel.select.push(self.sql_ident(&col, "column name")?);
599 }
600 self.pipeline(q, &mut sel, None)?;
601 }
602 Ok(sel.render())
603 }
604
605 fn table_branch(&mut self, b: NodeId) -> Result<(String, Vec<NodeId>), SqlError> {
608 let steps = self.kids(b, "step");
609 if steps.len() != 2 {
610 return Err(SqlError::Unsupported(
611 "navigation beyond /table/* (SQL sees tables and rows)".into(),
612 ));
613 }
614 let (t, rows) = (steps[0], steps[1]);
615 if self.prop_s(t, "axis") != "/"
616 || self.prop_s(t, "matcher-kind") != "name"
617 || self.prop_s(rows, "axis") != "/"
618 || self.prop_s(rows, "matcher-kind") != "any"
619 {
620 return Err(SqlError::Unsupported(
621 "navigation beyond /table/* (SQL sees tables and rows)".into(),
622 ));
623 }
624 if self.kid(t, "predicate").is_some() {
625 return Err(SqlError::Unsupported("a predicate on the table hop".into()));
626 }
627 Ok((self.prop_s(t, "matcher"), self.kids(rows, "predicate")))
628 }
629
630 fn predicate_cond(&mut self, p: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
633 if self.prop_s(p, "kind") != "expr" {
634 return Err(SqlError::Unsupported(
635 "a positional predicate on rows (SQL rows are unordered; ORDER BY + LIMIT)".into(),
636 ));
637 }
638 let parts: Vec<String> = self
639 .arbor
640 .children(p)
641 .into_iter()
642 .map(|c| self.pred_expr(c, qual))
643 .collect::<Result<_, _>>()?;
644 Ok(parts.join(" AND "))
645 }
646
647 fn pred_expr(&mut self, e: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
648 match self.kind(e).as_str() {
649 "and" | "or" => {
650 let op = self.kind(e).to_uppercase();
651 let kids: Vec<String> = self
652 .arbor
653 .children(e)
654 .into_iter()
655 .map(|c| self.pred_expr(c, qual))
656 .collect::<Result<_, _>>()?;
657 Ok(format!("({})", kids.join(&format!(" {op} "))))
658 }
659 "not" => {
660 if self.strict {
666 return Err(SqlError::Unsupported(
667 "pushdown: 'not(...)' drops the NULL rows Quarb keeps \
668 (SQL NOT propagates UNKNOWN)"
669 .into(),
670 ));
671 }
672 let inner: Vec<String> = self
673 .arbor
674 .children(e)
675 .into_iter()
676 .map(|c| self.pred_expr(c, qual))
677 .collect::<Result<_, _>>()?;
678 Ok(format!("NOT ({})", inner.join(" AND ")))
679 }
680 "parens" => {
681 let inner: Vec<String> = self
682 .arbor
683 .children(e)
684 .into_iter()
685 .map(|c| self.pred_expr(c, qual))
686 .collect::<Result<_, _>>()?;
687 Ok(format!("({})", inner.join(" AND ")))
688 }
689 "compare" => {
690 let op = self.prop_s(e, "op");
691 let kids = self.arbor.children(e);
692 if matches!(op.as_str(), "=" | "!=")
701 && (self.is_null_literal(kids[0]) || self.is_null_literal(kids[1]))
702 {
703 let other = if self.is_null_literal(kids[0]) {
704 kids[1]
705 } else {
706 kids[0]
707 };
708 let col = self.operand(other, qual)?;
709 return Ok(if op == "=" {
710 format!("{col} IS NULL")
711 } else {
712 format!("{col} IS NOT NULL")
713 });
714 }
715 if op == "="
728 && let Some(dialect) = self.dialect
729 {
730 for (pi, li) in [(0usize, 1usize), (1, 0)] {
731 if self.is_text_literal(kids[li])
732 && let Some((col, path)) = self.json_path(kids[pi])
733 {
734 let lit = self.operand(kids[li], qual)?;
735 let extract = json_extract(dialect, qual, &col, &path);
736 return Ok(format!("{extract} = {lit}"));
737 }
738 }
739 }
740 let l = self.operand(kids[0], qual)?;
741 let r = self.operand(kids[1], qual)?;
742 Ok(match op.as_str() {
743 "=" => format!("{l} = {r}"),
744 "!=" => {
745 if self.strict {
753 return Err(SqlError::Unsupported(
754 "pushdown: '!=' drops the NULL rows Quarb keeps \
755 (SQL '<>' is UNKNOWN for NULL; use '!= null' \
756 for IS NOT NULL)"
757 .into(),
758 ));
759 }
760 self.notes.push(
761 "'!=' → '<>': Quarb keeps rows whose column is NULL; \
762 SQL's '<>' drops them (use '!= null' for IS NOT NULL)"
763 .to_string(),
764 );
765 format!("{l} <> {r}")
766 }
767 "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
768 "*=" => {
769 if self.strict {
770 return Err(SqlError::Unsupported(
771 "pushdown: LIKE case folding differs per engine".into(),
772 ));
773 }
774 if !self.is_text_literal(kids[1]) {
779 return Err(SqlError::Unsupported(
780 "'*=' with a non-literal pattern".into(),
781 ));
782 }
783 self.notes.push(
784 "*= → LIKE: Quarb's substring test is case-sensitive; LIKE \
785 folds case on SQLite/MySQL but not PostgreSQL"
786 .to_string(),
787 );
788 let raw = self
793 .prop(kids[1], "value")
794 .map(|v| v.to_string())
795 .unwrap_or_default();
796 let pat = raw
797 .replace('\\', "\\\\")
798 .replace('%', "\\%")
799 .replace('_', "\\_")
800 .replace('\'', "''");
801 format!("{l} LIKE '%{pat}%' ESCAPE '\\'")
802 }
803 "=~" | "!~" => {
804 return Err(SqlError::Unsupported(
805 "regex matching (REGEXP dialects disagree; use *= or spell \
806 the SQL by hand)"
807 .into(),
808 ));
809 }
810 other => {
811 return Err(SqlError::Unsupported(format!("the '{other}' comparison")));
812 }
813 })
814 }
815 _ => {
817 if self.strict {
818 return Err(SqlError::Unsupported(
819 "pushdown: truthiness diverges (0 and '' are falsy in Quarb)".into(),
820 ));
821 }
822 self.notes.push(
823 "truthiness: '[::c]' exports as IS NOT NULL, but Quarb also treats \
824 0 and '' as falsy"
825 .to_string(),
826 );
827 Ok(format!("{} IS NOT NULL", self.operand(e, qual)?))
828 }
829 }
830 }
831
832 fn operand(&mut self, o: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
833 match self.kind(o).as_str() {
834 "literal" => {
835 let v = self.prop(o, "value").unwrap_or(Value::Null);
836 match self.prop_s(o, "type").as_str() {
837 "text" => {
838 let s = v.to_string();
839 if self.strict && (s.contains('\'') || s.contains('\\')) {
850 return Err(SqlError::Unsupported(
851 "pushdown: a text literal with a quote or backslash \
852 has no escaping portable across SQL dialects"
853 .into(),
854 ));
855 }
856 Ok(format!("'{}'", s.replace('\'', "''")))
857 }
858 "null" => Ok("NULL".to_string()),
859 _ => Ok(v.to_string()),
860 }
861 }
862 "path" => {
863 if self.kid(o, "step").is_some() {
864 let s = self.kids(o, "step")[0];
867 if self.prop_s(s, "axis") == "~>" {
868 return Err(SqlError::Unsupported(
869 "a '~>' resolution chain: the foreign-key targets live in \
870 the schema, not the query — spell the join with '<=>' to \
871 export it"
872 .into(),
873 ));
874 }
875 return Err(SqlError::Unsupported(
876 "navigation inside a predicate (SQL rows are flat)".into(),
877 ));
878 }
879 let p = self
880 .kid(o, "projection")
881 .ok_or_else(|| SqlError::Unsupported("an empty path operand".into()))?;
882 let col = self.projection_col(p)?;
883 let col = self.sql_ident(&col, "column name")?;
884 Ok(match qual {
885 Some(q) => format!("{q}.{col}"),
886 None => col,
887 })
888 }
889 "context" => {
890 let p = self
894 .kid(o, "projection")
895 .ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
896 let col = self.projection_col(p)?;
897 let col = self.sql_ident(&col, "column name")?;
898 if self.in_on {
899 return Err(SqlError::Unsupported(
900 "a '$*' reference inside the ON (the driver is '$$')".into(),
901 ));
902 }
903 let Some(join) = self.join_sql.clone() else {
904 return Err(SqlError::Unsupported(
905 "a '$*' reference outside a correlation join".into(),
906 ));
907 };
908 match self.prop(o, "index") {
909 Some(Value::Int(1)) => Ok(format!("{join}.{col}")),
910 None => Err(SqlError::Unsupported("a bare '$*' reference".into())),
911 Some(v) => Err(SqlError::Unsupported(format!(
912 "pushdown: $*{v} beyond a two-branch correlation"
913 ))),
914 }
915 }
916 "outer" => {
917 if !self.in_on {
921 return Err(SqlError::Unsupported(
922 "a '$$' reference outside the join's ON".into(),
923 ));
924 }
925 let kids = self.arbor.children(o);
926 let inner = *kids
927 .first()
928 .ok_or_else(|| SqlError::Unsupported("an empty '$$' reference".into()))?;
929 if self.kind(inner) != "path" || self.kid(inner, "step").is_some() {
930 return Err(SqlError::Unsupported(
931 "a '$$' reference beyond a plain column ($$::col)".into(),
932 ));
933 }
934 let p = self
935 .kid(inner, "projection")
936 .ok_or_else(|| SqlError::Unsupported("a bare '$$' reference".into()))?;
937 let col = self.projection_col(p)?;
938 let col = self.sql_ident(&col, "column name")?;
939 Ok(format!("{}.{col}", self.from_sql.clone()))
940 }
941 "arith" => {
942 let op = self.prop_s(o, "op");
943 let kids = self.arbor.children(o);
944 let l = self.operand(kids[0], qual)?;
945 let r = self.operand(kids[1], qual)?;
946 Ok(match op.as_str() {
947 "+" | "-" | "*" => format!("({l} {op} {r})"),
948 "div" => format!("({l} / {r})"),
949 "mod" => format!("({l} % {r})"),
950 other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
951 })
952 }
953 other => Err(SqlError::Unsupported(format!(
954 "the '{other}' operand (registers, topics, and captures are Quarb-side state)"
955 ))),
956 }
957 }
958
959 fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
960 match self.prop_s(p, "kind").as_str() {
961 "property" => match self.prop(p, "key") {
962 Some(k) => Ok(k.to_string()),
963 None => Err(SqlError::Unsupported(
964 "the bare '::' projection (name the column)".into(),
965 )),
966 },
967 other => Err(SqlError::Unsupported(format!(
968 "the {other} metadata projection"
969 ))),
970 }
971 }
972
973 fn is_text_literal(&self, o: NodeId) -> bool {
975 self.kind(o) == "literal" && self.prop_s(o, "type") == "text"
976 }
977
978 fn json_path(&self, o: NodeId) -> Option<(String, Vec<String>)> {
985 if self.kind(o) != "path" {
986 return None;
987 }
988 let steps = self.kids(o, "step");
989 if steps.len() < 2 {
990 return None;
991 }
992 let proj = self.kid(o, "projection")?;
995 if self.prop_s(proj, "kind") != "property" || self.prop(proj, "key").is_some() {
996 return None;
997 }
998 let mut names = Vec::with_capacity(steps.len());
999 for s in &steps {
1000 if self.prop_s(*s, "axis") != "/"
1001 || self.prop_s(*s, "matcher-kind") != "name"
1002 || self.kid(*s, "predicate").is_some()
1003 {
1004 return None;
1005 }
1006 let name = self.prop_s(*s, "matcher");
1007 let plain = !name.is_empty()
1010 && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
1011 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
1012 if !plain {
1013 return None;
1014 }
1015 names.push(name);
1016 }
1017 let col = names.remove(0);
1018 Some((col, names))
1019 }
1020
1021 fn alias(&mut self, name: &str) -> Result<String, SqlError> {
1025 if is_plain_ident(name) {
1026 return Ok(name.to_string());
1027 }
1028 if self.strict {
1029 return Err(SqlError::Unsupported(format!(
1030 "pushdown: field name {name:?} needs SQL quoting \
1031 (dialects disagree); rename the field"
1032 )));
1033 }
1034 self.notes
1035 .push(format!("field name {name:?} double-quoted (ANSI)"));
1036 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1037 }
1038
1039 fn sql_ident(&mut self, name: &str, what: &str) -> Result<String, SqlError> {
1046 if is_plain_ident(name) {
1047 return Ok(name.to_string());
1048 }
1049 if self.strict {
1050 return Err(SqlError::Unsupported(format!(
1051 "pushdown: {what} {name:?} is not a plain SQL identifier"
1052 )));
1053 }
1054 self.notes
1055 .push(format!("{what} {name:?} double-quoted (ANSI)"));
1056 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1057 }
1058
1059 fn split_join_pred(
1063 &mut self,
1064 p: NodeId,
1065 rtable: &str,
1066 on: &mut Vec<String>,
1067 wheres: &mut Vec<String>,
1068 ) -> Result<(), SqlError> {
1069 if self.prop_s(p, "kind") != "expr" {
1070 return Err(SqlError::Unsupported(
1071 "a positional predicate on the joined side".into(),
1072 ));
1073 }
1074 fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
1077 if ex.kind(e) == "and" {
1078 for c in ex.arbor.children(e) {
1079 conjuncts(ex, c, out);
1080 }
1081 } else {
1082 out.push(e);
1083 }
1084 }
1085 let mut parts = Vec::new();
1086 for c in self.arbor.children(p) {
1087 conjuncts(self, c, &mut parts);
1088 }
1089 for e in parts {
1090 let uses_driver = self.subtree_has(e, "outer");
1091 self.in_on = true;
1092 let cond = self.pred_expr(e, Some(rtable));
1093 self.in_on = false;
1094 let cond = cond?;
1095 if uses_driver && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
1096 self.collect_join_cols(e)?;
1103 on.push(cond);
1104 } else {
1105 wheres.push(cond);
1106 }
1107 }
1108 Ok(())
1109 }
1110
1111 fn collect_join_cols(&mut self, e: NodeId) -> Result<(), SqlError> {
1115 if self.kind(e) == "outer" {
1118 return Ok(());
1119 }
1120 if self.kind(e) == "path"
1121 && self.kid(e, "step").is_none()
1122 && let Some(p) = self.kid(e, "projection")
1123 {
1124 let col = self.projection_col(p)?;
1125 self.join_on_cols.push(col);
1126 }
1127 for c in self.arbor.children(e) {
1128 self.collect_join_cols(c)?;
1129 }
1130 Ok(())
1131 }
1132
1133 fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
1134 if self.kind(n) == kind {
1135 return true;
1136 }
1137 self.arbor
1138 .children(n)
1139 .into_iter()
1140 .any(|c| self.subtree_has(c, kind))
1141 }
1142
1143 fn pipeline(
1145 &mut self,
1146 q: NodeId,
1147 sel: &mut Select,
1148 join: Option<(&str, &str)>,
1149 ) -> Result<(), SqlError> {
1150 let Some(pipe) = self.kid(q, "pipeline") else {
1151 return Ok(());
1152 };
1153 let stages: Vec<NodeId> = self.arbor.children(pipe);
1154 let mut i = 0;
1155 let mut pending_col: Option<String> = None;
1157 let mut grouped: Option<(String, String, String)> = None;
1159
1160 while i < stages.len() {
1161 let s = stages[i];
1162 match self.kind(s).as_str() {
1163 "expr" => {
1164 let kids = self.arbor.children(s);
1165 pending_col = Some(self.operand(kids[0], join.map(|(l, _)| l))?);
1166 }
1167 "func" => {
1168 let name = self.prop_s(s, "name");
1169 match name.as_str() {
1170 "rec" | "record" => {
1171 sel.select = self.record_fields(s, join)?;
1172 }
1173 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1176 let (key, _, _) = grouped.as_ref().ok_or_else(|| {
1177 SqlError::Unsupported(format!(
1178 "'| {name}' outside a group (use '@| {name}')"
1179 ))
1180 })?;
1181 let col = if name == "count" {
1185 pending_col = None;
1186 None
1187 } else {
1188 pending_col.take()
1189 };
1190 let agg = sql_agg(&name, col)?;
1191 grouped = Some((key.clone(), agg.clone(), agg));
1192 }
1193 other => {
1194 return Err(SqlError::Unsupported(format!(
1195 "the '{other}' pipeline function"
1196 )));
1197 }
1198 }
1199 }
1200 "push" => {
1201 if let Some((k, agg, _)) = grouped.take() {
1203 let alias = self.prop_s(s, "name");
1204 grouped = Some((k, agg, alias));
1205 } else {
1206 return Err(SqlError::Unsupported(
1207 "a register push (Quarb-side state)".into(),
1208 ));
1209 }
1210 }
1211 "filter" => {
1212 if grouped.is_some() {
1213 let cond = self.having_cond(s)?;
1216 sel.having = Some(cond);
1217 } else {
1218 return Err(SqlError::Unsupported(
1219 "a mid-pipeline filter (put it in the row predicate)".into(),
1220 ));
1221 }
1222 }
1223 "recall" => {
1224 if self.prop_s(s, "ref") != "%." {
1226 return Err(SqlError::Unsupported(
1227 "a register recall (Quarb-side state)".into(),
1228 ));
1229 }
1230 let (key, agg, alias) = grouped.clone().ok_or_else(|| {
1231 SqlError::Unsupported("'%.', with nothing grouped".into())
1232 })?;
1233 let alias = self.alias(&alias)?;
1234 sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
1235 if let Some(h) = sel.having.take() {
1238 sel.having = Some(h.replace("__AGG__", &agg));
1239 }
1240 }
1241 "agg" => {
1242 let name = self.prop_s(s, "name");
1243 match name.as_str() {
1244 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1245 let col = if name == "count" {
1249 pending_col = None;
1250 None
1251 } else {
1252 pending_col.take()
1253 };
1254 sel.select = vec![sql_agg(&name, col)?];
1255 self.aggregate = true;
1256 }
1257 "group" => {
1258 if self.strict {
1259 return Err(SqlError::Unsupported(
1260 "pushdown: GROUP BY result order is unordered in SQL".into(),
1261 ));
1262 }
1263 self.notes.push(
1264 "GROUP BY: SQL keeps a NULL-key group; Quarb's group \
1265 drops null keys"
1266 .to_string(),
1267 );
1268 let key = self.group_key(s, join)?;
1269 sel.group_by = Some(key.clone());
1270 grouped = Some((key, String::new(), String::new()));
1271 }
1272 "sort_by" => {
1273 if self.strict {
1274 return Err(SqlError::Unsupported(
1275 "pushdown: ORDER BY collations differ per engine".into(),
1276 ));
1277 }
1278 if sel.limit.is_some() {
1283 return Err(SqlError::Unsupported(
1284 "a sort after a positional selection (SQL orders \
1285 before LIMIT)"
1286 .into(),
1287 ));
1288 }
1289 if sel.order_by.is_some() {
1290 return Err(SqlError::Unsupported(
1291 "a second sort (SQL has a single ORDER BY)".into(),
1292 ));
1293 }
1294 let kids = self.arbor.children(s);
1295 let e = self.operand(kids[0], join.map(|(l, _)| l))?;
1296 sel.order_by = Some((e, false));
1297 }
1298 "reverse" => match &mut sel.order_by {
1299 Some((_, desc)) => *desc = true,
1300 None => {
1301 return Err(SqlError::Unsupported(
1302 "reverse without an ORDER BY (rows are unordered)".into(),
1303 ));
1304 }
1305 },
1306 "top" => {
1307 if self.strict {
1308 return Err(SqlError::Unsupported(
1309 "pushdown: ORDER BY collations differ per engine".into(),
1310 ));
1311 }
1312 if sel.limit.is_some() {
1313 return Err(SqlError::Unsupported(
1314 "'top' after a positional selection (SQL orders \
1315 before LIMIT)"
1316 .into(),
1317 ));
1318 }
1319 if sel.order_by.is_some() {
1320 return Err(SqlError::Unsupported(
1321 "a second sort (SQL has a single ORDER BY)".into(),
1322 ));
1323 }
1324 let kids = self.arbor.children(s);
1325 let n = self.prop_s(kids[0], "value");
1326 let e = self.operand(kids[1], join.map(|(l, _)| l))?;
1327 sel.order_by = Some((e, true));
1328 sel.limit = Some(n);
1329 }
1330 "unique" => {
1331 if self.strict {
1332 return Err(SqlError::Unsupported(
1333 "pushdown: DISTINCT result order is unordered in SQL".into(),
1334 ));
1335 }
1336 if sel.limit.is_some() {
1339 return Err(SqlError::Unsupported(
1340 "'unique' after a positional selection (SQL applies \
1341 DISTINCT before LIMIT)"
1342 .into(),
1343 ));
1344 }
1345 if let Some(c) = pending_col.take() {
1346 sel.select = vec![c];
1347 }
1348 sel.distinct = true;
1349 }
1350 other => {
1351 return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
1352 }
1353 }
1354 }
1355 "select" => {
1356 if self.strict {
1357 return Err(SqlError::Unsupported(
1358 "pushdown: LIMIT without a guaranteed order".into(),
1359 ));
1360 }
1361 if sel.limit.is_some() {
1363 return Err(SqlError::Unsupported(
1364 "a second positional selection".into(),
1365 ));
1366 }
1367 let p = self.arbor.children(s)[0];
1368 match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
1369 ("range", Some(Value::Int(n)))
1370 if self.prop(p, "from").is_none() && n > 0 =>
1371 {
1372 sel.limit = Some(n.to_string());
1373 }
1374 _ => {
1375 return Err(SqlError::Unsupported(
1376 "positional selection beyond '@| [..n]'".into(),
1377 ));
1378 }
1379 }
1380 }
1381 other => {
1382 return Err(SqlError::Unsupported(format!(
1383 "the '{other}' stage (windows, subcontexts, and registers are \
1384 Quarb-side state)"
1385 )));
1386 }
1387 }
1388 i += 1;
1389 }
1390 if let Some(c) = pending_col
1392 && sel.select.is_empty()
1393 {
1394 sel.select = vec![c];
1395 }
1396 Ok(())
1397 }
1398
1399 fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
1400 let kids = self.arbor.children(s);
1401 let key = kids
1404 .iter()
1405 .rev()
1406 .find(|&&k| self.kind(k) != "literal")
1407 .ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
1408 self.operand(*key, join.map(|(l, _)| l))
1409 }
1410
1411 fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
1413 let kids = self.arbor.children(s);
1414 if kids.len() != 1 || self.kind(kids[0]) != "compare" {
1415 return Err(SqlError::Unsupported(
1416 "HAVING translates for a single comparison".into(),
1417 ));
1418 }
1419 let e = kids[0];
1420 let op = self.prop_s(e, "op");
1421 let cmp_kids = self.arbor.children(e);
1422 let l = match self.kind(cmp_kids[0]).as_str() {
1423 "topic" | "recall" => "__AGG__".to_string(),
1424 _ => {
1425 return Err(SqlError::Unsupported(
1426 "HAVING compares the aggregate ($_ or its register)".into(),
1427 ));
1428 }
1429 };
1430 let r = self.operand(cmp_kids[1], None)?;
1431 Ok(format!("{l} {op} {r}"))
1432 }
1433
1434 fn record_fields(
1436 &mut self,
1437 s: NodeId,
1438 join: Option<(&str, &str)>,
1439 ) -> Result<Vec<String>, SqlError> {
1440 let kids = self.arbor.children(s);
1441 let mut fields = Vec::new();
1442 let mut i = 0;
1443 while i < kids.len() {
1444 let k = kids[i];
1445 if self.kind(k) == "literal" {
1446 let name = self.prop_s(k, "value");
1447 let value = self.operand(kids[i + 1], join.map(|(l, _)| l))?;
1448 let name = self.alias(&name)?;
1449 fields.push(format!("{value} AS {name}"));
1450 i += 2;
1451 } else {
1452 let value = self.operand(k, join.map(|(l, _)| l))?;
1453 fields.push(value);
1454 i += 1;
1455 }
1456 }
1457 Ok(fields)
1458 }
1459}
1460
1461fn sql_agg(name: &str, col: Option<String>) -> Result<String, SqlError> {
1462 let f = match name {
1463 "count" => "COUNT",
1464 "sum" => "SUM",
1465 "mean" | "avg" => "AVG",
1466 "min" => "MIN",
1467 "max" => "MAX",
1468 _ => unreachable!("checked by caller"),
1469 };
1470 match col {
1471 Some(c) => Ok(format!("{f}({c})")),
1472 None if f == "COUNT" => Ok("COUNT(*)".to_string()),
1475 None => Err(SqlError::Unsupported(format!(
1476 "'{name}' over row nodes (project a column first: '| ::col @| {name}')"
1477 ))),
1478 }
1479}
1480
1481#[cfg(test)]
1482mod null_and_literal_tests {
1483 use super::{Dialect, export, partial_pushdown, pushdown};
1484
1485 const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
1488
1489 #[test]
1490 fn json_column_path_pushdown_per_dialect() {
1491 let q = "/orders/*[/data/meta/tier:: = 'gold']::id";
1495 let sql = |d| pushdown(q, Some(d)).unwrap().sql;
1496 assert_eq!(
1497 sql(Dialect::Postgres),
1498 "SELECT id FROM orders WHERE (data::jsonb #>> '{meta,tier}') = 'gold'"
1499 );
1500 assert_eq!(
1501 sql(Dialect::MySql),
1502 "SELECT id FROM orders WHERE JSON_UNQUOTE(JSON_EXTRACT(data, '$.meta.tier')) = 'gold'"
1503 );
1504 assert_eq!(
1505 sql(Dialect::Sqlite),
1506 "SELECT id FROM orders WHERE json_extract(data, '$.meta.tier') = 'gold'"
1507 );
1508 assert_eq!(
1509 sql(Dialect::Mssql),
1510 "SELECT id FROM orders WHERE JSON_VALUE(data, '$.meta.tier') = 'gold'"
1511 );
1512 assert_eq!(
1513 sql(Dialect::Oracle),
1514 "SELECT id FROM orders WHERE JSON_VALUE(data, '$.meta.tier') = 'gold'"
1515 );
1516 assert!(pushdown(q, None).is_none());
1519 }
1520
1521 #[test]
1522 fn json_pushdown_only_string_equality() {
1523 let d = Some(Dialect::Sqlite);
1527 assert!(pushdown("/o/*[/data/n:: > 2]::id", d).is_none());
1528 assert!(pushdown("/o/*[/data/tier:: != 'gold']::id", d).is_none());
1529 assert!(pushdown("/o/*[/data/items/*/sku:: = 'A1']::id", d).is_none());
1530 }
1531
1532 #[test]
1533 fn null_literal_compares_use_is_null() {
1534 assert_eq!(
1538 export("/t/*[::x = null] | ::x").unwrap().query,
1539 "SELECT x FROM t WHERE x IS NULL"
1540 );
1541 assert_eq!(
1542 export("/t/*[::x != null] | ::x").unwrap().query,
1543 "SELECT x FROM t WHERE x IS NOT NULL"
1544 );
1545 assert_eq!(
1546 pushdown("/t/*[::x = null] | ::x", None).unwrap().sql,
1547 "SELECT x FROM t WHERE x IS NULL"
1548 );
1549 assert_eq!(
1550 pushdown("/t/*[::x != null] | ::x", None).unwrap().sql,
1551 "SELECT x FROM t WHERE x IS NOT NULL"
1552 );
1553 }
1554
1555 #[test]
1556 fn ne_and_not_refuse_pushdown_but_display_diverges() {
1557 assert!(pushdown("/t/*[::x != 5] | ::x", None).is_none());
1561 assert!(pushdown("/t/*[!::x = 5] | ::x", None).is_none());
1562 assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}")).is_none());
1563 assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}")).is_none());
1564 let t = export("/t/*[::x != 5] | ::x").unwrap();
1566 assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
1567 assert!(t.notes.iter().any(|n| n.contains("NULL")));
1568 }
1569
1570 #[test]
1571 fn unescapable_text_literal_refuses_pushdown() {
1572 assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path", None).is_none());
1576 assert!(pushdown("/t/*[::name = \"it's\"] | ::name", None).is_none());
1577 assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}")).is_none());
1578 assert_eq!(
1579 pushdown("/t/*[::name = \"rare\"] | ::name", None).unwrap().sql,
1580 "SELECT name FROM t WHERE name = 'rare'"
1581 );
1582 }
1583}