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_left_cols: Vec::new(),
48 join_table: None,
49 aggregate: false,
50 };
51 let query = ex.query()?;
52 Ok(Translation {
53 query,
54 notes: ex.notes,
55 })
56}
57
58fn refuse_marker(quarb: &str) -> Result<(), SqlError> {
64 for marker in ["__LEFT__", "__AGG__"] {
65 if quarb.contains(marker) {
66 return Err(SqlError::Unsupported(format!(
67 "query text contains the reserved marker \"{marker}\""
68 )));
69 }
70 }
71 Ok(())
72}
73
74pub struct Pushdown {
80 pub sql: String,
81 pub order_table: Option<String>,
82 pub join_left: Option<(String, Vec<String>)>,
92}
93
94#[derive(Clone, Copy, PartialEq, Eq, Debug)]
100pub enum Dialect {
101 Postgres,
102 MySql,
103 Sqlite,
104 Mssql,
105 Oracle,
106}
107
108pub fn pushdown(quarb: &str, dialect: Option<Dialect>) -> Option<Pushdown> {
117 pushdown_explained(quarb, dialect).ok()
118}
119
120pub fn pushdown_explained(quarb: &str, dialect: Option<Dialect>) -> Result<Pushdown, SqlError> {
123 refuse_marker(quarb)?;
124 let arbor =
125 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
126 refuse_groups(&arbor)?;
127 let mut ex = Exporter {
128 arbor,
129 notes: Vec::new(),
130 strict: true,
131 dialect,
132 from_table: String::new(),
133 join_on_left_cols: Vec::new(),
134 join_table: None,
135 aggregate: false,
136 };
137 let sql = ex.query()?;
138 let order_table = if ex.aggregate {
139 None
140 } else {
141 Some(ex.join_table.clone().unwrap_or(ex.from_table.clone()))
145 };
146 let join_left = ex
147 .join_table
148 .is_some()
149 .then(|| (ex.from_table.clone(), ex.join_on_left_cols.clone()));
150 Ok(Pushdown {
151 sql,
152 order_table,
153 join_left,
154 })
155}
156
157fn json_extract(dialect: Dialect, qual: Option<&str>, col: &str, path: &[String]) -> String {
165 let qcol = match qual {
166 Some(q) => format!("{q}.{col}"),
167 None => col.to_string(),
168 };
169 match dialect {
170 Dialect::Postgres => format!("({qcol}::jsonb #>> '{{{}}}')", path.join(",")),
175 Dialect::MySql => {
179 format!("JSON_UNQUOTE(JSON_EXTRACT({qcol}, '$.{}'))", path.join("."))
180 }
181 Dialect::Sqlite => format!("json_extract({qcol}, '$.{}')", path.join(".")),
182 Dialect::Mssql | Dialect::Oracle => format!("JSON_VALUE({qcol}, '$.{}')", path.join(".")),
185 }
186}
187
188const SQL_KEYWORDS: &[&str] = &[
193 "all",
194 "and",
195 "as",
196 "asc",
197 "by",
198 "case",
199 "cross",
200 "desc",
201 "distinct",
202 "else",
203 "end",
204 "except",
205 "exists",
206 "from",
207 "group",
208 "having",
209 "in",
210 "index",
211 "inner",
212 "intersect",
213 "into",
214 "is",
215 "join",
216 "left",
217 "like",
218 "limit",
219 "not",
220 "null",
221 "offset",
222 "on",
223 "or",
224 "order",
225 "outer",
226 "right",
227 "select",
228 "set",
229 "table",
230 "then",
231 "union",
232 "unique",
233 "update",
234 "using",
235 "values",
236 "when",
237 "where",
238];
239
240fn is_plain_ident(name: &str) -> bool {
244 !name.is_empty()
245 && name
246 .chars()
247 .next()
248 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
249 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
250 && !SQL_KEYWORDS.contains(&name.to_ascii_lowercase().as_str())
251}
252
253#[derive(Default)]
255struct Select {
256 select: Vec<String>,
257 distinct: bool,
258 from: String,
259 join: Option<(String, String)>, wheres: Vec<String>,
261 group_by: Option<String>,
262 having: Option<String>,
263 order_by: Option<(String, bool)>, limit: Option<String>,
265}
266
267impl Select {
268 fn render(&self) -> String {
269 let mut out = String::from("SELECT ");
270 if self.distinct {
271 out.push_str("DISTINCT ");
272 }
273 if self.select.is_empty() {
274 out.push('*');
275 } else {
276 out.push_str(&self.select.join(", "));
277 }
278 out.push_str(&format!(" FROM {}", self.from));
279 if let Some((t, on)) = &self.join {
280 out.push_str(&format!(" JOIN {t} ON {on}"));
281 }
282 if !self.wheres.is_empty() {
283 out.push_str(&format!(" WHERE {}", self.wheres.join(" AND ")));
284 }
285 if let Some(g) = &self.group_by {
286 out.push_str(&format!(" GROUP BY {g}"));
287 }
288 if let Some(h) = &self.having {
289 out.push_str(&format!(" HAVING {h}"));
290 }
291 if let Some((e, desc)) = &self.order_by {
292 out.push_str(&format!(" ORDER BY {e}"));
293 if *desc {
294 out.push_str(" DESC");
295 }
296 }
297 if let Some(n) = &self.limit {
298 out.push_str(&format!(" LIMIT {n}"));
299 }
300 out
301 }
302}
303
304pub struct Partial {
309 pub table: String,
310 pub where_sql: String,
311}
312
313pub fn partial_pushdown(quarb: &str) -> Option<Partial> {
327 partial_pushdown_explained(quarb).ok()
328}
329
330pub fn partial_pushdown_explained(quarb: &str) -> Result<Partial, SqlError> {
332 refuse_marker(quarb)?;
333 let arbor =
334 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
335 refuse_groups(&arbor)?;
336 let mut ex = Exporter {
337 arbor,
338 notes: Vec::new(),
339 strict: true,
340 dialect: None,
341 from_table: String::new(),
342 join_on_left_cols: Vec::new(),
343 join_table: None,
344 aggregate: false,
345 };
346 ex.partial()
347}
348
349fn refuse_groups(arbor: &QueryArbor) -> Result<(), SqlError> {
355 let mut stack = vec![arbor.root()];
356 while let Some(n) = stack.pop() {
357 if arbor.name(n).as_deref() == Some("group") {
358 return Err(SqlError::Unsupported(
359 "path patterns (groups and quantifiers)".into(),
360 ));
361 }
362 stack.extend(arbor.children(n));
363 }
364 Ok(())
365}
366
367struct Exporter {
368 arbor: QueryArbor,
369 notes: Vec<String>,
370 strict: bool,
374 dialect: Option<Dialect>,
377 from_table: String,
378 join_on_left_cols: Vec<String>,
379 join_table: Option<String>,
380 aggregate: bool,
381}
382
383impl Exporter {
384 fn kids(&self, n: NodeId, kind: &str) -> Vec<NodeId> {
385 self.arbor
386 .children(n)
387 .into_iter()
388 .filter(|&c| self.arbor.name(c).as_deref() == Some(kind))
389 .collect()
390 }
391
392 fn kid(&self, n: NodeId, kind: &str) -> Option<NodeId> {
393 self.kids(n, kind).into_iter().next()
394 }
395
396 fn prop(&self, n: NodeId, key: &str) -> Option<Value> {
397 self.arbor.property(n, key)
398 }
399
400 fn prop_s(&self, n: NodeId, key: &str) -> String {
401 self.prop(n, key).map(|v| v.to_string()).unwrap_or_default()
402 }
403
404 fn kind(&self, n: NodeId) -> String {
405 self.arbor.name(n).unwrap_or_default()
406 }
407
408 fn is_null_literal(&self, n: NodeId) -> bool {
410 self.kind(n) == "literal" && self.prop_s(n, "type") == "null"
411 }
412
413 fn partial(&mut self) -> Result<Partial, SqlError> {
415 let root = self.arbor.root();
416 let q = self
417 .kid(root, "query")
418 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
419 if self.kid(q, "query").is_some() {
420 return Err(SqlError::Unsupported(
421 "partial pushdown: correlations address other tables".into(),
422 ));
423 }
424 let branches = self.kids(q, "branch");
425 if branches.len() != 1 {
426 return Err(SqlError::Unsupported(
427 "partial pushdown: a branch union".into(),
428 ));
429 }
430 let (table, preds) = self.table_branch(branches[0])?;
431
432 let all = self.walk_all(root);
434 let mut table_mentions = 0;
435 for n in &all {
436 match self.kind(*n).as_str() {
437 "step" => {
438 let axis = self.prop_s(*n, "axis");
439 if matches!(axis.as_str(), "->" | "<-" | "~>" | "<~") {
440 return Err(SqlError::Unsupported(
441 "partial pushdown: crosslink/resolution axes could reach \
442 the filtered table"
443 .into(),
444 ));
445 }
446 if self.prop_s(*n, "matcher") == table {
447 table_mentions += 1;
448 }
449 }
450 "projection" if self.prop_s(*n, "kind") != "property" => {
451 return Err(SqlError::Unsupported(
452 "partial pushdown: metadata would observe the filtering \
453 (;;;n-rows, :::index)"
454 .into(),
455 ));
456 }
457 _ => {}
458 }
459 }
460 if table_mentions != 1 {
461 return Err(SqlError::Unsupported(
462 "partial pushdown: the table is reached more than once".into(),
463 ));
464 }
465
466 let mut conds = Vec::new();
469 for p in preds {
470 if self.prop_s(p, "kind") != "expr" {
471 break;
472 }
473 conds.push(self.predicate_cond(p, None)?);
474 }
475 if conds.is_empty() {
476 return Err(SqlError::Unsupported(
477 "partial pushdown: no leading expression predicates to push".into(),
478 ));
479 }
480 Ok(Partial {
481 table,
482 where_sql: conds.join(" AND "),
483 })
484 }
485
486 fn walk_all(&self, n: NodeId) -> Vec<NodeId> {
487 let mut out = vec![n];
488 let mut i = 0;
489 while i < out.len() {
490 out.extend(self.arbor.children(out[i]));
491 i += 1;
492 }
493 out
494 }
495
496 fn query(&mut self) -> Result<String, SqlError> {
497 let root = self.arbor.root();
498 let q = self
499 .kid(root, "query")
500 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
501 let mut sel = Select::default();
502
503 let corrs = self.kids(q, "query");
505 let branches = self.kids(q, "branch");
506 if branches.len() != 1 {
507 return Err(SqlError::Unsupported(
508 "a branch union (SQL has UNION, but result shapes differ; export one branch)"
509 .into(),
510 ));
511 }
512
513 if corrs.len() > 1 {
514 return Err(SqlError::Unsupported(
515 "more than one correlation context".into(),
516 ));
517 }
518 if let Some(corr) = corrs.first() {
519 let (ltable, lpreds) = self.table_branch(self.kids(*corr, "branch")[0])?;
521 if !lpreds.is_empty() {
522 return Err(SqlError::Unsupported(
523 "predicates on the correlation context (put them on the joined side)".into(),
524 ));
525 }
526 let (rtable, rpreds) = self.table_branch(branches[0])?;
527 let lsql = self.sql_ident(<able, "table name")?;
531 let rsql = self.sql_ident(&rtable, "table name")?;
532 let mut on = Vec::new();
535 let mut wheres = Vec::new();
536 for p in rpreds {
537 self.split_join_pred(p, &lsql, &rsql, &mut on, &mut wheres)?;
538 }
539 if on.is_empty() {
540 return Err(SqlError::Unsupported(
541 "a correlation without a '$*1' equality (no JOIN condition)".into(),
542 ));
543 }
544 self.notes.push(
545 "JOIN: Quarb's binding is existential (one row per joined-side row); \
546 SQL multiplies rows when several left rows match"
547 .to_string(),
548 );
549 sel.from = lsql.clone();
550 self.from_table = ltable;
551 self.join_table = Some(rtable);
552 sel.join = Some((rsql.clone(), on.join(" AND ")));
553 sel.wheres = wheres;
554 if let Some(proj) = self.kid(branches[0], "projection") {
557 let col = self.projection_col(proj)?;
558 let col = self.sql_ident(&col, "column name")?;
559 sel.select.push(format!("{rsql}.{col}"));
560 }
561 self.pipeline(q, &mut sel, Some((&lsql, &rsql)))?;
562 } else {
563 let (table, preds) = self.table_branch(branches[0])?;
564 sel.from = self.sql_ident(&table, "table name")?;
565 self.from_table = table;
566 for p in preds {
567 let cond = self.predicate_cond(p, None)?;
568 sel.wheres.push(cond);
569 }
570 if let Some(proj) = self.kid(branches[0], "projection") {
572 let col = self.projection_col(proj)?;
573 sel.select.push(self.sql_ident(&col, "column name")?);
574 }
575 self.pipeline(q, &mut sel, None)?;
576 }
577 Ok(sel.render())
578 }
579
580 fn table_branch(&mut self, b: NodeId) -> Result<(String, Vec<NodeId>), SqlError> {
583 let steps = self.kids(b, "step");
584 if steps.len() != 2 {
585 return Err(SqlError::Unsupported(
586 "navigation beyond /table/* (SQL sees tables and rows)".into(),
587 ));
588 }
589 let (t, rows) = (steps[0], steps[1]);
590 if self.prop_s(t, "axis") != "/"
591 || self.prop_s(t, "matcher-kind") != "name"
592 || self.prop_s(rows, "axis") != "/"
593 || self.prop_s(rows, "matcher-kind") != "any"
594 {
595 return Err(SqlError::Unsupported(
596 "navigation beyond /table/* (SQL sees tables and rows)".into(),
597 ));
598 }
599 if self.kid(t, "predicate").is_some() {
600 return Err(SqlError::Unsupported("a predicate on the table hop".into()));
601 }
602 Ok((self.prop_s(t, "matcher"), self.kids(rows, "predicate")))
603 }
604
605 fn predicate_cond(&mut self, p: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
608 if self.prop_s(p, "kind") != "expr" {
609 return Err(SqlError::Unsupported(
610 "a positional predicate on rows (SQL rows are unordered; ORDER BY + LIMIT)".into(),
611 ));
612 }
613 let parts: Vec<String> = self
614 .arbor
615 .children(p)
616 .into_iter()
617 .map(|c| self.pred_expr(c, qual))
618 .collect::<Result<_, _>>()?;
619 Ok(parts.join(" AND "))
620 }
621
622 fn pred_expr(&mut self, e: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
623 match self.kind(e).as_str() {
624 "and" | "or" => {
625 let op = self.kind(e).to_uppercase();
626 let kids: Vec<String> = self
627 .arbor
628 .children(e)
629 .into_iter()
630 .map(|c| self.pred_expr(c, qual))
631 .collect::<Result<_, _>>()?;
632 Ok(format!("({})", kids.join(&format!(" {op} "))))
633 }
634 "not" => {
635 if self.strict {
641 return Err(SqlError::Unsupported(
642 "pushdown: 'not(...)' drops the NULL rows Quarb keeps \
643 (SQL NOT propagates UNKNOWN)"
644 .into(),
645 ));
646 }
647 let inner: Vec<String> = self
648 .arbor
649 .children(e)
650 .into_iter()
651 .map(|c| self.pred_expr(c, qual))
652 .collect::<Result<_, _>>()?;
653 Ok(format!("NOT ({})", inner.join(" AND ")))
654 }
655 "parens" => {
656 let inner: Vec<String> = self
657 .arbor
658 .children(e)
659 .into_iter()
660 .map(|c| self.pred_expr(c, qual))
661 .collect::<Result<_, _>>()?;
662 Ok(format!("({})", inner.join(" AND ")))
663 }
664 "compare" => {
665 let op = self.prop_s(e, "op");
666 let kids = self.arbor.children(e);
667 if matches!(op.as_str(), "=" | "!=")
676 && (self.is_null_literal(kids[0]) || self.is_null_literal(kids[1]))
677 {
678 let other = if self.is_null_literal(kids[0]) {
679 kids[1]
680 } else {
681 kids[0]
682 };
683 let col = self.operand(other, qual)?;
684 return Ok(if op == "=" {
685 format!("{col} IS NULL")
686 } else {
687 format!("{col} IS NOT NULL")
688 });
689 }
690 if op == "="
703 && let Some(dialect) = self.dialect
704 {
705 for (pi, li) in [(0usize, 1usize), (1, 0)] {
706 if self.is_text_literal(kids[li])
707 && let Some((col, path)) = self.json_path(kids[pi])
708 {
709 let lit = self.operand(kids[li], qual)?;
710 let extract = json_extract(dialect, qual, &col, &path);
711 return Ok(format!("{extract} = {lit}"));
712 }
713 }
714 }
715 let l = self.operand(kids[0], qual)?;
716 let r = self.operand(kids[1], qual)?;
717 Ok(match op.as_str() {
718 "=" => format!("{l} = {r}"),
719 "!=" => {
720 if self.strict {
728 return Err(SqlError::Unsupported(
729 "pushdown: '!=' drops the NULL rows Quarb keeps \
730 (SQL '<>' is UNKNOWN for NULL; use '!= null' \
731 for IS NOT NULL)"
732 .into(),
733 ));
734 }
735 self.notes.push(
736 "'!=' → '<>': Quarb keeps rows whose column is NULL; \
737 SQL's '<>' drops them (use '!= null' for IS NOT NULL)"
738 .to_string(),
739 );
740 format!("{l} <> {r}")
741 }
742 "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
743 "*=" => {
744 if self.strict {
745 return Err(SqlError::Unsupported(
746 "pushdown: LIKE case folding differs per engine".into(),
747 ));
748 }
749 if !self.is_text_literal(kids[1]) {
754 return Err(SqlError::Unsupported(
755 "'*=' with a non-literal pattern".into(),
756 ));
757 }
758 self.notes.push(
759 "*= → LIKE: Quarb's substring test is case-sensitive; LIKE \
760 folds case on SQLite/MySQL but not PostgreSQL"
761 .to_string(),
762 );
763 let raw = self
768 .prop(kids[1], "value")
769 .map(|v| v.to_string())
770 .unwrap_or_default();
771 let pat = raw
772 .replace('\\', "\\\\")
773 .replace('%', "\\%")
774 .replace('_', "\\_")
775 .replace('\'', "''");
776 format!("{l} LIKE '%{pat}%' ESCAPE '\\'")
777 }
778 "=~" | "!~" => {
779 return Err(SqlError::Unsupported(
780 "regex matching (REGEXP dialects disagree; use *= or spell \
781 the SQL by hand)"
782 .into(),
783 ));
784 }
785 other => {
786 return Err(SqlError::Unsupported(format!("the '{other}' comparison")));
787 }
788 })
789 }
790 _ => {
792 if self.strict {
793 return Err(SqlError::Unsupported(
794 "pushdown: truthiness diverges (0 and '' are falsy in Quarb)".into(),
795 ));
796 }
797 self.notes.push(
798 "truthiness: '[::c]' exports as IS NOT NULL, but Quarb also treats \
799 0 and '' as falsy"
800 .to_string(),
801 );
802 Ok(format!("{} IS NOT NULL", self.operand(e, qual)?))
803 }
804 }
805 }
806
807 fn operand(&mut self, o: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
808 match self.kind(o).as_str() {
809 "literal" => {
810 let v = self.prop(o, "value").unwrap_or(Value::Null);
811 match self.prop_s(o, "type").as_str() {
812 "text" => {
813 let s = v.to_string();
814 if self.strict && (s.contains('\'') || s.contains('\\')) {
825 return Err(SqlError::Unsupported(
826 "pushdown: a text literal with a quote or backslash \
827 has no escaping portable across SQL dialects"
828 .into(),
829 ));
830 }
831 Ok(format!("'{}'", s.replace('\'', "''")))
832 }
833 "null" => Ok("NULL".to_string()),
834 _ => Ok(v.to_string()),
835 }
836 }
837 "path" => {
838 if self.kid(o, "step").is_some() {
839 let s = self.kids(o, "step")[0];
842 if self.prop_s(s, "axis") == "~>" {
843 return Err(SqlError::Unsupported(
844 "a '~>' resolution chain: the foreign-key targets live in \
845 the schema, not the query — spell the join with '<=>' to \
846 export it"
847 .into(),
848 ));
849 }
850 return Err(SqlError::Unsupported(
851 "navigation inside a predicate (SQL rows are flat)".into(),
852 ));
853 }
854 let p = self
855 .kid(o, "projection")
856 .ok_or_else(|| SqlError::Unsupported("an empty path operand".into()))?;
857 let col = self.projection_col(p)?;
858 let col = self.sql_ident(&col, "column name")?;
859 Ok(match qual {
860 Some(q) => format!("{q}.{col}"),
861 None => col,
862 })
863 }
864 "context" => {
865 let p = self
873 .kid(o, "projection")
874 .ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
875 let col = self.projection_col(p)?;
876 let col = self.sql_ident(&col, "column name")?;
877 if qual.is_none() {
878 return Err(SqlError::Unsupported(
879 "a '$*' reference outside a correlation join".into(),
880 ));
881 }
882 match self.prop(o, "index") {
883 None | Some(Value::Int(1)) => Ok(format!("__LEFT__.{col}")),
884 Some(Value::Int(2)) => Ok(format!("{}.{col}", qual.expect("checked above"))),
885 Some(v) => Err(SqlError::Unsupported(format!(
886 "pushdown: $*{v} beyond a two-branch correlation"
887 ))),
888 }
889 }
890 "arith" => {
891 let op = self.prop_s(o, "op");
892 let kids = self.arbor.children(o);
893 let l = self.operand(kids[0], qual)?;
894 let r = self.operand(kids[1], qual)?;
895 Ok(match op.as_str() {
896 "+" | "-" | "*" => format!("({l} {op} {r})"),
897 "div" => format!("({l} / {r})"),
898 "mod" => format!("({l} % {r})"),
899 other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
900 })
901 }
902 other => Err(SqlError::Unsupported(format!(
903 "the '{other}' operand (registers, topics, and captures are Quarb-side state)"
904 ))),
905 }
906 }
907
908 fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
909 match self.prop_s(p, "kind").as_str() {
910 "property" => match self.prop(p, "key") {
911 Some(k) => Ok(k.to_string()),
912 None => Err(SqlError::Unsupported(
913 "the bare '::' projection (name the column)".into(),
914 )),
915 },
916 other => Err(SqlError::Unsupported(format!(
917 "the {other} metadata projection"
918 ))),
919 }
920 }
921
922 fn is_text_literal(&self, o: NodeId) -> bool {
924 self.kind(o) == "literal" && self.prop_s(o, "type") == "text"
925 }
926
927 fn json_path(&self, o: NodeId) -> Option<(String, Vec<String>)> {
934 if self.kind(o) != "path" {
935 return None;
936 }
937 let steps = self.kids(o, "step");
938 if steps.len() < 2 {
939 return None;
940 }
941 let proj = self.kid(o, "projection")?;
944 if self.prop_s(proj, "kind") != "property" || self.prop(proj, "key").is_some() {
945 return None;
946 }
947 let mut names = Vec::with_capacity(steps.len());
948 for s in &steps {
949 if self.prop_s(*s, "axis") != "/"
950 || self.prop_s(*s, "matcher-kind") != "name"
951 || self.kid(*s, "predicate").is_some()
952 {
953 return None;
954 }
955 let name = self.prop_s(*s, "matcher");
956 let plain = !name.is_empty()
959 && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
960 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
961 if !plain {
962 return None;
963 }
964 names.push(name);
965 }
966 let col = names.remove(0);
967 Some((col, names))
968 }
969
970 fn alias(&mut self, name: &str) -> Result<String, SqlError> {
974 if is_plain_ident(name) {
975 return Ok(name.to_string());
976 }
977 if self.strict {
978 return Err(SqlError::Unsupported(format!(
979 "pushdown: field name {name:?} needs SQL quoting \
980 (dialects disagree); rename the field"
981 )));
982 }
983 self.notes
984 .push(format!("field name {name:?} double-quoted (ANSI)"));
985 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
986 }
987
988 fn sql_ident(&mut self, name: &str, what: &str) -> Result<String, SqlError> {
995 if is_plain_ident(name) {
996 return Ok(name.to_string());
997 }
998 if self.strict {
999 return Err(SqlError::Unsupported(format!(
1000 "pushdown: {what} {name:?} is not a plain SQL identifier"
1001 )));
1002 }
1003 self.notes
1004 .push(format!("{what} {name:?} double-quoted (ANSI)"));
1005 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1006 }
1007
1008 fn split_join_pred(
1012 &mut self,
1013 p: NodeId,
1014 ltable: &str,
1015 rtable: &str,
1016 on: &mut Vec<String>,
1017 wheres: &mut Vec<String>,
1018 ) -> Result<(), SqlError> {
1019 if self.prop_s(p, "kind") != "expr" {
1020 return Err(SqlError::Unsupported(
1021 "a positional predicate on the joined side".into(),
1022 ));
1023 }
1024 fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
1027 if ex.kind(e) == "and" {
1028 for c in ex.arbor.children(e) {
1029 conjuncts(ex, c, out);
1030 }
1031 } else {
1032 out.push(e);
1033 }
1034 }
1035 let mut parts = Vec::new();
1036 for c in self.arbor.children(p) {
1037 conjuncts(self, c, &mut parts);
1038 }
1039 for e in parts {
1040 let uses_ctx = self.subtree_has(e, "context");
1041 let cond = self.pred_expr(e, Some(rtable))?.replace("__LEFT__", ltable);
1042 if uses_ctx && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
1043 self.collect_left_cols(e)?;
1050 on.push(cond);
1051 } else {
1052 wheres.push(cond);
1053 }
1054 }
1055 Ok(())
1056 }
1057
1058 fn collect_left_cols(&mut self, e: NodeId) -> Result<(), SqlError> {
1062 if self.kind(e) == "context"
1063 && matches!(self.prop(e, "index"), None | Some(Value::Int(1)))
1064 && let Some(p) = self.kid(e, "projection")
1065 {
1066 let col = self.projection_col(p)?;
1067 self.join_on_left_cols.push(col);
1068 }
1069 for c in self.arbor.children(e) {
1070 self.collect_left_cols(c)?;
1071 }
1072 Ok(())
1073 }
1074
1075 fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
1076 if self.kind(n) == kind {
1077 return true;
1078 }
1079 self.arbor
1080 .children(n)
1081 .into_iter()
1082 .any(|c| self.subtree_has(c, kind))
1083 }
1084
1085 fn pipeline(
1087 &mut self,
1088 q: NodeId,
1089 sel: &mut Select,
1090 join: Option<(&str, &str)>,
1091 ) -> Result<(), SqlError> {
1092 let Some(pipe) = self.kid(q, "pipeline") else {
1093 return Ok(());
1094 };
1095 let stages: Vec<NodeId> = self.arbor.children(pipe);
1096 let mut i = 0;
1097 let mut pending_col: Option<String> = None;
1099 let mut grouped: Option<(String, String, String)> = None;
1101
1102 while i < stages.len() {
1103 let s = stages[i];
1104 match self.kind(s).as_str() {
1105 "expr" => {
1106 let kids = self.arbor.children(s);
1107 pending_col = Some(self.operand(kids[0], join.map(|(_, r)| r))?);
1108 }
1109 "func" => {
1110 let name = self.prop_s(s, "name");
1111 match name.as_str() {
1112 "rec" | "record" => {
1113 sel.select = self.record_fields(s, join)?;
1114 }
1115 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1118 let (key, _, _) = grouped.as_ref().ok_or_else(|| {
1119 SqlError::Unsupported(format!(
1120 "'| {name}' outside a group (use '@| {name}')"
1121 ))
1122 })?;
1123 let col = if name == "count" {
1127 pending_col = None;
1128 None
1129 } else {
1130 pending_col.take()
1131 };
1132 let agg = sql_agg(&name, col)?;
1133 grouped = Some((key.clone(), agg.clone(), agg));
1134 }
1135 other => {
1136 return Err(SqlError::Unsupported(format!(
1137 "the '{other}' pipeline function"
1138 )));
1139 }
1140 }
1141 }
1142 "push" => {
1143 if let Some((k, agg, _)) = grouped.take() {
1145 let alias = self.prop_s(s, "name");
1146 grouped = Some((k, agg, alias));
1147 } else {
1148 return Err(SqlError::Unsupported(
1149 "a register push (Quarb-side state)".into(),
1150 ));
1151 }
1152 }
1153 "filter" => {
1154 if grouped.is_some() {
1155 let cond = self.having_cond(s)?;
1158 sel.having = Some(cond);
1159 } else {
1160 return Err(SqlError::Unsupported(
1161 "a mid-pipeline filter (put it in the row predicate)".into(),
1162 ));
1163 }
1164 }
1165 "recall" => {
1166 if self.prop_s(s, "ref") != "%." {
1168 return Err(SqlError::Unsupported(
1169 "a register recall (Quarb-side state)".into(),
1170 ));
1171 }
1172 let (key, agg, alias) = grouped.clone().ok_or_else(|| {
1173 SqlError::Unsupported("'%.', with nothing grouped".into())
1174 })?;
1175 let alias = self.alias(&alias)?;
1176 sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
1177 if let Some(h) = sel.having.take() {
1180 sel.having = Some(h.replace("__AGG__", &agg));
1181 }
1182 }
1183 "agg" => {
1184 let name = self.prop_s(s, "name");
1185 match name.as_str() {
1186 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1187 let col = if name == "count" {
1191 pending_col = None;
1192 None
1193 } else {
1194 pending_col.take()
1195 };
1196 sel.select = vec![sql_agg(&name, col)?];
1197 self.aggregate = true;
1198 }
1199 "group" => {
1200 if self.strict {
1201 return Err(SqlError::Unsupported(
1202 "pushdown: GROUP BY result order is unordered in SQL".into(),
1203 ));
1204 }
1205 self.notes.push(
1206 "GROUP BY: SQL keeps a NULL-key group; Quarb's group \
1207 drops null keys"
1208 .to_string(),
1209 );
1210 let key = self.group_key(s, join)?;
1211 sel.group_by = Some(key.clone());
1212 grouped = Some((key, String::new(), String::new()));
1213 }
1214 "sort_by" => {
1215 if self.strict {
1216 return Err(SqlError::Unsupported(
1217 "pushdown: ORDER BY collations differ per engine".into(),
1218 ));
1219 }
1220 if sel.limit.is_some() {
1225 return Err(SqlError::Unsupported(
1226 "a sort after a positional selection (SQL orders \
1227 before LIMIT)"
1228 .into(),
1229 ));
1230 }
1231 if sel.order_by.is_some() {
1232 return Err(SqlError::Unsupported(
1233 "a second sort (SQL has a single ORDER BY)".into(),
1234 ));
1235 }
1236 let kids = self.arbor.children(s);
1237 let e = self.operand(kids[0], join.map(|(_, r)| r))?;
1238 sel.order_by = Some((e, false));
1239 }
1240 "reverse" => match &mut sel.order_by {
1241 Some((_, desc)) => *desc = true,
1242 None => {
1243 return Err(SqlError::Unsupported(
1244 "reverse without an ORDER BY (rows are unordered)".into(),
1245 ));
1246 }
1247 },
1248 "top" => {
1249 if self.strict {
1250 return Err(SqlError::Unsupported(
1251 "pushdown: ORDER BY collations differ per engine".into(),
1252 ));
1253 }
1254 if sel.limit.is_some() {
1255 return Err(SqlError::Unsupported(
1256 "'top' after a positional selection (SQL orders \
1257 before LIMIT)"
1258 .into(),
1259 ));
1260 }
1261 if sel.order_by.is_some() {
1262 return Err(SqlError::Unsupported(
1263 "a second sort (SQL has a single ORDER BY)".into(),
1264 ));
1265 }
1266 let kids = self.arbor.children(s);
1267 let n = self.prop_s(kids[0], "value");
1268 let e = self.operand(kids[1], join.map(|(_, r)| r))?;
1269 sel.order_by = Some((e, true));
1270 sel.limit = Some(n);
1271 }
1272 "unique" => {
1273 if self.strict {
1274 return Err(SqlError::Unsupported(
1275 "pushdown: DISTINCT result order is unordered in SQL".into(),
1276 ));
1277 }
1278 if sel.limit.is_some() {
1281 return Err(SqlError::Unsupported(
1282 "'unique' after a positional selection (SQL applies \
1283 DISTINCT before LIMIT)"
1284 .into(),
1285 ));
1286 }
1287 if let Some(c) = pending_col.take() {
1288 sel.select = vec![c];
1289 }
1290 sel.distinct = true;
1291 }
1292 other => {
1293 return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
1294 }
1295 }
1296 }
1297 "select" => {
1298 if self.strict {
1299 return Err(SqlError::Unsupported(
1300 "pushdown: LIMIT without a guaranteed order".into(),
1301 ));
1302 }
1303 if sel.limit.is_some() {
1305 return Err(SqlError::Unsupported(
1306 "a second positional selection".into(),
1307 ));
1308 }
1309 let p = self.arbor.children(s)[0];
1310 match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
1311 ("range", Some(Value::Int(n)))
1312 if self.prop(p, "from").is_none() && n > 0 =>
1313 {
1314 sel.limit = Some(n.to_string());
1315 }
1316 _ => {
1317 return Err(SqlError::Unsupported(
1318 "positional selection beyond '@| [..n]'".into(),
1319 ));
1320 }
1321 }
1322 }
1323 other => {
1324 return Err(SqlError::Unsupported(format!(
1325 "the '{other}' stage (windows, subcontexts, and registers are \
1326 Quarb-side state)"
1327 )));
1328 }
1329 }
1330 i += 1;
1331 }
1332 if let Some(c) = pending_col
1334 && sel.select.is_empty()
1335 {
1336 sel.select = vec![c];
1337 }
1338 Ok(())
1339 }
1340
1341 fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
1342 let kids = self.arbor.children(s);
1343 let key = kids
1346 .iter()
1347 .rev()
1348 .find(|&&k| self.kind(k) != "literal")
1349 .ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
1350 self.operand(*key, join.map(|(_, r)| r))
1351 }
1352
1353 fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
1355 let kids = self.arbor.children(s);
1356 if kids.len() != 1 || self.kind(kids[0]) != "compare" {
1357 return Err(SqlError::Unsupported(
1358 "HAVING translates for a single comparison".into(),
1359 ));
1360 }
1361 let e = kids[0];
1362 let op = self.prop_s(e, "op");
1363 let cmp_kids = self.arbor.children(e);
1364 let l = match self.kind(cmp_kids[0]).as_str() {
1365 "topic" | "recall" => "__AGG__".to_string(),
1366 _ => {
1367 return Err(SqlError::Unsupported(
1368 "HAVING compares the aggregate ($_ or its register)".into(),
1369 ));
1370 }
1371 };
1372 let r = self.operand(cmp_kids[1], None)?;
1373 Ok(format!("{l} {op} {r}"))
1374 }
1375
1376 fn record_fields(
1378 &mut self,
1379 s: NodeId,
1380 join: Option<(&str, &str)>,
1381 ) -> Result<Vec<String>, SqlError> {
1382 let kids = self.arbor.children(s);
1383 let mut fields = Vec::new();
1384 let mut i = 0;
1385 while i < kids.len() {
1386 let k = kids[i];
1387 if self.kind(k) == "literal" {
1388 let name = self.prop_s(k, "value");
1389 let value = self.operand(kids[i + 1], join.map(|(_, r)| r))?;
1390 let value = match join {
1391 Some((l, _)) => value.replace("__LEFT__", l),
1392 None => value,
1393 };
1394 let name = self.alias(&name)?;
1395 fields.push(format!("{value} AS {name}"));
1396 i += 2;
1397 } else {
1398 let value = self.operand(k, join.map(|(_, r)| r))?;
1399 let value = match join {
1400 Some((l, _)) => value.replace("__LEFT__", l),
1401 None => value,
1402 };
1403 fields.push(value);
1404 i += 1;
1405 }
1406 }
1407 Ok(fields)
1408 }
1409}
1410
1411fn sql_agg(name: &str, col: Option<String>) -> Result<String, SqlError> {
1412 let f = match name {
1413 "count" => "COUNT",
1414 "sum" => "SUM",
1415 "mean" | "avg" => "AVG",
1416 "min" => "MIN",
1417 "max" => "MAX",
1418 _ => unreachable!("checked by caller"),
1419 };
1420 match col {
1421 Some(c) => Ok(format!("{f}({c})")),
1422 None if f == "COUNT" => Ok("COUNT(*)".to_string()),
1425 None => Err(SqlError::Unsupported(format!(
1426 "'{name}' over row nodes (project a column first: '| ::col @| {name}')"
1427 ))),
1428 }
1429}
1430
1431#[cfg(test)]
1432mod null_and_literal_tests {
1433 use super::{Dialect, export, partial_pushdown, pushdown};
1434
1435 const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
1438
1439 #[test]
1440 fn json_column_path_pushdown_per_dialect() {
1441 let q = "/orders/*[/data/meta/tier:: = 'gold']::id";
1445 let sql = |d| pushdown(q, Some(d)).unwrap().sql;
1446 assert_eq!(
1447 sql(Dialect::Postgres),
1448 "SELECT id FROM orders WHERE (data::jsonb #>> '{meta,tier}') = 'gold'"
1449 );
1450 assert_eq!(
1451 sql(Dialect::MySql),
1452 "SELECT id FROM orders WHERE JSON_UNQUOTE(JSON_EXTRACT(data, '$.meta.tier')) = 'gold'"
1453 );
1454 assert_eq!(
1455 sql(Dialect::Sqlite),
1456 "SELECT id FROM orders WHERE json_extract(data, '$.meta.tier') = 'gold'"
1457 );
1458 assert_eq!(
1459 sql(Dialect::Mssql),
1460 "SELECT id FROM orders WHERE JSON_VALUE(data, '$.meta.tier') = 'gold'"
1461 );
1462 assert_eq!(
1463 sql(Dialect::Oracle),
1464 "SELECT id FROM orders WHERE JSON_VALUE(data, '$.meta.tier') = 'gold'"
1465 );
1466 assert!(pushdown(q, None).is_none());
1469 }
1470
1471 #[test]
1472 fn json_pushdown_only_string_equality() {
1473 let d = Some(Dialect::Sqlite);
1477 assert!(pushdown("/o/*[/data/n:: > 2]::id", d).is_none());
1478 assert!(pushdown("/o/*[/data/tier:: != 'gold']::id", d).is_none());
1479 assert!(pushdown("/o/*[/data/items/*/sku:: = 'A1']::id", d).is_none());
1480 }
1481
1482 #[test]
1483 fn null_literal_compares_use_is_null() {
1484 assert_eq!(
1488 export("/t/*[::x = null] | ::x").unwrap().query,
1489 "SELECT x FROM t WHERE x IS NULL"
1490 );
1491 assert_eq!(
1492 export("/t/*[::x != null] | ::x").unwrap().query,
1493 "SELECT x FROM t WHERE x IS NOT NULL"
1494 );
1495 assert_eq!(
1496 pushdown("/t/*[::x = null] | ::x", None).unwrap().sql,
1497 "SELECT x FROM t WHERE x IS NULL"
1498 );
1499 assert_eq!(
1500 pushdown("/t/*[::x != null] | ::x", None).unwrap().sql,
1501 "SELECT x FROM t WHERE x IS NOT NULL"
1502 );
1503 }
1504
1505 #[test]
1506 fn ne_and_not_refuse_pushdown_but_display_diverges() {
1507 assert!(pushdown("/t/*[::x != 5] | ::x", None).is_none());
1511 assert!(pushdown("/t/*[!::x = 5] | ::x", None).is_none());
1512 assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}")).is_none());
1513 assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}")).is_none());
1514 let t = export("/t/*[::x != 5] | ::x").unwrap();
1516 assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
1517 assert!(t.notes.iter().any(|n| n.contains("NULL")));
1518 }
1519
1520 #[test]
1521 fn unescapable_text_literal_refuses_pushdown() {
1522 assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path", None).is_none());
1526 assert!(pushdown("/t/*[::name = \"it's\"] | ::name", None).is_none());
1527 assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}")).is_none());
1528 assert_eq!(
1529 pushdown("/t/*[::name = \"rare\"] | ::name", None).unwrap().sql,
1530 "SELECT name FROM t WHERE name = 'rare'"
1531 );
1532 }
1533}