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
194fn json_valid_guard(dialect: Dialect, qual: Option<&str>, col: &str, cond: String) -> String {
204 let qcol = match qual {
205 Some(q) => format!("{q}.{col}"),
206 None => col.to_string(),
207 };
208 match dialect {
209 Dialect::Sqlite => format!("(json_valid({qcol}) AND {cond})"),
210 Dialect::MySql => format!("(JSON_VALID({qcol}) AND {cond})"),
211 Dialect::Mssql => format!("(ISJSON({qcol}) = 1 AND {cond})"),
212 Dialect::Postgres | Dialect::Oracle => format!("({cond})"),
213 }
214}
215
216fn json_type_probe(
222 dialect: Dialect,
223 qual: Option<&str>,
224 col: &str,
225 path: &[String],
226) -> Option<String> {
227 let qcol = match qual {
228 Some(q) => format!("{q}.{col}"),
229 None => col.to_string(),
230 };
231 match dialect {
232 Dialect::Sqlite => Some(format!("json_type({qcol}, '$.{}')", path.join("."))),
233 Dialect::Postgres => Some(format!(
234 "jsonb_typeof({qcol}::jsonb #> '{{{}}}')",
235 path.join(",")
236 )),
237 Dialect::MySql | Dialect::Mssql | Dialect::Oracle => None,
238 }
239}
240
241fn flip_op(op: &str) -> String {
244 match op {
245 "<" => ">".into(),
246 "<=" => ">=".into(),
247 ">" => "<".into(),
248 ">=" => "<=".into(),
249 other => other.into(),
250 }
251}
252
253const SQL_KEYWORDS: &[&str] = &[
258 "all",
259 "and",
260 "as",
261 "asc",
262 "by",
263 "case",
264 "cross",
265 "desc",
266 "distinct",
267 "else",
268 "end",
269 "except",
270 "exists",
271 "from",
272 "group",
273 "having",
274 "in",
275 "index",
276 "inner",
277 "intersect",
278 "into",
279 "is",
280 "join",
281 "left",
282 "like",
283 "limit",
284 "not",
285 "null",
286 "offset",
287 "on",
288 "or",
289 "order",
290 "outer",
291 "right",
292 "select",
293 "set",
294 "table",
295 "then",
296 "union",
297 "unique",
298 "update",
299 "using",
300 "values",
301 "when",
302 "where",
303];
304
305fn is_plain_ident(name: &str) -> bool {
309 !name.is_empty()
310 && name
311 .chars()
312 .next()
313 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
314 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
315 && !SQL_KEYWORDS.contains(&name.to_ascii_lowercase().as_str())
316}
317
318#[derive(Default)]
320struct Select {
321 select: Vec<String>,
322 distinct: bool,
323 from: String,
324 join: Option<(String, String)>, wheres: Vec<String>,
326 group_by: Option<String>,
327 having: Option<String>,
328 order_by: Option<(String, bool)>, limit: Option<String>,
330}
331
332impl Select {
333 fn render(&self) -> String {
334 let mut out = String::from("SELECT ");
335 if self.distinct {
336 out.push_str("DISTINCT ");
337 }
338 if self.select.is_empty() {
339 out.push('*');
340 } else {
341 out.push_str(&self.select.join(", "));
342 }
343 out.push_str(&format!(" FROM {}", self.from));
344 if let Some((t, on)) = &self.join {
345 out.push_str(&format!(" JOIN {t} ON {on}"));
346 }
347 if !self.wheres.is_empty() {
348 out.push_str(&format!(" WHERE {}", self.wheres.join(" AND ")));
349 }
350 if let Some(g) = &self.group_by {
351 out.push_str(&format!(" GROUP BY {g}"));
352 }
353 if let Some(h) = &self.having {
354 out.push_str(&format!(" HAVING {h}"));
355 }
356 if let Some((e, desc)) = &self.order_by {
357 out.push_str(&format!(" ORDER BY {e}"));
358 if *desc {
359 out.push_str(" DESC");
360 }
361 }
362 if let Some(n) = &self.limit {
363 out.push_str(&format!(" LIMIT {n}"));
364 }
365 out
366 }
367}
368
369pub struct Partial {
374 pub table: String,
375 pub where_sql: String,
376}
377
378pub fn partial_pushdown(quarb: &str, dialect: Option<Dialect>) -> Option<Partial> {
392 partial_pushdown_explained(quarb, dialect).ok()
393}
394
395pub fn partial_pushdown_explained(
402 quarb: &str,
403 dialect: Option<Dialect>,
404) -> Result<Partial, SqlError> {
405 refuse_marker(quarb)?;
406 let arbor =
407 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
408 refuse_groups(&arbor)?;
409 let mut ex = Exporter {
410 arbor,
411 notes: Vec::new(),
412 strict: true,
413 dialect,
414 from_table: String::new(),
415 join_on_cols: Vec::new(),
416 join_table: None,
417 from_sql: String::new(),
418 join_sql: None,
419 in_on: false,
420 aggregate: false,
421 };
422 ex.partial()
423}
424
425fn refuse_groups(arbor: &QueryArbor) -> Result<(), SqlError> {
431 let mut stack = vec![arbor.root()];
432 while let Some(n) = stack.pop() {
433 if arbor.name(n).as_deref() == Some("group") {
434 return Err(SqlError::Unsupported(
435 "path patterns (groups and quantifiers)".into(),
436 ));
437 }
438 stack.extend(arbor.children(n));
439 }
440 Ok(())
441}
442
443struct Exporter {
444 arbor: QueryArbor,
445 notes: Vec<String>,
446 strict: bool,
450 dialect: Option<Dialect>,
453 from_table: String,
454 join_on_cols: Vec<String>,
455 join_table: Option<String>,
456 from_sql: String,
457 join_sql: Option<String>,
458 in_on: bool,
459 aggregate: bool,
460}
461
462impl Exporter {
463 fn kids(&self, n: NodeId, kind: &str) -> Vec<NodeId> {
464 self.arbor
465 .children(n)
466 .into_iter()
467 .filter(|&c| self.arbor.name(c).as_deref() == Some(kind))
468 .collect()
469 }
470
471 fn kid(&self, n: NodeId, kind: &str) -> Option<NodeId> {
472 self.kids(n, kind).into_iter().next()
473 }
474
475 fn prop(&self, n: NodeId, key: &str) -> Option<Value> {
476 self.arbor.property(n, key)
477 }
478
479 fn prop_s(&self, n: NodeId, key: &str) -> String {
480 self.prop(n, key).map(|v| v.to_string()).unwrap_or_default()
481 }
482
483 fn kind(&self, n: NodeId) -> String {
484 self.arbor.name(n).unwrap_or_default()
485 }
486
487 fn is_null_literal(&self, n: NodeId) -> bool {
489 self.kind(n) == "literal" && self.prop_s(n, "type") == "null"
490 }
491
492 fn partial(&mut self) -> Result<Partial, SqlError> {
494 let root = self.arbor.root();
495 let q = self
496 .kid(root, "query")
497 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
498 if self.kid(q, "query").is_some() {
499 return Err(SqlError::Unsupported(
500 "correlations address other tables".into(),
501 ));
502 }
503 let branches = self.kids(q, "branch");
504 if branches.len() != 1 {
505 return Err(SqlError::Unsupported(
506 "a branch union".into(),
507 ));
508 }
509 let (table, preds) = self.table_branch(branches[0])?;
510
511 let all = self.walk_all(root);
513 let mut table_mentions = 0;
514 for n in &all {
515 match self.kind(*n).as_str() {
516 "step" => {
517 let axis = self.prop_s(*n, "axis");
518 if matches!(axis.as_str(), "->" | "<-" | "--" | "-->" | "<--") {
519 return Err(SqlError::Unsupported(
520 "crosslink/resolution axes could reach \
521 the filtered table"
522 .into(),
523 ));
524 }
525 if self.prop_s(*n, "matcher") == table {
526 table_mentions += 1;
527 }
528 }
529 "projection" if self.prop_s(*n, "kind") != "property" => {
530 return Err(SqlError::Unsupported(
531 "metadata would observe the filtering \
532 (;;;n-rows, :::index)"
533 .into(),
534 ));
535 }
536 _ => {}
537 }
538 }
539 if table_mentions != 1 {
540 return Err(SqlError::Unsupported(
541 "the table is reached more than once".into(),
542 ));
543 }
544
545 let mut conds = Vec::new();
557 for p in preds {
558 if self.prop_s(p, "kind") != "expr" {
559 break;
560 }
561 match self.predicate_cond(p, None) {
562 Ok(c) => conds.push(c),
563 Err(_) => {
564 if let Some(c) = self.prefilter_cond(p) {
565 conds.push(c);
566 }
567 }
568 }
569 }
570 if conds.is_empty() {
571 return Err(SqlError::Unsupported(
572 "no leading expression predicates to push".into(),
573 ));
574 }
575 Ok(Partial {
576 table,
577 where_sql: conds.join(" AND "),
578 })
579 }
580
581 fn prefilter_cond(&mut self, p: NodeId) -> Option<String> {
586 let parts: Vec<String> = self
587 .arbor
588 .children(p)
589 .into_iter()
590 .filter_map(|c| {
591 self.pred_expr(c, None)
592 .ok()
593 .or_else(|| self.prefilter_expr(c))
594 })
595 .collect();
596 if parts.is_empty() {
597 None
598 } else {
599 Some(parts.join(" AND "))
600 }
601 }
602
603 fn prefilter_expr(&mut self, e: NodeId) -> Option<String> {
611 let dialect = self.dialect?;
612 match self.kind(e).as_str() {
613 "path" => {
617 let (col, path) = self.json_path_loose(e)?;
618 let probe = json_type_probe(dialect, None, &col, &path)?;
619 Some(json_valid_guard(
620 dialect,
621 None,
622 &col,
623 format!("{probe} IS NOT NULL"),
624 ))
625 }
626 "compare" => {
627 let op = self.prop_s(e, "op");
628 let kids = self.arbor.children(e);
629 if kids.len() != 2 {
630 return None;
631 }
632 for (pi, li) in [(0usize, 1usize), (1, 0)] {
633 let Some((col, path)) = self.json_path(kids[pi]) else {
634 continue;
635 };
636 if self.kind(kids[li]) != "literal" {
637 continue;
638 }
639 let ty = self.prop_s(kids[li], "type");
640 if ty == "null" {
641 return None;
642 }
643 let raw = self
644 .prop(kids[li], "value")
645 .map(|v| v.to_string())
646 .unwrap_or_default();
647 let op = if pi == 0 { op.clone() } else { flip_op(&op) };
648 return self.json_prefilter(dialect, &col, &path, &op, &ty, &raw);
649 }
650 None
651 }
652 _ => None,
653 }
654 }
655
656 fn json_prefilter(
659 &self,
660 d: Dialect,
661 col: &str,
662 path: &[String],
663 op: &str,
664 lit_ty: &str,
665 raw: &str,
666 ) -> Option<String> {
667 if !matches!(op, "=" | "!=" | "<" | "<=" | ">" | ">=") {
668 return None;
669 }
670 if op == "!=" {
674 let probe = json_type_probe(d, None, col, path)?;
675 return Some(json_valid_guard(
676 d,
677 None,
678 col,
679 format!("{probe} IS NOT NULL"),
680 ));
681 }
682 let numeric_lit = raw.trim().parse::<f64>().is_ok_and(|f| f.is_finite())
683 && (lit_ty != "text" || !raw.trim().is_empty());
684 if numeric_lit {
685 let num: f64 = raw.trim().parse().ok()?;
686 if num.abs() >= 9_007_199_254_740_992.0 {
689 return None;
690 }
691 let lit = raw.trim();
692 match d {
693 Dialect::Sqlite => {
699 let ex = json_extract(d, None, col, path);
700 let ty = json_type_probe(d, None, col, path)?;
701 Some(json_valid_guard(
702 d,
703 None,
704 col,
705 format!("({ex} {op} {lit} OR {ty} = 'text')"),
706 ))
707 }
708 Dialect::Postgres => {
712 let text = json_extract(d, None, col, path);
713 let ty = json_type_probe(d, None, col, path)?;
714 Some(format!(
715 "(CASE WHEN {ty} = 'number' THEN ({text})::float8 {op} {lit} \
716 WHEN {ty} = 'string' THEN true ELSE false END)"
717 ))
718 }
719 Dialect::MySql | Dialect::Mssql | Dialect::Oracle => None,
720 }
721 } else if lit_ty == "text" {
722 if raw.contains('\'') || raw.contains('\\') {
723 return None;
724 }
725 match d {
726 Dialect::Sqlite => {
732 let ex = json_extract(d, None, col, path);
733 Some(json_valid_guard(
734 d,
735 None,
736 col,
737 format!("{ex} {op} '{raw}'"),
738 ))
739 }
740 _ => None,
743 }
744 } else {
745 None
746 }
747 }
748
749 fn json_path_loose(&self, o: NodeId) -> Option<(String, Vec<String>)> {
753 if self.json_path(o).is_some() {
754 return self.json_path(o);
755 }
756 if self.kind(o) != "path" || self.kid(o, "projection").is_some() {
757 return None;
758 }
759 let steps = self.kids(o, "step");
760 if steps.len() < 2 {
761 return None;
762 }
763 let mut names = Vec::with_capacity(steps.len());
764 for s in &steps {
765 if self.prop_s(*s, "axis") != "/"
766 || self.prop_s(*s, "matcher-kind") != "name"
767 || self.kid(*s, "predicate").is_some()
768 {
769 return None;
770 }
771 let name = self.prop_s(*s, "matcher");
772 let plain = !name.is_empty()
773 && name
774 .chars()
775 .next()
776 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
777 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
778 if !plain {
779 return None;
780 }
781 names.push(name);
782 }
783 let col = names.remove(0);
784 Some((col, names))
785 }
786
787 fn walk_all(&self, n: NodeId) -> Vec<NodeId> {
788 let mut out = vec![n];
789 let mut i = 0;
790 while i < out.len() {
791 out.extend(self.arbor.children(out[i]));
792 i += 1;
793 }
794 out
795 }
796
797 fn query(&mut self) -> Result<String, SqlError> {
798 let root = self.arbor.root();
799 let q = self
800 .kid(root, "query")
801 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
802 let mut sel = Select::default();
803
804 let corrs = self.kids(q, "query");
806 let branches = self.kids(q, "branch");
807 if branches.len() != 1 {
808 return Err(SqlError::Unsupported(
809 "a branch union (SQL has UNION, but result shapes differ; export one branch)"
810 .into(),
811 ));
812 }
813
814 if corrs.len() > 1 {
815 return Err(SqlError::Unsupported(
816 "more than one correlation context".into(),
817 ));
818 }
819 if let Some(corr) = corrs.first() {
820 let (ltable, lpreds) = self.table_branch(branches[0])?;
824 let (rtable, rpreds) = self.table_branch(self.kids(*corr, "branch")[0])?;
825 let lsql = self.sql_ident(<able, "table name")?;
829 let rsql = self.sql_ident(&rtable, "table name")?;
830 self.from_sql = lsql.clone();
831 self.join_sql = Some(rsql.clone());
832 let driver_conds: Vec<NodeId> = lpreds;
834 let mut on = Vec::new();
837 let mut wheres = Vec::new();
838 for p in rpreds {
839 self.split_join_pred(p, &rsql, &mut on, &mut wheres)?;
840 }
841 if on.is_empty() {
842 return Err(SqlError::Unsupported(
843 "a correlation without a '$$' equality (no JOIN condition)".into(),
844 ));
845 }
846 for p in driver_conds {
847 let cond = self.predicate_cond(p, Some(&lsql.clone()))?;
848 wheres.push(cond);
849 }
850 self.notes.push(
851 "JOIN: Quarb's binding is existential (one row per FROM row, \
852 bound to its first witness); SQL multiplies rows when \
853 several joined rows match"
854 .to_string(),
855 );
856 sel.from = lsql.clone();
857 self.from_table = ltable;
858 self.join_table = Some(rtable);
859 sel.join = Some((rsql.clone(), on.join(" AND ")));
860 sel.wheres = wheres;
861 if let Some(proj) = self.kid(branches[0], "projection") {
864 let col = self.projection_col(proj)?;
865 let col = self.sql_ident(&col, "column name")?;
866 sel.select.push(format!("{lsql}.{col}"));
867 }
868 if self.kid(self.kids(*corr, "branch")[0], "projection").is_some() {
869 return Err(SqlError::Unsupported(
870 "a projection on the joined expression (project the witness \
871 in the pipeline: '$*1::col')"
872 .into(),
873 ));
874 }
875 self.pipeline(q, &mut sel, Some((&lsql, &rsql)))?;
876 } else {
877 let (table, preds) = self.table_branch(branches[0])?;
878 sel.from = self.sql_ident(&table, "table name")?;
879 self.from_table = table;
880 for p in preds {
881 let cond = self.predicate_cond(p, None)?;
882 sel.wheres.push(cond);
883 }
884 if let Some(proj) = self.kid(branches[0], "projection") {
886 let col = self.projection_col(proj)?;
887 sel.select.push(self.sql_ident(&col, "column name")?);
888 }
889 self.pipeline(q, &mut sel, None)?;
890 }
891 Ok(sel.render())
892 }
893
894 fn table_branch(&mut self, b: NodeId) -> Result<(String, Vec<NodeId>), SqlError> {
897 let steps = self.kids(b, "step");
898 if steps.len() != 2 {
899 return Err(SqlError::Unsupported(
900 "navigation beyond /table/* (SQL sees tables and rows)".into(),
901 ));
902 }
903 let (t, rows) = (steps[0], steps[1]);
904 if self.prop_s(t, "axis") != "/"
905 || self.prop_s(t, "matcher-kind") != "name"
906 || self.prop_s(rows, "axis") != "/"
907 || self.prop_s(rows, "matcher-kind") != "any"
908 {
909 return Err(SqlError::Unsupported(
910 "navigation beyond /table/* (SQL sees tables and rows)".into(),
911 ));
912 }
913 if self.kid(t, "predicate").is_some() {
914 return Err(SqlError::Unsupported("a predicate on the table hop".into()));
915 }
916 Ok((self.prop_s(t, "matcher"), self.kids(rows, "predicate")))
917 }
918
919 fn predicate_cond(&mut self, p: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
922 if self.prop_s(p, "kind") != "expr" {
923 return Err(SqlError::Unsupported(
924 "a positional predicate on rows (SQL rows are unordered; ORDER BY + LIMIT)".into(),
925 ));
926 }
927 let parts: Vec<String> = self
928 .arbor
929 .children(p)
930 .into_iter()
931 .map(|c| self.pred_expr(c, qual))
932 .collect::<Result<_, _>>()?;
933 Ok(parts.join(" AND "))
934 }
935
936 fn pred_expr(&mut self, e: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
937 match self.kind(e).as_str() {
938 "and" | "or" => {
939 let op = self.kind(e).to_uppercase();
940 let kids: Vec<String> = self
941 .arbor
942 .children(e)
943 .into_iter()
944 .map(|c| self.pred_expr(c, qual))
945 .collect::<Result<_, _>>()?;
946 Ok(format!("({})", kids.join(&format!(" {op} "))))
947 }
948 "not" => {
949 if self.strict {
955 return Err(SqlError::Semantics(
956 "SQL NOT propagates UNKNOWN, dropping the NULL rows \
957 Quarb keeps"
958 .into(),
959 ));
960 }
961 let inner: Vec<String> = self
962 .arbor
963 .children(e)
964 .into_iter()
965 .map(|c| self.pred_expr(c, qual))
966 .collect::<Result<_, _>>()?;
967 Ok(format!("NOT ({})", inner.join(" AND ")))
968 }
969 "parens" => {
970 let inner: Vec<String> = self
971 .arbor
972 .children(e)
973 .into_iter()
974 .map(|c| self.pred_expr(c, qual))
975 .collect::<Result<_, _>>()?;
976 Ok(format!("({})", inner.join(" AND ")))
977 }
978 "compare" => {
979 let op = self.prop_s(e, "op");
980 let kids = self.arbor.children(e);
981 if matches!(op.as_str(), "=" | "!=")
990 && (self.is_null_literal(kids[0]) || self.is_null_literal(kids[1]))
991 {
992 let other = if self.is_null_literal(kids[0]) {
993 kids[1]
994 } else {
995 kids[0]
996 };
997 let col = self.operand(other, qual)?;
998 return Ok(if op == "=" {
999 format!("{col} IS NULL")
1000 } else {
1001 format!("{col} IS NOT NULL")
1002 });
1003 }
1004 if op == "="
1020 && let Some(dialect) = self.dialect
1021 {
1022 for (pi, li) in [(0usize, 1usize), (1, 0)] {
1023 if self.is_text_literal(kids[li])
1024 && let Some((col, path)) = self.json_path(kids[pi])
1025 {
1026 let raw = self
1035 .prop(kids[li], "value")
1036 .map(|v| v.to_string())
1037 .unwrap_or_default();
1038 if raw.trim().parse::<f64>().is_ok() {
1039 return Err(SqlError::Semantics(
1040 "a numeric-looking text literal against a \
1041 JSON path compares numerically in Quarb \
1042 (value coercion); no SQL extractor \
1043 matches that"
1044 .into(),
1045 ));
1046 }
1047 let lit = self.operand(kids[li], qual)?;
1048 let extract = json_extract(dialect, qual, &col, &path);
1049 return Ok(json_valid_guard(
1050 dialect,
1051 qual,
1052 &col,
1053 format!("{extract} = {lit}"),
1054 ));
1055 }
1056 }
1057 }
1058 if self.dialect.is_some() {
1065 for side in [kids[0], kids[1]] {
1066 if self.json_path(side).is_some()
1067 || self.json_path_loose(side).is_some()
1068 {
1069 return Err(SqlError::Semantics(
1070 "a JSON-path comparison pushes whole only as \
1071 fixed-path string equality (Quarb coerces \
1072 numeric-looking values; no extractor matches \
1073 that); this shape rides the partial prefilter"
1074 .into(),
1075 ));
1076 }
1077 }
1078 }
1079 let l = self.operand(kids[0], qual)?;
1080 if self.kind(kids[1]) == "pattern" {
1088 if op.as_str() != "=" {
1089 return Err(SqlError::Unsupported(format!(
1090 "a pattern literal on '{op}' (only '=' pushes)"
1091 )));
1092 }
1093 if self.strict && self.dialect != Some(Dialect::Postgres) {
1101 return Err(SqlError::Semantics(
1102 "LIKE case folding differs per engine \
1103 (provably case-sensitive only on PostgreSQL)"
1104 .into(),
1105 ));
1106 }
1107 if !self.strict {
1108 self.notes.push(
1109 "pattern → LIKE: Quarb's pattern test is \
1110 case-sensitive; LIKE folds case on \
1111 SQLite/MySQL but not PostgreSQL"
1112 .to_string(),
1113 );
1114 }
1115 let mut like = String::new();
1116 for kid in self.arbor.children(kids[1]) {
1117 match self.kind(kid).as_str() {
1118 "star" => like.push('%'),
1119 _ => {
1120 let raw = self
1121 .prop(kid, "value")
1122 .map(|v| v.to_string())
1123 .unwrap_or_default();
1124 like.push_str(
1125 &raw.replace('\\', "\\\\")
1126 .replace('%', "\\%")
1127 .replace('_', "\\_")
1128 .replace('\'', "''"),
1129 );
1130 }
1131 }
1132 }
1133 return Ok(format!("{l} LIKE '{like}' ESCAPE '\\'"));
1134 }
1135 let r = self.operand(kids[1], qual)?;
1136 Ok(match op.as_str() {
1137 "=" => format!("{l} = {r}"),
1138 "!=" => {
1139 if self.strict {
1147 return Err(SqlError::Semantics(
1148 "'!=' drops the NULL rows Quarb keeps \
1149 (SQL '<>' is UNKNOWN for NULL; use '!= null' \
1150 for IS NOT NULL)"
1151 .into(),
1152 ));
1153 }
1154 self.notes.push(
1155 "'!=' → '<>': Quarb keeps rows whose column is NULL; \
1156 SQL's '<>' drops them (use '!= null' for IS NOT NULL)"
1157 .to_string(),
1158 );
1159 format!("{l} <> {r}")
1160 }
1161 "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
1162 "*=" => {
1163 if self.strict && self.dialect != Some(Dialect::Postgres) {
1168 return Err(SqlError::Semantics(
1169 "LIKE case folding differs per engine \
1170 (provably case-sensitive only on PostgreSQL)"
1171 .into(),
1172 ));
1173 }
1174 if !self.is_text_literal(kids[1]) {
1179 return Err(SqlError::Unsupported(
1180 "'*=' with a non-literal pattern".into(),
1181 ));
1182 }
1183 self.notes.push(
1184 "*= → LIKE: Quarb's substring test is case-sensitive; LIKE \
1185 folds case on SQLite/MySQL but not PostgreSQL"
1186 .to_string(),
1187 );
1188 let raw = self
1193 .prop(kids[1], "value")
1194 .map(|v| v.to_string())
1195 .unwrap_or_default();
1196 let pat = raw
1197 .replace('\\', "\\\\")
1198 .replace('%', "\\%")
1199 .replace('_', "\\_")
1200 .replace('\'', "''");
1201 format!("{l} LIKE '%{pat}%' ESCAPE '\\'")
1202 }
1203 "=~" | "!~" => {
1204 return Err(SqlError::Semantics(
1205 "regex matching means a different REGEXP engine per \
1206 backend; regexes run engine-side"
1207 .into(),
1208 ));
1209 }
1210 other => {
1211 return Err(SqlError::Unsupported(format!("the '{other}' comparison")));
1212 }
1213 })
1214 }
1215 _ => {
1217 if self.strict {
1218 return Err(SqlError::Semantics(
1219 "truthiness diverges (0 and '' are falsy in Quarb)".into(),
1220 ));
1221 }
1222 self.notes.push(
1223 "truthiness: '[::c]' exports as IS NOT NULL, but Quarb also treats \
1224 0 and '' as falsy"
1225 .to_string(),
1226 );
1227 Ok(format!("{} IS NOT NULL", self.operand(e, qual)?))
1228 }
1229 }
1230 }
1231
1232 fn operand(&mut self, o: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
1233 match self.kind(o).as_str() {
1234 "literal" => {
1235 let v = self.prop(o, "value").unwrap_or(Value::Null);
1236 match self.prop_s(o, "type").as_str() {
1237 "text" => {
1238 let s = v.to_string();
1239 if self.strict && (s.contains('\'') || s.contains('\\')) {
1250 return Err(SqlError::Semantics(
1251 "a text literal with a quote or backslash \
1252 has no escaping portable across SQL dialects"
1253 .into(),
1254 ));
1255 }
1256 Ok(format!("'{}'", s.replace('\'', "''")))
1257 }
1258 "null" => Ok("NULL".to_string()),
1259 _ => Ok(v.to_string()),
1260 }
1261 }
1262 "path" => {
1263 if self.kid(o, "step").is_some() {
1264 let s = self.kids(o, "step")[0];
1267 if self.prop_s(s, "axis") == "-->" {
1268 return Err(SqlError::Unsupported(
1269 "a '~>' resolution chain: the foreign-key targets live in \
1270 the schema, not the query — spell the join with '<=>' to \
1271 export it"
1272 .into(),
1273 ));
1274 }
1275 return Err(SqlError::Unsupported(
1276 "navigation inside a predicate (SQL rows are flat)".into(),
1277 ));
1278 }
1279 let p = self
1280 .kid(o, "projection")
1281 .ok_or_else(|| SqlError::Unsupported("an empty path operand".into()))?;
1282 let col = self.projection_col(p)?;
1283 let col = self.sql_ident(&col, "column name")?;
1284 Ok(match qual {
1285 Some(q) => format!("{q}.{col}"),
1286 None => col,
1287 })
1288 }
1289 "context" => {
1290 let p = self
1294 .kid(o, "projection")
1295 .ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
1296 let col = self.projection_col(p)?;
1297 let col = self.sql_ident(&col, "column name")?;
1298 if self.in_on {
1299 return Err(SqlError::Unsupported(
1300 "a '$*' reference inside the ON (the driver is '$$')".into(),
1301 ));
1302 }
1303 let Some(join) = self.join_sql.clone() else {
1304 return Err(SqlError::Unsupported(
1305 "a '$*' reference outside a correlation join".into(),
1306 ));
1307 };
1308 match self.prop(o, "index") {
1309 Some(Value::Int(1)) => Ok(format!("{join}.{col}")),
1310 None => Err(SqlError::Unsupported("a bare '$*' reference".into())),
1311 Some(v) => Err(SqlError::Unsupported(format!(
1312 "pushdown: $*{v} beyond a two-branch correlation"
1313 ))),
1314 }
1315 }
1316 "outer" => {
1317 if !self.in_on {
1321 return Err(SqlError::Unsupported(
1322 "a '$$' reference outside the join's ON".into(),
1323 ));
1324 }
1325 let kids = self.arbor.children(o);
1326 let inner = *kids
1327 .first()
1328 .ok_or_else(|| SqlError::Unsupported("an empty '$$' reference".into()))?;
1329 if self.kind(inner) != "path" || self.kid(inner, "step").is_some() {
1330 return Err(SqlError::Unsupported(
1331 "a '$$' reference beyond a plain column ($$::col)".into(),
1332 ));
1333 }
1334 let p = self
1335 .kid(inner, "projection")
1336 .ok_or_else(|| SqlError::Unsupported("a bare '$$' reference".into()))?;
1337 let col = self.projection_col(p)?;
1338 let col = self.sql_ident(&col, "column name")?;
1339 Ok(format!("{}.{col}", self.from_sql.clone()))
1340 }
1341 "arith" => {
1342 let op = self.prop_s(o, "op");
1343 let kids = self.arbor.children(o);
1344 let l = self.operand(kids[0], qual)?;
1345 let r = self.operand(kids[1], qual)?;
1346 Ok(match op.as_str() {
1347 "+" | "-" | "*" => format!("({l} {op} {r})"),
1348 "div" => format!("({l} / {r})"),
1349 "mod" => format!("({l} % {r})"),
1350 other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
1351 })
1352 }
1353 other => Err(SqlError::Unsupported(format!(
1354 "the '{other}' operand (registers, topics, and captures are Quarb-side state)"
1355 ))),
1356 }
1357 }
1358
1359 fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
1360 match self.prop_s(p, "kind").as_str() {
1361 "property" => match self.prop(p, "key") {
1362 Some(k) => Ok(k.to_string()),
1363 None => Err(SqlError::Unsupported(
1364 "the bare '::' projection (name the column)".into(),
1365 )),
1366 },
1367 other => Err(SqlError::Unsupported(format!(
1368 "the {other} metadata projection"
1369 ))),
1370 }
1371 }
1372
1373 fn is_text_literal(&self, o: NodeId) -> bool {
1375 self.kind(o) == "literal" && self.prop_s(o, "type") == "text"
1376 }
1377
1378 fn json_path(&self, o: NodeId) -> Option<(String, Vec<String>)> {
1385 if self.kind(o) != "path" {
1386 return None;
1387 }
1388 let steps = self.kids(o, "step");
1389 if steps.len() < 2 {
1390 return None;
1391 }
1392 let proj = self.kid(o, "projection")?;
1395 if self.prop_s(proj, "kind") != "property" || self.prop(proj, "key").is_some() {
1396 return None;
1397 }
1398 let mut names = Vec::with_capacity(steps.len());
1399 for s in &steps {
1400 if self.prop_s(*s, "axis") != "/"
1401 || self.prop_s(*s, "matcher-kind") != "name"
1402 || self.kid(*s, "predicate").is_some()
1403 {
1404 return None;
1405 }
1406 let name = self.prop_s(*s, "matcher");
1407 let plain = !name.is_empty()
1410 && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
1411 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
1412 if !plain {
1413 return None;
1414 }
1415 names.push(name);
1416 }
1417 let col = names.remove(0);
1418 Some((col, names))
1419 }
1420
1421 fn alias(&mut self, name: &str) -> Result<String, SqlError> {
1425 if is_plain_ident(name) {
1426 return Ok(name.to_string());
1427 }
1428 if self.strict {
1429 return Err(SqlError::Unsupported(format!(
1430 "pushdown: field name {name:?} needs SQL quoting \
1431 (dialects disagree); rename the field"
1432 )));
1433 }
1434 self.notes
1435 .push(format!("field name {name:?} double-quoted (ANSI)"));
1436 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1437 }
1438
1439 fn sql_ident(&mut self, name: &str, what: &str) -> Result<String, SqlError> {
1446 if is_plain_ident(name) {
1447 return Ok(name.to_string());
1448 }
1449 if self.strict {
1450 return Err(SqlError::Unsupported(format!(
1451 "pushdown: {what} {name:?} is not a plain SQL identifier"
1452 )));
1453 }
1454 self.notes
1455 .push(format!("{what} {name:?} double-quoted (ANSI)"));
1456 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1457 }
1458
1459 fn split_join_pred(
1463 &mut self,
1464 p: NodeId,
1465 rtable: &str,
1466 on: &mut Vec<String>,
1467 wheres: &mut Vec<String>,
1468 ) -> Result<(), SqlError> {
1469 if self.prop_s(p, "kind") != "expr" {
1470 return Err(SqlError::Unsupported(
1471 "a positional predicate on the joined side".into(),
1472 ));
1473 }
1474 fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
1477 if ex.kind(e) == "and" {
1478 for c in ex.arbor.children(e) {
1479 conjuncts(ex, c, out);
1480 }
1481 } else {
1482 out.push(e);
1483 }
1484 }
1485 let mut parts = Vec::new();
1486 for c in self.arbor.children(p) {
1487 conjuncts(self, c, &mut parts);
1488 }
1489 for e in parts {
1490 let uses_driver = self.subtree_has(e, "outer");
1491 self.in_on = true;
1492 let cond = self.pred_expr(e, Some(rtable));
1493 self.in_on = false;
1494 let cond = cond?;
1495 if uses_driver && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
1496 self.collect_join_cols(e)?;
1503 on.push(cond);
1504 } else {
1505 wheres.push(cond);
1506 }
1507 }
1508 Ok(())
1509 }
1510
1511 fn collect_join_cols(&mut self, e: NodeId) -> Result<(), SqlError> {
1515 if self.kind(e) == "outer" {
1518 return Ok(());
1519 }
1520 if self.kind(e) == "path"
1521 && self.kid(e, "step").is_none()
1522 && let Some(p) = self.kid(e, "projection")
1523 {
1524 let col = self.projection_col(p)?;
1525 self.join_on_cols.push(col);
1526 }
1527 for c in self.arbor.children(e) {
1528 self.collect_join_cols(c)?;
1529 }
1530 Ok(())
1531 }
1532
1533 fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
1534 if self.kind(n) == kind {
1535 return true;
1536 }
1537 self.arbor
1538 .children(n)
1539 .into_iter()
1540 .any(|c| self.subtree_has(c, kind))
1541 }
1542
1543 fn pipeline(
1545 &mut self,
1546 q: NodeId,
1547 sel: &mut Select,
1548 join: Option<(&str, &str)>,
1549 ) -> Result<(), SqlError> {
1550 let Some(pipe) = self.kid(q, "pipeline") else {
1551 return Ok(());
1552 };
1553 let stages: Vec<NodeId> = self.arbor.children(pipe);
1554 let mut i = 0;
1555 let mut pending_col: Option<String> = None;
1557 let mut grouped: Option<(String, String, String)> = None;
1559
1560 while i < stages.len() {
1561 let s = stages[i];
1562 match self.kind(s).as_str() {
1563 "expr" => {
1564 let kids = self.arbor.children(s);
1565 pending_col = Some(self.operand(kids[0], join.map(|(l, _)| l))?);
1566 }
1567 "func" => {
1568 let name = self.prop_s(s, "name");
1569 match name.as_str() {
1570 "rec" | "record" => {
1571 sel.select = self.record_fields(s, join)?;
1572 }
1573 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1576 let (key, _, _) = grouped.as_ref().ok_or_else(|| {
1577 SqlError::Unsupported(format!(
1578 "'| {name}' outside a group (use '@| {name}')"
1579 ))
1580 })?;
1581 let col = if name == "count" {
1585 pending_col = None;
1586 None
1587 } else {
1588 pending_col.take()
1589 };
1590 let agg = sql_agg(&name, col)?;
1591 grouped = Some((key.clone(), agg.clone(), agg));
1592 }
1593 other => {
1594 if matches!(other, "sort" | "sort_by" | "unique" | "top" | "bottom") {
1600 return Err(SqlError::Semantics(format!(
1601 "'{other}' means the backend's collation and \
1602 duplicate semantics; one engine-side ordering \
1603 keeps it identical everywhere"
1604 )));
1605 }
1606 return Err(SqlError::Unsupported(format!(
1607 "the '{other}' pipeline function"
1608 )));
1609 }
1610 }
1611 }
1612 "push" => {
1613 if let Some((k, agg, _)) = grouped.take() {
1615 let alias = self.prop_s(s, "name");
1616 grouped = Some((k, agg, alias));
1617 } else {
1618 return Err(SqlError::Unsupported(
1619 "a register push (Quarb-side state)".into(),
1620 ));
1621 }
1622 }
1623 "filter" => {
1624 if grouped.is_some() {
1625 let cond = self.having_cond(s)?;
1628 sel.having = Some(cond);
1629 } else {
1630 return Err(SqlError::Unsupported(
1631 "a mid-pipeline filter (put it in the row predicate)".into(),
1632 ));
1633 }
1634 }
1635 "recall" => {
1636 if self.prop_s(s, "ref") != "%." {
1638 return Err(SqlError::Unsupported(
1639 "a register recall (Quarb-side state)".into(),
1640 ));
1641 }
1642 let (key, agg, alias) = grouped.clone().ok_or_else(|| {
1643 SqlError::Unsupported("'%.', with nothing grouped".into())
1644 })?;
1645 let alias = self.alias(&alias)?;
1646 sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
1647 if let Some(h) = sel.having.take() {
1650 sel.having = Some(h.replace("__AGG__", &agg));
1651 }
1652 }
1653 "agg" => {
1654 let name = self.prop_s(s, "name");
1655 match name.as_str() {
1656 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1657 let col = if name == "count" {
1661 pending_col = None;
1662 None
1663 } else {
1664 pending_col.take()
1665 };
1666 sel.select = vec![sql_agg(&name, col)?];
1667 self.aggregate = true;
1668 }
1669 "group" => {
1670 if self.strict {
1671 return Err(SqlError::Semantics(
1672 "GROUP BY result order is unordered in SQL".into(),
1673 ));
1674 }
1675 self.notes.push(
1676 "GROUP BY: SQL keeps a NULL-key group; Quarb's group \
1677 drops null keys"
1678 .to_string(),
1679 );
1680 let key = self.group_key(s, join)?;
1681 sel.group_by = Some(key.clone());
1682 grouped = Some((key, String::new(), String::new()));
1683 }
1684 "sort_by" => {
1685 if self.strict {
1686 return Err(SqlError::Semantics(
1687 "ORDER BY collations differ per engine".into(),
1688 ));
1689 }
1690 if sel.limit.is_some() {
1695 return Err(SqlError::Unsupported(
1696 "a sort after a positional selection (SQL orders \
1697 before LIMIT)"
1698 .into(),
1699 ));
1700 }
1701 if sel.order_by.is_some() {
1702 return Err(SqlError::Unsupported(
1703 "a second sort (SQL has a single ORDER BY)".into(),
1704 ));
1705 }
1706 let kids = self.arbor.children(s);
1707 let e = self.operand(kids[0], join.map(|(l, _)| l))?;
1708 sel.order_by = Some((e, false));
1709 }
1710 "reverse" => match &mut sel.order_by {
1711 Some((_, desc)) => *desc = true,
1712 None => {
1713 return Err(SqlError::Unsupported(
1714 "reverse without an ORDER BY (rows are unordered)".into(),
1715 ));
1716 }
1717 },
1718 "top" => {
1719 if self.strict {
1720 return Err(SqlError::Semantics(
1721 "ORDER BY collations differ per engine".into(),
1722 ));
1723 }
1724 if sel.limit.is_some() {
1725 return Err(SqlError::Unsupported(
1726 "'top' after a positional selection (SQL orders \
1727 before LIMIT)"
1728 .into(),
1729 ));
1730 }
1731 if sel.order_by.is_some() {
1732 return Err(SqlError::Unsupported(
1733 "a second sort (SQL has a single ORDER BY)".into(),
1734 ));
1735 }
1736 let kids = self.arbor.children(s);
1737 let n = self.prop_s(kids[0], "value");
1738 let e = self.operand(kids[1], join.map(|(l, _)| l))?;
1739 sel.order_by = Some((e, true));
1740 sel.limit = Some(n);
1741 }
1742 "unique" => {
1743 if self.strict {
1744 return Err(SqlError::Unsupported(
1745 "pushdown: DISTINCT result order is unordered in SQL".into(),
1746 ));
1747 }
1748 if sel.limit.is_some() {
1751 return Err(SqlError::Unsupported(
1752 "'unique' after a positional selection (SQL applies \
1753 DISTINCT before LIMIT)"
1754 .into(),
1755 ));
1756 }
1757 if let Some(c) = pending_col.take() {
1758 sel.select = vec![c];
1759 }
1760 sel.distinct = true;
1761 }
1762 other => {
1763 return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
1764 }
1765 }
1766 }
1767 "select" => {
1768 if self.strict {
1769 return Err(SqlError::Unsupported(
1770 "pushdown: LIMIT without a guaranteed order".into(),
1771 ));
1772 }
1773 if sel.limit.is_some() {
1775 return Err(SqlError::Unsupported(
1776 "a second positional selection".into(),
1777 ));
1778 }
1779 let p = self.arbor.children(s)[0];
1780 match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
1781 ("range", Some(Value::Int(n)))
1782 if self.prop(p, "from").is_none() && n > 0 =>
1783 {
1784 sel.limit = Some(n.to_string());
1785 }
1786 _ => {
1787 return Err(SqlError::Unsupported(
1788 "positional selection beyond '@| [..n]'".into(),
1789 ));
1790 }
1791 }
1792 }
1793 other => {
1794 return Err(SqlError::Unsupported(format!(
1795 "the '{other}' stage (windows, subcontexts, and registers are \
1796 Quarb-side state)"
1797 )));
1798 }
1799 }
1800 i += 1;
1801 }
1802 if let Some(c) = pending_col
1804 && sel.select.is_empty()
1805 {
1806 sel.select = vec![c];
1807 }
1808 Ok(())
1809 }
1810
1811 fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
1812 let kids = self.arbor.children(s);
1813 let key = kids
1816 .iter()
1817 .rev()
1818 .find(|&&k| self.kind(k) != "literal")
1819 .ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
1820 self.operand(*key, join.map(|(l, _)| l))
1821 }
1822
1823 fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
1825 let kids = self.arbor.children(s);
1826 if kids.len() != 1 || self.kind(kids[0]) != "compare" {
1827 return Err(SqlError::Unsupported(
1828 "HAVING translates for a single comparison".into(),
1829 ));
1830 }
1831 let e = kids[0];
1832 let op = self.prop_s(e, "op");
1833 let cmp_kids = self.arbor.children(e);
1834 let l = match self.kind(cmp_kids[0]).as_str() {
1835 "topic" | "recall" => "__AGG__".to_string(),
1836 _ => {
1837 return Err(SqlError::Unsupported(
1838 "HAVING compares the aggregate ($_ or its register)".into(),
1839 ));
1840 }
1841 };
1842 let r = self.operand(cmp_kids[1], None)?;
1843 Ok(format!("{l} {op} {r}"))
1844 }
1845
1846 fn record_fields(
1848 &mut self,
1849 s: NodeId,
1850 join: Option<(&str, &str)>,
1851 ) -> Result<Vec<String>, SqlError> {
1852 let kids = self.arbor.children(s);
1853 let mut fields = Vec::new();
1854 let mut i = 0;
1855 while i < kids.len() {
1856 let k = kids[i];
1857 if self.kind(k) == "literal" {
1858 let name = self.prop_s(k, "value");
1859 let value = self.operand(kids[i + 1], join.map(|(l, _)| l))?;
1860 let name = self.alias(&name)?;
1861 fields.push(format!("{value} AS {name}"));
1862 i += 2;
1863 } else {
1864 let value = self.operand(k, join.map(|(l, _)| l))?;
1865 fields.push(value);
1866 i += 1;
1867 }
1868 }
1869 Ok(fields)
1870 }
1871}
1872
1873fn sql_agg(name: &str, col: Option<String>) -> Result<String, SqlError> {
1874 let f = match name {
1875 "count" => "COUNT",
1876 "sum" => "SUM",
1877 "mean" | "avg" => "AVG",
1878 "min" => "MIN",
1879 "max" => "MAX",
1880 _ => unreachable!("checked by caller"),
1881 };
1882 match col {
1883 Some(c) => Ok(format!("{f}({c})")),
1884 None if f == "COUNT" => Ok("COUNT(*)".to_string()),
1887 None => Err(SqlError::Unsupported(format!(
1888 "'{name}' over row nodes (project a column first: '| ::col @| {name}')"
1889 ))),
1890 }
1891}
1892
1893#[cfg(test)]
1894mod null_and_literal_tests {
1895 use super::{Dialect, export, partial_pushdown, pushdown};
1896
1897 const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
1900
1901 #[test]
1902 fn pattern_literal_pushes_as_like() {
1903 let q = "/users/*[::name = 'app-'*]::id";
1908 let p = pushdown(q, Some(Dialect::Postgres)).unwrap();
1909 assert_eq!(
1910 p.sql,
1911 "SELECT id FROM users WHERE name LIKE 'app-%' ESCAPE '\\'"
1912 );
1913 assert!(pushdown(q, Some(Dialect::Sqlite)).is_none());
1914 assert!(pushdown(q, Some(Dialect::MySql)).is_none());
1915 let p = pushdown("/users/*[::name = *'web'*]::id", Some(Dialect::Postgres)).unwrap();
1917 assert!(p.sql.contains("LIKE '%web%'"), "{}", p.sql);
1918 let p = pushdown("/users/*[::name = *'a'*'.gz']::id", Some(Dialect::Postgres)).unwrap();
1919 assert!(p.sql.contains("LIKE '%a%.gz'"), "{}", p.sql);
1920 let p = pushdown("/users/*[::name = *'50%_x'*]::id", Some(Dialect::Postgres)).unwrap();
1922 assert!(p.sql.contains("LIKE '%50\\%\\_x%'"), "{}", p.sql);
1923 assert!(
1925 pushdown("/users/*[::name != *'web'*]::id", Some(Dialect::Postgres)).is_none()
1926 );
1927 let p = pushdown("/users/*[::name *= 'web']::id", Some(Dialect::Postgres)).unwrap();
1929 assert!(p.sql.contains("LIKE '%web%'"), "{}", p.sql);
1930 assert!(pushdown("/users/*[::name *= 'web']::id", Some(Dialect::Sqlite)).is_none());
1931 }
1932
1933 #[test]
1934 fn json_column_path_pushdown_per_dialect() {
1935 let q = "/orders/*[/data/meta/tier:: = 'gold']::id";
1939 let sql = |d| pushdown(q, Some(d)).unwrap().sql;
1940 assert_eq!(
1941 sql(Dialect::Postgres),
1942 "SELECT id FROM orders WHERE ((data::jsonb #>> '{meta,tier}') = 'gold')"
1943 );
1944 assert_eq!(
1945 sql(Dialect::MySql),
1946 "SELECT id FROM orders WHERE (JSON_VALID(data) AND \
1947 JSON_UNQUOTE(JSON_EXTRACT(data, '$.meta.tier')) = 'gold')"
1948 );
1949 assert_eq!(
1950 sql(Dialect::Sqlite),
1951 "SELECT id FROM orders WHERE (json_valid(data) AND \
1952 json_extract(data, '$.meta.tier') = 'gold')"
1953 );
1954 assert_eq!(
1955 sql(Dialect::Mssql),
1956 "SELECT id FROM orders WHERE (ISJSON(data) = 1 AND \
1957 JSON_VALUE(data, '$.meta.tier') = 'gold')"
1958 );
1959 assert_eq!(
1960 sql(Dialect::Oracle),
1961 "SELECT id FROM orders WHERE (JSON_VALUE(data, '$.meta.tier') = 'gold')"
1962 );
1963 assert!(pushdown(q, None).is_none());
1966 }
1967
1968 #[test]
1969 fn json_pushdown_only_string_equality() {
1970 let d = Some(Dialect::Sqlite);
1974 assert!(pushdown("/o/*[/data/n:: > 2]::id", d).is_none());
1975 assert!(pushdown("/o/*[/data/tier:: != 'gold']::id", d).is_none());
1976 assert!(pushdown("/o/*[/data/items/*/sku:: = 'A1']::id", d).is_none());
1977 }
1978
1979 #[test]
1980 fn json_pushdown_refuses_numeric_looking_literal() {
1981 let q = "/orders/*[/data/total:: = '150']::id";
1985 assert!(pushdown(q, Some(Dialect::Sqlite)).is_none());
1986 assert!(pushdown(q, Some(Dialect::Postgres)).is_none());
1987 }
1988
1989 #[test]
1990 fn partial_json_prefilters_sqlite() {
1991 let p = partial_pushdown(
1993 "/orders/*[/payload/customer/geo/city:: = 'Lyon']::id",
1994 Some(Dialect::Sqlite),
1995 )
1996 .unwrap();
1997 assert_eq!(p.table, "orders");
1998 assert_eq!(
1999 p.where_sql,
2000 "(json_valid(payload) AND \
2001 json_extract(payload, '$.customer.geo.city') = 'Lyon')"
2002 );
2003 let p = partial_pushdown(
2005 "/orders/*[/payload/total:: > 150]::id",
2006 Some(Dialect::Sqlite),
2007 )
2008 .unwrap();
2009 assert_eq!(
2010 p.where_sql,
2011 "(json_valid(payload) AND (json_extract(payload, '$.total') > 150 \
2012 OR json_type(payload, '$.total') = 'text'))"
2013 );
2014 let p = partial_pushdown("/orders/*[/payload/gift]::id", Some(Dialect::Sqlite)).unwrap();
2016 assert_eq!(
2017 p.where_sql,
2018 "(json_valid(payload) AND json_type(payload, '$.gift') IS NOT NULL)"
2019 );
2020 let p = partial_pushdown(
2022 "/orders/*[/payload/status:: != 'x']::id",
2023 Some(Dialect::Sqlite),
2024 )
2025 .unwrap();
2026 assert_eq!(
2027 p.where_sql,
2028 "(json_valid(payload) AND json_type(payload, '$.status') IS NOT NULL)"
2029 );
2030 let p = partial_pushdown(
2032 "/orders/*[150 < /payload/total::]::id",
2033 Some(Dialect::Sqlite),
2034 )
2035 .unwrap();
2036 assert!(p.where_sql.contains("> 150"), "{}", p.where_sql);
2037 assert!(partial_pushdown("/orders/*[/payload/gift]::id", None).is_none());
2039 }
2040
2041 #[test]
2042 fn partial_json_prefilters_postgres() {
2043 let p = partial_pushdown(
2044 "/orders/*[/payload/total:: > 150]::id",
2045 Some(Dialect::Postgres),
2046 )
2047 .unwrap();
2048 assert_eq!(
2049 p.where_sql,
2050 "(CASE WHEN jsonb_typeof(payload::jsonb #> '{total}') = 'number' \
2051 THEN ((payload::jsonb #>> '{total}'))::float8 > 150 \
2052 WHEN jsonb_typeof(payload::jsonb #> '{total}') = 'string' \
2053 THEN true ELSE false END)"
2054 );
2055 let p =
2056 partial_pushdown("/orders/*[/payload/gift]::id", Some(Dialect::Postgres)).unwrap();
2057 assert_eq!(
2058 p.where_sql,
2059 "(jsonb_typeof(payload::jsonb #> '{gift}') IS NOT NULL)"
2060 );
2061 }
2062
2063 #[test]
2064 fn partial_skips_untranslatable_keeps_prefix() {
2065 let p = partial_pushdown(
2069 "/orders/*[::status = 'shipped'][/payload/line_items/*/sku:: = 'X']::id",
2070 Some(Dialect::Sqlite),
2071 )
2072 .unwrap();
2073 assert_eq!(p.where_sql, "status = 'shipped'");
2074 }
2075
2076 #[test]
2077 fn null_literal_compares_use_is_null() {
2078 assert_eq!(
2082 export("/t/*[::x = null] | ::x").unwrap().query,
2083 "SELECT x FROM t WHERE x IS NULL"
2084 );
2085 assert_eq!(
2086 export("/t/*[::x != null] | ::x").unwrap().query,
2087 "SELECT x FROM t WHERE x IS NOT NULL"
2088 );
2089 assert_eq!(
2090 pushdown("/t/*[::x = null] | ::x", None).unwrap().sql,
2091 "SELECT x FROM t WHERE x IS NULL"
2092 );
2093 assert_eq!(
2094 pushdown("/t/*[::x != null] | ::x", None).unwrap().sql,
2095 "SELECT x FROM t WHERE x IS NOT NULL"
2096 );
2097 }
2098
2099 #[test]
2100 fn ne_and_not_refuse_pushdown_but_display_diverges() {
2101 assert!(pushdown("/t/*[::x != 5] | ::x", None).is_none());
2105 assert!(pushdown("/t/*[!::x = 5] | ::x", None).is_none());
2106 assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}"), None).is_none());
2107 assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}"), None).is_none());
2108 let t = export("/t/*[::x != 5] | ::x").unwrap();
2110 assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
2111 assert!(t.notes.iter().any(|n| n.contains("NULL")));
2112 }
2113
2114 #[test]
2115 fn unescapable_text_literal_refuses_pushdown() {
2116 assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path", None).is_none());
2120 assert!(pushdown("/t/*[::name = \"it's\"] | ::name", None).is_none());
2121 assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}"), None).is_none());
2122 assert_eq!(
2123 pushdown("/t/*[::name = \"rare\"] | ::name", None).unwrap().sql,
2124 "SELECT name FROM t WHERE name = 'rare'"
2125 );
2126 }
2127}