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.join_table.clone().map(|t| (t, ex.join_on_cols.clone()));
153 Ok(Pushdown {
154 sql,
155 order_table,
156 join_left,
157 })
158}
159
160fn json_extract(dialect: Dialect, qual: Option<&str>, col: &str, path: &[String]) -> String {
168 let qcol = match qual {
169 Some(q) => format!("{q}.{col}"),
170 None => col.to_string(),
171 };
172 match dialect {
173 Dialect::Postgres => format!("({qcol}::jsonb #>> '{{{}}}')", path.join(",")),
178 Dialect::MySql => {
182 format!("JSON_UNQUOTE(JSON_EXTRACT({qcol}, '$.{}'))", path.join("."))
183 }
184 Dialect::Sqlite => format!("json_extract({qcol}, '$.{}')", path.join(".")),
185 Dialect::Mssql | Dialect::Oracle => format!("JSON_VALUE({qcol}, '$.{}')", path.join(".")),
188 }
189}
190
191fn json_valid_guard(dialect: Dialect, qual: Option<&str>, col: &str, cond: String) -> String {
201 let qcol = match qual {
202 Some(q) => format!("{q}.{col}"),
203 None => col.to_string(),
204 };
205 match dialect {
206 Dialect::Sqlite => format!("(json_valid({qcol}) AND {cond})"),
207 Dialect::MySql => format!("(JSON_VALID({qcol}) AND {cond})"),
208 Dialect::Mssql => format!("(ISJSON({qcol}) = 1 AND {cond})"),
209 Dialect::Postgres | Dialect::Oracle => format!("({cond})"),
210 }
211}
212
213fn json_type_probe(
219 dialect: Dialect,
220 qual: Option<&str>,
221 col: &str,
222 path: &[String],
223) -> Option<String> {
224 let qcol = match qual {
225 Some(q) => format!("{q}.{col}"),
226 None => col.to_string(),
227 };
228 match dialect {
229 Dialect::Sqlite => Some(format!("json_type({qcol}, '$.{}')", path.join("."))),
230 Dialect::Postgres => Some(format!(
231 "jsonb_typeof({qcol}::jsonb #> '{{{}}}')",
232 path.join(",")
233 )),
234 Dialect::MySql | Dialect::Mssql | Dialect::Oracle => None,
235 }
236}
237
238fn flip_op(op: &str) -> String {
241 match op {
242 "<" => ">".into(),
243 "<=" => ">=".into(),
244 ">" => "<".into(),
245 ">=" => "<=".into(),
246 other => other.into(),
247 }
248}
249
250const SQL_KEYWORDS: &[&str] = &[
255 "all",
256 "and",
257 "as",
258 "asc",
259 "by",
260 "case",
261 "cross",
262 "desc",
263 "distinct",
264 "else",
265 "end",
266 "except",
267 "exists",
268 "from",
269 "group",
270 "having",
271 "in",
272 "index",
273 "inner",
274 "intersect",
275 "into",
276 "is",
277 "join",
278 "left",
279 "like",
280 "limit",
281 "not",
282 "null",
283 "offset",
284 "on",
285 "or",
286 "order",
287 "outer",
288 "right",
289 "select",
290 "set",
291 "table",
292 "then",
293 "union",
294 "unique",
295 "update",
296 "using",
297 "values",
298 "when",
299 "where",
300];
301
302fn is_plain_ident(name: &str) -> bool {
306 !name.is_empty()
307 && name
308 .chars()
309 .next()
310 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
311 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
312 && !SQL_KEYWORDS.contains(&name.to_ascii_lowercase().as_str())
313}
314
315#[derive(Default)]
317struct Select {
318 select: Vec<String>,
319 distinct: bool,
320 from: String,
321 join: Option<(String, String)>, wheres: Vec<String>,
323 group_by: Option<String>,
324 having: Option<String>,
325 order_by: Option<(String, bool)>, limit: Option<String>,
327}
328
329impl Select {
330 fn render(&self) -> String {
331 let mut out = String::from("SELECT ");
332 if self.distinct {
333 out.push_str("DISTINCT ");
334 }
335 if self.select.is_empty() {
336 out.push('*');
337 } else {
338 out.push_str(&self.select.join(", "));
339 }
340 out.push_str(&format!(" FROM {}", self.from));
341 if let Some((t, on)) = &self.join {
342 out.push_str(&format!(" JOIN {t} ON {on}"));
343 }
344 if !self.wheres.is_empty() {
345 out.push_str(&format!(" WHERE {}", self.wheres.join(" AND ")));
346 }
347 if let Some(g) = &self.group_by {
348 out.push_str(&format!(" GROUP BY {g}"));
349 }
350 if let Some(h) = &self.having {
351 out.push_str(&format!(" HAVING {h}"));
352 }
353 if let Some((e, desc)) = &self.order_by {
354 out.push_str(&format!(" ORDER BY {e}"));
355 if *desc {
356 out.push_str(" DESC");
357 }
358 }
359 if let Some(n) = &self.limit {
360 out.push_str(&format!(" LIMIT {n}"));
361 }
362 out
363 }
364}
365
366pub struct Partial {
371 pub table: String,
372 pub where_sql: String,
373}
374
375pub fn partial_pushdown(quarb: &str, dialect: Option<Dialect>) -> Option<Partial> {
389 partial_pushdown_explained(quarb, dialect).ok()
390}
391
392pub fn partial_pushdown_explained(
399 quarb: &str,
400 dialect: Option<Dialect>,
401) -> Result<Partial, SqlError> {
402 refuse_marker(quarb)?;
403 let arbor =
404 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
405 refuse_groups(&arbor)?;
406 let mut ex = Exporter {
407 arbor,
408 notes: Vec::new(),
409 strict: true,
410 dialect,
411 from_table: String::new(),
412 join_on_cols: Vec::new(),
413 join_table: None,
414 from_sql: String::new(),
415 join_sql: None,
416 in_on: false,
417 aggregate: false,
418 };
419 ex.partial()
420}
421
422fn refuse_groups(arbor: &QueryArbor) -> Result<(), SqlError> {
428 let mut stack = vec![arbor.root()];
429 while let Some(n) = stack.pop() {
430 if arbor.name(n).as_deref() == Some("group") {
431 return Err(SqlError::Unsupported(
432 "path patterns (groups and quantifiers)".into(),
433 ));
434 }
435 stack.extend(arbor.children(n));
436 }
437 Ok(())
438}
439
440struct Exporter {
441 arbor: QueryArbor,
442 notes: Vec<String>,
443 strict: bool,
447 dialect: Option<Dialect>,
450 from_table: String,
451 join_on_cols: Vec<String>,
452 join_table: Option<String>,
453 from_sql: String,
454 join_sql: Option<String>,
455 in_on: bool,
456 aggregate: bool,
457}
458
459impl Exporter {
460 fn kids(&self, n: NodeId, kind: &str) -> Vec<NodeId> {
461 self.arbor
462 .children(n)
463 .into_iter()
464 .filter(|&c| self.arbor.name(c).as_deref() == Some(kind))
465 .collect()
466 }
467
468 fn kid(&self, n: NodeId, kind: &str) -> Option<NodeId> {
469 self.kids(n, kind).into_iter().next()
470 }
471
472 fn prop(&self, n: NodeId, key: &str) -> Option<Value> {
473 self.arbor.property(n, key)
474 }
475
476 fn prop_s(&self, n: NodeId, key: &str) -> String {
477 self.prop(n, key).map(|v| v.to_string()).unwrap_or_default()
478 }
479
480 fn kind(&self, n: NodeId) -> String {
481 self.arbor.name(n).unwrap_or_default()
482 }
483
484 fn is_null_literal(&self, n: NodeId) -> bool {
486 self.kind(n) == "literal" && self.prop_s(n, "type") == "null"
487 }
488
489 fn partial(&mut self) -> Result<Partial, SqlError> {
491 let root = self.arbor.root();
492 let q = self
493 .kid(root, "query")
494 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
495 if self.kid(q, "query").is_some() {
496 return Err(SqlError::Unsupported(
497 "correlations address other tables".into(),
498 ));
499 }
500 let branches = self.kids(q, "branch");
501 if branches.len() != 1 {
502 return Err(SqlError::Unsupported("a branch union".into()));
503 }
504 let (table, preds) = self.table_branch(branches[0])?;
505
506 let all = self.walk_all(root);
508 let mut table_mentions = 0;
509 for n in &all {
510 match self.kind(*n).as_str() {
511 "step" => {
512 let axis = self.prop_s(*n, "axis");
513 if matches!(axis.as_str(), "->" | "<-" | "--" | "-->" | "<--") {
514 return Err(SqlError::Unsupported(
515 "crosslink/resolution axes could reach \
516 the filtered table"
517 .into(),
518 ));
519 }
520 if self.prop_s(*n, "matcher") == table {
521 table_mentions += 1;
522 }
523 }
524 "projection" if self.prop_s(*n, "kind") != "property" => {
525 return Err(SqlError::Unsupported(
526 "metadata would observe the filtering \
527 (;;;n-rows, :::index)"
528 .into(),
529 ));
530 }
531 _ => {}
532 }
533 }
534 if table_mentions != 1 {
535 return Err(SqlError::Unsupported(
536 "the table is reached more than once".into(),
537 ));
538 }
539
540 let mut conds = Vec::new();
552 for p in preds {
553 if self.prop_s(p, "kind") != "expr" {
554 break;
555 }
556 match self.predicate_cond(p, None) {
557 Ok(c) => conds.push(c),
558 Err(_) => {
559 if let Some(c) = self.prefilter_cond(p) {
560 conds.push(c);
561 }
562 }
563 }
564 }
565 if conds.is_empty() {
566 return Err(SqlError::Unsupported(
567 "no leading expression predicates to push".into(),
568 ));
569 }
570 Ok(Partial {
571 table,
572 where_sql: conds.join(" AND "),
573 })
574 }
575
576 fn prefilter_cond(&mut self, p: NodeId) -> Option<String> {
581 let parts: Vec<String> = self
582 .arbor
583 .children(p)
584 .into_iter()
585 .filter_map(|c| {
586 self.pred_expr(c, None)
587 .ok()
588 .or_else(|| self.prefilter_expr(c))
589 })
590 .collect();
591 if parts.is_empty() {
592 None
593 } else {
594 Some(parts.join(" AND "))
595 }
596 }
597
598 fn prefilter_expr(&mut self, e: NodeId) -> Option<String> {
606 let dialect = self.dialect?;
607 match self.kind(e).as_str() {
608 "path" => {
612 let (col, path) = self.json_path_loose(e)?;
613 let probe = json_type_probe(dialect, None, &col, &path)?;
614 Some(json_valid_guard(
615 dialect,
616 None,
617 &col,
618 format!("{probe} IS NOT NULL"),
619 ))
620 }
621 "compare" => {
622 let op = self.prop_s(e, "op");
623 let kids = self.arbor.children(e);
624 if kids.len() != 2 {
625 return None;
626 }
627 for (pi, li) in [(0usize, 1usize), (1, 0)] {
628 let Some((col, path)) = self.json_path(kids[pi]) else {
629 continue;
630 };
631 if self.kind(kids[li]) != "literal" {
632 continue;
633 }
634 let ty = self.prop_s(kids[li], "type");
635 if ty == "null" {
636 return None;
637 }
638 let raw = self
639 .prop(kids[li], "value")
640 .map(|v| v.to_string())
641 .unwrap_or_default();
642 let op = if pi == 0 { op.clone() } else { flip_op(&op) };
643 return self.json_prefilter(dialect, &col, &path, &op, &ty, &raw);
644 }
645 None
646 }
647 _ => None,
648 }
649 }
650
651 fn json_prefilter(
654 &self,
655 d: Dialect,
656 col: &str,
657 path: &[String],
658 op: &str,
659 lit_ty: &str,
660 raw: &str,
661 ) -> Option<String> {
662 if !matches!(op, "=" | "!=" | "<" | "<=" | ">" | ">=") {
663 return None;
664 }
665 if op == "!=" {
669 let probe = json_type_probe(d, None, col, path)?;
670 return Some(json_valid_guard(
671 d,
672 None,
673 col,
674 format!("{probe} IS NOT NULL"),
675 ));
676 }
677 let numeric_lit = raw.trim().parse::<f64>().is_ok_and(|f| f.is_finite())
678 && (lit_ty != "text" || !raw.trim().is_empty());
679 if numeric_lit {
680 let num: f64 = raw.trim().parse().ok()?;
681 if num.abs() >= 9_007_199_254_740_992.0 {
684 return None;
685 }
686 let lit = raw.trim();
687 match d {
688 Dialect::Sqlite => {
694 let ex = json_extract(d, None, col, path);
695 let ty = json_type_probe(d, None, col, path)?;
696 Some(json_valid_guard(
697 d,
698 None,
699 col,
700 format!("({ex} {op} {lit} OR {ty} = 'text')"),
701 ))
702 }
703 Dialect::Postgres => {
707 let text = json_extract(d, None, col, path);
708 let ty = json_type_probe(d, None, col, path)?;
709 Some(format!(
710 "(CASE WHEN {ty} = 'number' THEN ({text})::float8 {op} {lit} \
711 WHEN {ty} = 'string' THEN true ELSE false END)"
712 ))
713 }
714 Dialect::MySql | Dialect::Mssql | Dialect::Oracle => None,
715 }
716 } else if lit_ty == "text" {
717 if raw.contains('\'') || raw.contains('\\') {
718 return None;
719 }
720 match d {
721 Dialect::Sqlite => {
727 let ex = json_extract(d, None, col, path);
728 Some(json_valid_guard(d, None, col, format!("{ex} {op} '{raw}'")))
729 }
730 _ => None,
733 }
734 } else {
735 None
736 }
737 }
738
739 fn json_path_loose(&self, o: NodeId) -> Option<(String, Vec<String>)> {
743 if self.json_path(o).is_some() {
744 return self.json_path(o);
745 }
746 if self.kind(o) != "path" || self.kid(o, "projection").is_some() {
747 return None;
748 }
749 let steps = self.kids(o, "step");
750 if steps.len() < 2 {
751 return None;
752 }
753 let mut names = Vec::with_capacity(steps.len());
754 for s in &steps {
755 if self.prop_s(*s, "axis") != "/"
756 || self.prop_s(*s, "matcher-kind") != "name"
757 || self.kid(*s, "predicate").is_some()
758 {
759 return None;
760 }
761 let name = self.prop_s(*s, "matcher");
762 let plain = !name.is_empty()
763 && name
764 .chars()
765 .next()
766 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
767 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
768 if !plain {
769 return None;
770 }
771 names.push(name);
772 }
773 let col = names.remove(0);
774 Some((col, names))
775 }
776
777 fn walk_all(&self, n: NodeId) -> Vec<NodeId> {
778 let mut out = vec![n];
779 let mut i = 0;
780 while i < out.len() {
781 out.extend(self.arbor.children(out[i]));
782 i += 1;
783 }
784 out
785 }
786
787 fn query(&mut self) -> Result<String, SqlError> {
788 let root = self.arbor.root();
789 let q = self
790 .kid(root, "query")
791 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
792 let mut sel = Select::default();
793
794 let corrs = self.kids(q, "query");
796 let branches = self.kids(q, "branch");
797 if branches.len() != 1 {
798 return Err(SqlError::Unsupported(
799 "a branch union (SQL has UNION, but result shapes differ; export one branch)"
800 .into(),
801 ));
802 }
803
804 if corrs.len() > 1 {
805 return Err(SqlError::Unsupported(
806 "more than one correlation context".into(),
807 ));
808 }
809 if let Some(corr) = corrs.first() {
810 let (ltable, lpreds) = self.table_branch(branches[0])?;
814 let (rtable, rpreds) = self.table_branch(self.kids(*corr, "branch")[0])?;
815 let lsql = self.sql_ident(<able, "table name")?;
819 let rsql = self.sql_ident(&rtable, "table name")?;
820 self.from_sql = lsql.clone();
821 self.join_sql = Some(rsql.clone());
822 let driver_conds: Vec<NodeId> = lpreds;
824 let mut on = Vec::new();
827 let mut wheres = Vec::new();
828 for p in rpreds {
829 self.split_join_pred(p, &rsql, &mut on, &mut wheres)?;
830 }
831 if on.is_empty() {
832 return Err(SqlError::Unsupported(
833 "a correlation without a '$$' equality (no JOIN condition)".into(),
834 ));
835 }
836 for p in driver_conds {
837 let cond = self.predicate_cond(p, Some(&lsql.clone()))?;
838 wheres.push(cond);
839 }
840 self.notes.push(
841 "JOIN: Quarb's binding is existential (one row per FROM row, \
842 bound to its first witness); SQL multiplies rows when \
843 several joined rows match"
844 .to_string(),
845 );
846 sel.from = lsql.clone();
847 self.from_table = ltable;
848 self.join_table = Some(rtable);
849 sel.join = Some((rsql.clone(), on.join(" AND ")));
850 sel.wheres = wheres;
851 if let Some(proj) = self.kid(branches[0], "projection") {
854 let col = self.projection_col(proj)?;
855 let col = self.sql_ident(&col, "column name")?;
856 sel.select.push(format!("{lsql}.{col}"));
857 }
858 if self
859 .kid(self.kids(*corr, "branch")[0], "projection")
860 .is_some()
861 {
862 return Err(SqlError::Unsupported(
863 "a projection on the joined expression (project the witness \
864 in the pipeline: '$*1::col')"
865 .into(),
866 ));
867 }
868 self.pipeline(q, &mut sel, Some((&lsql, &rsql)))?;
869 } else {
870 let (table, preds) = self.table_branch(branches[0])?;
871 sel.from = self.sql_ident(&table, "table name")?;
872 self.from_table = table;
873 for p in preds {
874 let cond = self.predicate_cond(p, None)?;
875 sel.wheres.push(cond);
876 }
877 if let Some(proj) = self.kid(branches[0], "projection") {
879 let col = self.projection_col(proj)?;
880 sel.select.push(self.sql_ident(&col, "column name")?);
881 }
882 self.pipeline(q, &mut sel, None)?;
883 }
884 Ok(sel.render())
885 }
886
887 fn table_branch(&mut self, b: NodeId) -> Result<(String, Vec<NodeId>), SqlError> {
890 let steps = self.kids(b, "step");
891 if steps.len() != 2 {
892 return Err(SqlError::Unsupported(
893 "navigation beyond /table/* (SQL sees tables and rows)".into(),
894 ));
895 }
896 let (t, rows) = (steps[0], steps[1]);
897 if self.prop_s(t, "axis") != "/"
898 || self.prop_s(t, "matcher-kind") != "name"
899 || self.prop_s(rows, "axis") != "/"
900 || self.prop_s(rows, "matcher-kind") != "any"
901 {
902 return Err(SqlError::Unsupported(
903 "navigation beyond /table/* (SQL sees tables and rows)".into(),
904 ));
905 }
906 if self.kid(t, "predicate").is_some() {
907 return Err(SqlError::Unsupported("a predicate on the table hop".into()));
908 }
909 Ok((self.prop_s(t, "matcher"), self.kids(rows, "predicate")))
910 }
911
912 fn predicate_cond(&mut self, p: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
915 if self.prop_s(p, "kind") != "expr" {
916 return Err(SqlError::Unsupported(
917 "a positional predicate on rows (SQL rows are unordered; ORDER BY + LIMIT)".into(),
918 ));
919 }
920 let parts: Vec<String> = self
921 .arbor
922 .children(p)
923 .into_iter()
924 .map(|c| self.pred_expr(c, qual))
925 .collect::<Result<_, _>>()?;
926 Ok(parts.join(" AND "))
927 }
928
929 fn pred_expr(&mut self, e: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
930 match self.kind(e).as_str() {
931 "and" | "or" => {
932 let op = self.kind(e).to_uppercase();
933 let kids: Vec<String> = self
934 .arbor
935 .children(e)
936 .into_iter()
937 .map(|c| self.pred_expr(c, qual))
938 .collect::<Result<_, _>>()?;
939 Ok(format!("({})", kids.join(&format!(" {op} "))))
940 }
941 "not" => {
942 if self.strict {
948 return Err(SqlError::Semantics(
949 "SQL NOT propagates UNKNOWN, dropping the NULL rows \
950 Quarb keeps"
951 .into(),
952 ));
953 }
954 let inner: Vec<String> = self
955 .arbor
956 .children(e)
957 .into_iter()
958 .map(|c| self.pred_expr(c, qual))
959 .collect::<Result<_, _>>()?;
960 Ok(format!("NOT ({})", inner.join(" AND ")))
961 }
962 "parens" => {
963 let inner: Vec<String> = self
964 .arbor
965 .children(e)
966 .into_iter()
967 .map(|c| self.pred_expr(c, qual))
968 .collect::<Result<_, _>>()?;
969 Ok(format!("({})", inner.join(" AND ")))
970 }
971 "compare" => {
972 let op = self.prop_s(e, "op");
973 let kids = self.arbor.children(e);
974 if matches!(op.as_str(), "=" | "!=")
983 && (self.is_null_literal(kids[0]) || self.is_null_literal(kids[1]))
984 {
985 let other = if self.is_null_literal(kids[0]) {
986 kids[1]
987 } else {
988 kids[0]
989 };
990 let col = self.operand(other, qual)?;
991 return Ok(if op == "=" {
992 format!("{col} IS NULL")
993 } else {
994 format!("{col} IS NOT NULL")
995 });
996 }
997 if op == "="
1013 && let Some(dialect) = self.dialect
1014 {
1015 for (pi, li) in [(0usize, 1usize), (1, 0)] {
1016 if self.is_text_literal(kids[li])
1017 && let Some((col, path)) = self.json_path(kids[pi])
1018 {
1019 let raw = self
1028 .prop(kids[li], "value")
1029 .map(|v| v.to_string())
1030 .unwrap_or_default();
1031 if raw.trim().parse::<f64>().is_ok() {
1032 return Err(SqlError::Semantics(
1033 "a numeric-looking text literal against a \
1034 JSON path compares numerically in Quarb \
1035 (value coercion); no SQL extractor \
1036 matches that"
1037 .into(),
1038 ));
1039 }
1040 let lit = self.operand(kids[li], qual)?;
1041 let extract = json_extract(dialect, qual, &col, &path);
1042 return Ok(json_valid_guard(
1043 dialect,
1044 qual,
1045 &col,
1046 format!("{extract} = {lit}"),
1047 ));
1048 }
1049 }
1050 }
1051 if self.dialect.is_some() {
1058 for side in [kids[0], kids[1]] {
1059 if self.json_path(side).is_some() || self.json_path_loose(side).is_some() {
1060 return Err(SqlError::Semantics(
1061 "a JSON-path comparison pushes whole only as \
1062 fixed-path string equality (Quarb coerces \
1063 numeric-looking values; no extractor matches \
1064 that); this shape rides the partial prefilter"
1065 .into(),
1066 ));
1067 }
1068 }
1069 }
1070 let l = self.operand(kids[0], qual)?;
1071 if self.kind(kids[1]) == "pattern" {
1079 if op.as_str() != "=" {
1080 return Err(SqlError::Unsupported(format!(
1081 "a pattern literal on '{op}' (only '=' pushes)"
1082 )));
1083 }
1084 if self.strict && self.dialect != Some(Dialect::Postgres) {
1092 return Err(SqlError::Semantics(
1093 "LIKE case folding differs per engine \
1094 (provably case-sensitive only on PostgreSQL)"
1095 .into(),
1096 ));
1097 }
1098 if !self.strict {
1099 self.notes.push(
1100 "pattern → LIKE: Quarb's pattern test is \
1101 case-sensitive; LIKE folds case on \
1102 SQLite/MySQL but not PostgreSQL"
1103 .to_string(),
1104 );
1105 }
1106 let mut like = String::new();
1107 for kid in self.arbor.children(kids[1]) {
1108 match self.kind(kid).as_str() {
1109 "star" => like.push('%'),
1110 _ => {
1111 let raw = self
1112 .prop(kid, "value")
1113 .map(|v| v.to_string())
1114 .unwrap_or_default();
1115 like.push_str(
1116 &raw.replace('\\', "\\\\")
1117 .replace('%', "\\%")
1118 .replace('_', "\\_")
1119 .replace('\'', "''"),
1120 );
1121 }
1122 }
1123 }
1124 return Ok(format!("{l} LIKE '{like}' ESCAPE '\\'"));
1125 }
1126 let r = self.operand(kids[1], qual)?;
1127 Ok(match op.as_str() {
1128 "=" => format!("{l} = {r}"),
1129 "!=" => {
1130 if self.strict {
1138 return Err(SqlError::Semantics(
1139 "'!=' drops the NULL rows Quarb keeps \
1140 (SQL '<>' is UNKNOWN for NULL; use '!= null' \
1141 for IS NOT NULL)"
1142 .into(),
1143 ));
1144 }
1145 self.notes.push(
1146 "'!=' → '<>': Quarb keeps rows whose column is NULL; \
1147 SQL's '<>' drops them (use '!= null' for IS NOT NULL)"
1148 .to_string(),
1149 );
1150 format!("{l} <> {r}")
1151 }
1152 "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
1153 "*=" => {
1154 if self.strict && self.dialect != Some(Dialect::Postgres) {
1159 return Err(SqlError::Semantics(
1160 "LIKE case folding differs per engine \
1161 (provably case-sensitive only on PostgreSQL)"
1162 .into(),
1163 ));
1164 }
1165 if !self.is_text_literal(kids[1]) {
1170 return Err(SqlError::Unsupported(
1171 "'*=' with a non-literal pattern".into(),
1172 ));
1173 }
1174 self.notes.push(
1175 "*= → LIKE: Quarb's substring test is case-sensitive; LIKE \
1176 folds case on SQLite/MySQL but not PostgreSQL"
1177 .to_string(),
1178 );
1179 let raw = self
1184 .prop(kids[1], "value")
1185 .map(|v| v.to_string())
1186 .unwrap_or_default();
1187 let pat = raw
1188 .replace('\\', "\\\\")
1189 .replace('%', "\\%")
1190 .replace('_', "\\_")
1191 .replace('\'', "''");
1192 format!("{l} LIKE '%{pat}%' ESCAPE '\\'")
1193 }
1194 "==" | "!==" | "=~" | "!~" => {
1195 return Err(SqlError::Semantics(
1196 "regex matching means a different REGEXP engine per \
1197 backend; regexes run engine-side"
1198 .into(),
1199 ));
1200 }
1201 other => {
1202 return Err(SqlError::Unsupported(format!("the '{other}' comparison")));
1203 }
1204 })
1205 }
1206 _ => {
1208 if self.strict {
1209 return Err(SqlError::Semantics(
1210 "truthiness diverges (0 and '' are falsy in Quarb)".into(),
1211 ));
1212 }
1213 self.notes.push(
1214 "truthiness: '[::c]' exports as IS NOT NULL, but Quarb also treats \
1215 0 and '' as falsy"
1216 .to_string(),
1217 );
1218 Ok(format!("{} IS NOT NULL", self.operand(e, qual)?))
1219 }
1220 }
1221 }
1222
1223 fn operand(&mut self, o: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
1224 match self.kind(o).as_str() {
1225 "literal" => {
1226 let v = self.prop(o, "value").unwrap_or(Value::Null);
1227 match self.prop_s(o, "type").as_str() {
1228 "text" => {
1229 let s = v.to_string();
1230 if self.strict && (s.contains('\'') || s.contains('\\')) {
1241 return Err(SqlError::Semantics(
1242 "a text literal with a quote or backslash \
1243 has no escaping portable across SQL dialects"
1244 .into(),
1245 ));
1246 }
1247 Ok(format!("'{}'", s.replace('\'', "''")))
1248 }
1249 "null" => Ok("NULL".to_string()),
1250 _ => Ok(v.to_string()),
1251 }
1252 }
1253 "path" => {
1254 if self.kid(o, "step").is_some() {
1255 let s = self.kids(o, "step")[0];
1258 if self.prop_s(s, "axis") == "-->" {
1259 return Err(SqlError::Unsupported(
1260 "a '~>' resolution chain: the foreign-key targets live in \
1261 the schema, not the query — spell the join with '<=>' to \
1262 export it"
1263 .into(),
1264 ));
1265 }
1266 return Err(SqlError::Unsupported(
1267 "navigation inside a predicate (SQL rows are flat)".into(),
1268 ));
1269 }
1270 let p = self
1271 .kid(o, "projection")
1272 .ok_or_else(|| SqlError::Unsupported("an empty path operand".into()))?;
1273 let col = self.projection_col(p)?;
1274 let col = self.sql_ident(&col, "column name")?;
1275 Ok(match qual {
1276 Some(q) => format!("{q}.{col}"),
1277 None => col,
1278 })
1279 }
1280 "context" => {
1281 let p = self
1285 .kid(o, "projection")
1286 .ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
1287 let col = self.projection_col(p)?;
1288 let col = self.sql_ident(&col, "column name")?;
1289 if self.in_on {
1290 return Err(SqlError::Unsupported(
1291 "a '$*' reference inside the ON (the driver is '$$')".into(),
1292 ));
1293 }
1294 let Some(join) = self.join_sql.clone() else {
1295 return Err(SqlError::Unsupported(
1296 "a '$*' reference outside a correlation join".into(),
1297 ));
1298 };
1299 match self.prop(o, "index") {
1300 Some(Value::Int(1)) => Ok(format!("{join}.{col}")),
1301 None => Err(SqlError::Unsupported("a bare '$*' reference".into())),
1302 Some(v) => Err(SqlError::Unsupported(format!(
1303 "pushdown: $*{v} beyond a two-branch correlation"
1304 ))),
1305 }
1306 }
1307 "outer" => {
1308 if !self.in_on {
1312 return Err(SqlError::Unsupported(
1313 "a '$$' reference outside the join's ON".into(),
1314 ));
1315 }
1316 let kids = self.arbor.children(o);
1317 let inner = *kids
1318 .first()
1319 .ok_or_else(|| SqlError::Unsupported("an empty '$$' reference".into()))?;
1320 if self.kind(inner) != "path" || self.kid(inner, "step").is_some() {
1321 return Err(SqlError::Unsupported(
1322 "a '$$' reference beyond a plain column ($$::col)".into(),
1323 ));
1324 }
1325 let p = self
1326 .kid(inner, "projection")
1327 .ok_or_else(|| SqlError::Unsupported("a bare '$$' reference".into()))?;
1328 let col = self.projection_col(p)?;
1329 let col = self.sql_ident(&col, "column name")?;
1330 Ok(format!("{}.{col}", self.from_sql.clone()))
1331 }
1332 "arith" => {
1333 let op = self.prop_s(o, "op");
1334 let kids = self.arbor.children(o);
1335 let l = self.operand(kids[0], qual)?;
1336 let r = self.operand(kids[1], qual)?;
1337 Ok(match op.as_str() {
1338 "+" | "-" | "*" => format!("({l} {op} {r})"),
1339 "div" => format!("({l} / {r})"),
1340 "mod" => format!("({l} % {r})"),
1341 other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
1342 })
1343 }
1344 other => Err(SqlError::Unsupported(format!(
1345 "the '{other}' operand (registers, topics, and captures are Quarb-side state)"
1346 ))),
1347 }
1348 }
1349
1350 fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
1351 match self.prop_s(p, "kind").as_str() {
1352 "property" => match self.prop(p, "key") {
1353 Some(k) => Ok(k.to_string()),
1354 None => Err(SqlError::Unsupported(
1355 "the bare '::' projection (name the column)".into(),
1356 )),
1357 },
1358 other => Err(SqlError::Unsupported(format!(
1359 "the {other} metadata projection"
1360 ))),
1361 }
1362 }
1363
1364 fn is_text_literal(&self, o: NodeId) -> bool {
1366 self.kind(o) == "literal" && self.prop_s(o, "type") == "text"
1367 }
1368
1369 fn json_path(&self, o: NodeId) -> Option<(String, Vec<String>)> {
1376 if self.kind(o) != "path" {
1377 return None;
1378 }
1379 let steps = self.kids(o, "step");
1380 if steps.len() < 2 {
1381 return None;
1382 }
1383 let proj = self.kid(o, "projection")?;
1386 if self.prop_s(proj, "kind") != "property" || self.prop(proj, "key").is_some() {
1387 return None;
1388 }
1389 let mut names = Vec::with_capacity(steps.len());
1390 for s in &steps {
1391 if self.prop_s(*s, "axis") != "/"
1392 || self.prop_s(*s, "matcher-kind") != "name"
1393 || self.kid(*s, "predicate").is_some()
1394 {
1395 return None;
1396 }
1397 let name = self.prop_s(*s, "matcher");
1398 let plain = !name.is_empty()
1401 && name
1402 .chars()
1403 .next()
1404 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
1405 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
1406 if !plain {
1407 return None;
1408 }
1409 names.push(name);
1410 }
1411 let col = names.remove(0);
1412 Some((col, names))
1413 }
1414
1415 fn alias(&mut self, name: &str) -> Result<String, SqlError> {
1419 if is_plain_ident(name) {
1420 return Ok(name.to_string());
1421 }
1422 if self.strict {
1423 return Err(SqlError::Unsupported(format!(
1424 "pushdown: field name {name:?} needs SQL quoting \
1425 (dialects disagree); rename the field"
1426 )));
1427 }
1428 self.notes
1429 .push(format!("field name {name:?} double-quoted (ANSI)"));
1430 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1431 }
1432
1433 fn sql_ident(&mut self, name: &str, what: &str) -> Result<String, SqlError> {
1440 if is_plain_ident(name) {
1441 return Ok(name.to_string());
1442 }
1443 if self.strict {
1444 return Err(SqlError::Unsupported(format!(
1445 "pushdown: {what} {name:?} is not a plain SQL identifier"
1446 )));
1447 }
1448 self.notes
1449 .push(format!("{what} {name:?} double-quoted (ANSI)"));
1450 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1451 }
1452
1453 fn split_join_pred(
1457 &mut self,
1458 p: NodeId,
1459 rtable: &str,
1460 on: &mut Vec<String>,
1461 wheres: &mut Vec<String>,
1462 ) -> Result<(), SqlError> {
1463 if self.prop_s(p, "kind") != "expr" {
1464 return Err(SqlError::Unsupported(
1465 "a positional predicate on the joined side".into(),
1466 ));
1467 }
1468 fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
1471 if ex.kind(e) == "and" {
1472 for c in ex.arbor.children(e) {
1473 conjuncts(ex, c, out);
1474 }
1475 } else {
1476 out.push(e);
1477 }
1478 }
1479 let mut parts = Vec::new();
1480 for c in self.arbor.children(p) {
1481 conjuncts(self, c, &mut parts);
1482 }
1483 for e in parts {
1484 let uses_driver = self.subtree_has(e, "outer");
1485 self.in_on = true;
1486 let cond = self.pred_expr(e, Some(rtable));
1487 self.in_on = false;
1488 let cond = cond?;
1489 if uses_driver && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
1490 self.collect_join_cols(e)?;
1497 on.push(cond);
1498 } else {
1499 wheres.push(cond);
1500 }
1501 }
1502 Ok(())
1503 }
1504
1505 fn collect_join_cols(&mut self, e: NodeId) -> Result<(), SqlError> {
1509 if self.kind(e) == "outer" {
1512 return Ok(());
1513 }
1514 if self.kind(e) == "path"
1515 && self.kid(e, "step").is_none()
1516 && let Some(p) = self.kid(e, "projection")
1517 {
1518 let col = self.projection_col(p)?;
1519 self.join_on_cols.push(col);
1520 }
1521 for c in self.arbor.children(e) {
1522 self.collect_join_cols(c)?;
1523 }
1524 Ok(())
1525 }
1526
1527 fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
1528 if self.kind(n) == kind {
1529 return true;
1530 }
1531 self.arbor
1532 .children(n)
1533 .into_iter()
1534 .any(|c| self.subtree_has(c, kind))
1535 }
1536
1537 fn pipeline(
1539 &mut self,
1540 q: NodeId,
1541 sel: &mut Select,
1542 join: Option<(&str, &str)>,
1543 ) -> Result<(), SqlError> {
1544 let Some(pipe) = self.kid(q, "pipeline") else {
1545 return Ok(());
1546 };
1547 let stages: Vec<NodeId> = self.arbor.children(pipe);
1548 let mut i = 0;
1549 let mut pending_col: Option<String> = None;
1551 let mut grouped: Option<(String, String, String)> = None;
1553
1554 while i < stages.len() {
1555 let s = stages[i];
1556 match self.kind(s).as_str() {
1557 "expr" => {
1558 let kids = self.arbor.children(s);
1559 pending_col = Some(self.operand(kids[0], join.map(|(l, _)| l))?);
1560 }
1561 "func" => {
1562 let name = self.prop_s(s, "name");
1563 match name.as_str() {
1564 "rec" | "record" => {
1565 sel.select = self.record_fields(s, join)?;
1566 }
1567 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1570 let (key, _, _) = grouped.as_ref().ok_or_else(|| {
1571 SqlError::Unsupported(format!(
1572 "'| {name}' outside a group (use '@| {name}')"
1573 ))
1574 })?;
1575 let col = if name == "count" {
1579 pending_col = None;
1580 None
1581 } else {
1582 pending_col.take()
1583 };
1584 let agg = sql_agg(&name, col)?;
1585 grouped = Some((key.clone(), agg.clone(), agg));
1586 }
1587 other => {
1588 if matches!(other, "sort" | "sort_by" | "unique" | "top" | "bottom") {
1594 return Err(SqlError::Semantics(format!(
1595 "'{other}' means the backend's collation and \
1596 duplicate semantics; one engine-side ordering \
1597 keeps it identical everywhere"
1598 )));
1599 }
1600 return Err(SqlError::Unsupported(format!(
1601 "the '{other}' pipeline function"
1602 )));
1603 }
1604 }
1605 }
1606 "push" => {
1607 if let Some((k, agg, _)) = grouped.take() {
1609 let alias = self.prop_s(s, "name");
1610 grouped = Some((k, agg, alias));
1611 } else {
1612 return Err(SqlError::Unsupported(
1613 "a register push (Quarb-side state)".into(),
1614 ));
1615 }
1616 }
1617 "filter" => {
1618 if grouped.is_some() {
1619 let cond = self.having_cond(s)?;
1622 sel.having = Some(cond);
1623 } else {
1624 return Err(SqlError::Unsupported(
1625 "a mid-pipeline filter (put it in the row predicate)".into(),
1626 ));
1627 }
1628 }
1629 "recall" => {
1630 if self.prop_s(s, "ref") != "%." {
1632 return Err(SqlError::Unsupported(
1633 "a register recall (Quarb-side state)".into(),
1634 ));
1635 }
1636 let (key, agg, alias) = grouped.clone().ok_or_else(|| {
1637 SqlError::Unsupported("'%.', with nothing grouped".into())
1638 })?;
1639 let alias = self.alias(&alias)?;
1640 sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
1641 if let Some(h) = sel.having.take() {
1644 sel.having = Some(h.replace("__AGG__", &agg));
1645 }
1646 }
1647 "agg" => {
1648 let name = self.prop_s(s, "name");
1649 match name.as_str() {
1650 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1651 let col = if name == "count" {
1655 pending_col = None;
1656 None
1657 } else {
1658 pending_col.take()
1659 };
1660 sel.select = vec![sql_agg(&name, col)?];
1661 self.aggregate = true;
1662 }
1663 "group" => {
1664 if self.strict {
1665 return Err(SqlError::Semantics(
1666 "GROUP BY result order is unordered in SQL".into(),
1667 ));
1668 }
1669 self.notes.push(
1670 "GROUP BY: SQL keeps a NULL-key group; Quarb's group \
1671 drops null keys"
1672 .to_string(),
1673 );
1674 let key = self.group_key(s, join)?;
1675 sel.group_by = Some(key.clone());
1676 grouped = Some((key, String::new(), String::new()));
1677 }
1678 "sort_by" => {
1679 if self.strict {
1680 return Err(SqlError::Semantics(
1681 "ORDER BY collations differ per engine".into(),
1682 ));
1683 }
1684 if sel.limit.is_some() {
1689 return Err(SqlError::Unsupported(
1690 "a sort after a positional selection (SQL orders \
1691 before LIMIT)"
1692 .into(),
1693 ));
1694 }
1695 if sel.order_by.is_some() {
1696 return Err(SqlError::Unsupported(
1697 "a second sort (SQL has a single ORDER BY)".into(),
1698 ));
1699 }
1700 let kids = self.arbor.children(s);
1701 let e = self.operand(kids[0], join.map(|(l, _)| l))?;
1702 sel.order_by = Some((e, false));
1703 }
1704 "reverse" => match &mut sel.order_by {
1705 Some((_, desc)) => *desc = true,
1706 None => {
1707 return Err(SqlError::Unsupported(
1708 "reverse without an ORDER BY (rows are unordered)".into(),
1709 ));
1710 }
1711 },
1712 "top" => {
1713 if self.strict {
1714 return Err(SqlError::Semantics(
1715 "ORDER BY collations differ per engine".into(),
1716 ));
1717 }
1718 if sel.limit.is_some() {
1719 return Err(SqlError::Unsupported(
1720 "'top' after a positional selection (SQL orders \
1721 before LIMIT)"
1722 .into(),
1723 ));
1724 }
1725 if sel.order_by.is_some() {
1726 return Err(SqlError::Unsupported(
1727 "a second sort (SQL has a single ORDER BY)".into(),
1728 ));
1729 }
1730 let kids = self.arbor.children(s);
1731 let n = self.prop_s(kids[0], "value");
1732 let e = self.operand(kids[1], join.map(|(l, _)| l))?;
1733 sel.order_by = Some((e, true));
1734 sel.limit = Some(n);
1735 }
1736 "unique" => {
1737 if self.strict {
1738 return Err(SqlError::Unsupported(
1739 "pushdown: DISTINCT result order is unordered in SQL".into(),
1740 ));
1741 }
1742 if sel.limit.is_some() {
1745 return Err(SqlError::Unsupported(
1746 "'unique' after a positional selection (SQL applies \
1747 DISTINCT before LIMIT)"
1748 .into(),
1749 ));
1750 }
1751 if let Some(c) = pending_col.take() {
1752 sel.select = vec![c];
1753 }
1754 sel.distinct = true;
1755 }
1756 other => {
1757 return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
1758 }
1759 }
1760 }
1761 "select" => {
1762 if self.strict {
1763 return Err(SqlError::Unsupported(
1764 "pushdown: LIMIT without a guaranteed order".into(),
1765 ));
1766 }
1767 if sel.limit.is_some() {
1769 return Err(SqlError::Unsupported(
1770 "a second positional selection".into(),
1771 ));
1772 }
1773 let p = self.arbor.children(s)[0];
1774 match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
1775 ("range", Some(Value::Int(n)))
1776 if self.prop(p, "from").is_none() && n > 0 =>
1777 {
1778 sel.limit = Some(n.to_string());
1779 }
1780 _ => {
1781 return Err(SqlError::Unsupported(
1782 "positional selection beyond '@| [..n]'".into(),
1783 ));
1784 }
1785 }
1786 }
1787 other => {
1788 return Err(SqlError::Unsupported(format!(
1789 "the '{other}' stage (windows, subcontexts, and registers are \
1790 Quarb-side state)"
1791 )));
1792 }
1793 }
1794 i += 1;
1795 }
1796 if let Some(c) = pending_col
1798 && sel.select.is_empty()
1799 {
1800 sel.select = vec![c];
1801 }
1802 Ok(())
1803 }
1804
1805 fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
1806 let kids = self.arbor.children(s);
1807 let key = kids
1810 .iter()
1811 .rev()
1812 .find(|&&k| self.kind(k) != "literal")
1813 .ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
1814 self.operand(*key, join.map(|(l, _)| l))
1815 }
1816
1817 fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
1819 let kids = self.arbor.children(s);
1820 if kids.len() != 1 || self.kind(kids[0]) != "compare" {
1821 return Err(SqlError::Unsupported(
1822 "HAVING translates for a single comparison".into(),
1823 ));
1824 }
1825 let e = kids[0];
1826 let op = self.prop_s(e, "op");
1827 let cmp_kids = self.arbor.children(e);
1828 let l = match self.kind(cmp_kids[0]).as_str() {
1829 "topic" | "recall" => "__AGG__".to_string(),
1830 _ => {
1831 return Err(SqlError::Unsupported(
1832 "HAVING compares the aggregate ($_ or its register)".into(),
1833 ));
1834 }
1835 };
1836 let r = self.operand(cmp_kids[1], None)?;
1837 Ok(format!("{l} {op} {r}"))
1838 }
1839
1840 fn record_fields(
1842 &mut self,
1843 s: NodeId,
1844 join: Option<(&str, &str)>,
1845 ) -> Result<Vec<String>, SqlError> {
1846 let kids = self.arbor.children(s);
1847 let mut fields = Vec::new();
1848 let mut i = 0;
1849 while i < kids.len() {
1850 let k = kids[i];
1851 if self.kind(k) == "literal" {
1852 let name = self.prop_s(k, "value");
1853 let value = self.operand(kids[i + 1], join.map(|(l, _)| l))?;
1854 let name = self.alias(&name)?;
1855 fields.push(format!("{value} AS {name}"));
1856 i += 2;
1857 } else {
1858 let value = self.operand(k, join.map(|(l, _)| l))?;
1859 fields.push(value);
1860 i += 1;
1861 }
1862 }
1863 Ok(fields)
1864 }
1865}
1866
1867fn sql_agg(name: &str, col: Option<String>) -> Result<String, SqlError> {
1868 let f = match name {
1869 "count" => "COUNT",
1870 "sum" => "SUM",
1871 "mean" | "avg" => "AVG",
1872 "min" => "MIN",
1873 "max" => "MAX",
1874 _ => unreachable!("checked by caller"),
1875 };
1876 match col {
1877 Some(c) => Ok(format!("{f}({c})")),
1878 None if f == "COUNT" => Ok("COUNT(*)".to_string()),
1881 None => Err(SqlError::Unsupported(format!(
1882 "'{name}' over row nodes (project a column first: '| ::col @| {name}')"
1883 ))),
1884 }
1885}
1886
1887#[cfg(test)]
1888mod null_and_literal_tests {
1889 use super::{Dialect, export, partial_pushdown, pushdown};
1890
1891 const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
1894
1895 #[test]
1896 fn pattern_literal_pushes_as_like() {
1897 let q = "/users/*[::name == 'app-'*]::id";
1902 let p = pushdown(q, Some(Dialect::Postgres)).unwrap();
1903 assert_eq!(
1904 p.sql,
1905 "SELECT id FROM users WHERE name LIKE 'app-%' ESCAPE '\\'"
1906 );
1907 assert!(pushdown(q, Some(Dialect::Sqlite)).is_none());
1908 assert!(pushdown(q, Some(Dialect::MySql)).is_none());
1909 let p = pushdown("/users/*[::name == *'web'*]::id", Some(Dialect::Postgres)).unwrap();
1911 assert!(p.sql.contains("LIKE '%web%'"), "{}", p.sql);
1912 let p = pushdown(
1913 "/users/*[::name == *'a'*'.gz']::id",
1914 Some(Dialect::Postgres),
1915 )
1916 .unwrap();
1917 assert!(p.sql.contains("LIKE '%a%.gz'"), "{}", p.sql);
1918 let p = pushdown("/users/*[::name == *'50%_x'*]::id", Some(Dialect::Postgres)).unwrap();
1920 assert!(p.sql.contains("LIKE '%50\\%\\_x%'"), "{}", p.sql);
1921 assert!(pushdown("/users/*[::name !== *'web'*]::id", Some(Dialect::Postgres)).is_none());
1923 let p = pushdown("/users/*[::name *= 'web']::id", Some(Dialect::Postgres)).unwrap();
1925 assert!(p.sql.contains("LIKE '%web%'"), "{}", p.sql);
1926 assert!(pushdown("/users/*[::name *= 'web']::id", Some(Dialect::Sqlite)).is_none());
1927 }
1928
1929 #[test]
1930 fn json_column_path_pushdown_per_dialect() {
1931 let q = "/orders/*[/data/meta/tier:: = 'gold']::id";
1935 let sql = |d| pushdown(q, Some(d)).unwrap().sql;
1936 assert_eq!(
1937 sql(Dialect::Postgres),
1938 "SELECT id FROM orders WHERE ((data::jsonb #>> '{meta,tier}') = 'gold')"
1939 );
1940 assert_eq!(
1941 sql(Dialect::MySql),
1942 "SELECT id FROM orders WHERE (JSON_VALID(data) AND \
1943 JSON_UNQUOTE(JSON_EXTRACT(data, '$.meta.tier')) = 'gold')"
1944 );
1945 assert_eq!(
1946 sql(Dialect::Sqlite),
1947 "SELECT id FROM orders WHERE (json_valid(data) AND \
1948 json_extract(data, '$.meta.tier') = 'gold')"
1949 );
1950 assert_eq!(
1951 sql(Dialect::Mssql),
1952 "SELECT id FROM orders WHERE (ISJSON(data) = 1 AND \
1953 JSON_VALUE(data, '$.meta.tier') = 'gold')"
1954 );
1955 assert_eq!(
1956 sql(Dialect::Oracle),
1957 "SELECT id FROM orders WHERE (JSON_VALUE(data, '$.meta.tier') = 'gold')"
1958 );
1959 assert!(pushdown(q, None).is_none());
1962 }
1963
1964 #[test]
1965 fn json_pushdown_only_string_equality() {
1966 let d = Some(Dialect::Sqlite);
1970 assert!(pushdown("/o/*[/data/n:: > 2]::id", d).is_none());
1971 assert!(pushdown("/o/*[/data/tier:: != 'gold']::id", d).is_none());
1972 assert!(pushdown("/o/*[/data/items/*/sku:: = 'A1']::id", d).is_none());
1973 }
1974
1975 #[test]
1976 fn json_pushdown_refuses_numeric_looking_literal() {
1977 let q = "/orders/*[/data/total:: = '150']::id";
1981 assert!(pushdown(q, Some(Dialect::Sqlite)).is_none());
1982 assert!(pushdown(q, Some(Dialect::Postgres)).is_none());
1983 }
1984
1985 #[test]
1986 fn partial_json_prefilters_sqlite() {
1987 let p = partial_pushdown(
1989 "/orders/*[/payload/customer/geo/city:: = 'Lyon']::id",
1990 Some(Dialect::Sqlite),
1991 )
1992 .unwrap();
1993 assert_eq!(p.table, "orders");
1994 assert_eq!(
1995 p.where_sql,
1996 "(json_valid(payload) AND \
1997 json_extract(payload, '$.customer.geo.city') = 'Lyon')"
1998 );
1999 let p = partial_pushdown(
2001 "/orders/*[/payload/total:: > 150]::id",
2002 Some(Dialect::Sqlite),
2003 )
2004 .unwrap();
2005 assert_eq!(
2006 p.where_sql,
2007 "(json_valid(payload) AND (json_extract(payload, '$.total') > 150 \
2008 OR json_type(payload, '$.total') = 'text'))"
2009 );
2010 let p = partial_pushdown("/orders/*[/payload/gift]::id", Some(Dialect::Sqlite)).unwrap();
2012 assert_eq!(
2013 p.where_sql,
2014 "(json_valid(payload) AND json_type(payload, '$.gift') IS NOT NULL)"
2015 );
2016 let p = partial_pushdown(
2018 "/orders/*[/payload/status:: != 'x']::id",
2019 Some(Dialect::Sqlite),
2020 )
2021 .unwrap();
2022 assert_eq!(
2023 p.where_sql,
2024 "(json_valid(payload) AND json_type(payload, '$.status') IS NOT NULL)"
2025 );
2026 let p = partial_pushdown(
2028 "/orders/*[150 < /payload/total::]::id",
2029 Some(Dialect::Sqlite),
2030 )
2031 .unwrap();
2032 assert!(p.where_sql.contains("> 150"), "{}", p.where_sql);
2033 assert!(partial_pushdown("/orders/*[/payload/gift]::id", None).is_none());
2035 }
2036
2037 #[test]
2038 fn partial_json_prefilters_postgres() {
2039 let p = partial_pushdown(
2040 "/orders/*[/payload/total:: > 150]::id",
2041 Some(Dialect::Postgres),
2042 )
2043 .unwrap();
2044 assert_eq!(
2045 p.where_sql,
2046 "(CASE WHEN jsonb_typeof(payload::jsonb #> '{total}') = 'number' \
2047 THEN ((payload::jsonb #>> '{total}'))::float8 > 150 \
2048 WHEN jsonb_typeof(payload::jsonb #> '{total}') = 'string' \
2049 THEN true ELSE false END)"
2050 );
2051 let p = partial_pushdown("/orders/*[/payload/gift]::id", Some(Dialect::Postgres)).unwrap();
2052 assert_eq!(
2053 p.where_sql,
2054 "(jsonb_typeof(payload::jsonb #> '{gift}') IS NOT NULL)"
2055 );
2056 }
2057
2058 #[test]
2059 fn partial_skips_untranslatable_keeps_prefix() {
2060 let p = partial_pushdown(
2064 "/orders/*[::status = 'shipped'][/payload/line_items/*/sku:: = 'X']::id",
2065 Some(Dialect::Sqlite),
2066 )
2067 .unwrap();
2068 assert_eq!(p.where_sql, "status = 'shipped'");
2069 }
2070
2071 #[test]
2072 fn null_literal_compares_use_is_null() {
2073 assert_eq!(
2077 export("/t/*[::x = null] | ::x").unwrap().query,
2078 "SELECT x FROM t WHERE x IS NULL"
2079 );
2080 assert_eq!(
2081 export("/t/*[::x != null] | ::x").unwrap().query,
2082 "SELECT x FROM t WHERE x IS NOT NULL"
2083 );
2084 assert_eq!(
2085 pushdown("/t/*[::x = null] | ::x", None).unwrap().sql,
2086 "SELECT x FROM t WHERE x IS NULL"
2087 );
2088 assert_eq!(
2089 pushdown("/t/*[::x != null] | ::x", None).unwrap().sql,
2090 "SELECT x FROM t WHERE x IS NOT NULL"
2091 );
2092 }
2093
2094 #[test]
2095 fn ne_and_not_refuse_pushdown_but_display_diverges() {
2096 assert!(pushdown("/t/*[::x != 5] | ::x", None).is_none());
2100 assert!(pushdown("/t/*[!::x = 5] | ::x", None).is_none());
2101 assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}"), None).is_none());
2102 assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}"), None).is_none());
2103 let t = export("/t/*[::x != 5] | ::x").unwrap();
2105 assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
2106 assert!(t.notes.iter().any(|n| n.contains("NULL")));
2107 }
2108
2109 #[test]
2110 fn unescapable_text_literal_refuses_pushdown() {
2111 assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path", None).is_none());
2115 assert!(pushdown("/t/*[::name = \"it's\"] | ::name", None).is_none());
2116 assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}"), None).is_none());
2117 assert_eq!(
2118 pushdown("/t/*[::name = \"rare\"] | ::name", None)
2119 .unwrap()
2120 .sql,
2121 "SELECT name FROM t WHERE name = 'rare'"
2122 );
2123 }
2124}