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 let r = self.operand(kids[1], qual)?;
1081 Ok(match op.as_str() {
1082 "=" => format!("{l} = {r}"),
1083 "!=" => {
1084 if self.strict {
1092 return Err(SqlError::Semantics(
1093 "'!=' drops the NULL rows Quarb keeps \
1094 (SQL '<>' is UNKNOWN for NULL; use '!= null' \
1095 for IS NOT NULL)"
1096 .into(),
1097 ));
1098 }
1099 self.notes.push(
1100 "'!=' → '<>': Quarb keeps rows whose column is NULL; \
1101 SQL's '<>' drops them (use '!= null' for IS NOT NULL)"
1102 .to_string(),
1103 );
1104 format!("{l} <> {r}")
1105 }
1106 "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
1107 "*=" => {
1108 if self.strict {
1109 return Err(SqlError::Semantics(
1110 "LIKE case folding differs per engine".into(),
1111 ));
1112 }
1113 if !self.is_text_literal(kids[1]) {
1118 return Err(SqlError::Unsupported(
1119 "'*=' with a non-literal pattern".into(),
1120 ));
1121 }
1122 self.notes.push(
1123 "*= → LIKE: Quarb's substring test is case-sensitive; LIKE \
1124 folds case on SQLite/MySQL but not PostgreSQL"
1125 .to_string(),
1126 );
1127 let raw = self
1132 .prop(kids[1], "value")
1133 .map(|v| v.to_string())
1134 .unwrap_or_default();
1135 let pat = raw
1136 .replace('\\', "\\\\")
1137 .replace('%', "\\%")
1138 .replace('_', "\\_")
1139 .replace('\'', "''");
1140 format!("{l} LIKE '%{pat}%' ESCAPE '\\'")
1141 }
1142 "=~" | "!~" => {
1143 return Err(SqlError::Semantics(
1144 "regex matching means a different REGEXP engine per \
1145 backend; regexes run engine-side"
1146 .into(),
1147 ));
1148 }
1149 other => {
1150 return Err(SqlError::Unsupported(format!("the '{other}' comparison")));
1151 }
1152 })
1153 }
1154 _ => {
1156 if self.strict {
1157 return Err(SqlError::Semantics(
1158 "truthiness diverges (0 and '' are falsy in Quarb)".into(),
1159 ));
1160 }
1161 self.notes.push(
1162 "truthiness: '[::c]' exports as IS NOT NULL, but Quarb also treats \
1163 0 and '' as falsy"
1164 .to_string(),
1165 );
1166 Ok(format!("{} IS NOT NULL", self.operand(e, qual)?))
1167 }
1168 }
1169 }
1170
1171 fn operand(&mut self, o: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
1172 match self.kind(o).as_str() {
1173 "literal" => {
1174 let v = self.prop(o, "value").unwrap_or(Value::Null);
1175 match self.prop_s(o, "type").as_str() {
1176 "text" => {
1177 let s = v.to_string();
1178 if self.strict && (s.contains('\'') || s.contains('\\')) {
1189 return Err(SqlError::Semantics(
1190 "a text literal with a quote or backslash \
1191 has no escaping portable across SQL dialects"
1192 .into(),
1193 ));
1194 }
1195 Ok(format!("'{}'", s.replace('\'', "''")))
1196 }
1197 "null" => Ok("NULL".to_string()),
1198 _ => Ok(v.to_string()),
1199 }
1200 }
1201 "path" => {
1202 if self.kid(o, "step").is_some() {
1203 let s = self.kids(o, "step")[0];
1206 if self.prop_s(s, "axis") == "-->" {
1207 return Err(SqlError::Unsupported(
1208 "a '~>' resolution chain: the foreign-key targets live in \
1209 the schema, not the query — spell the join with '<=>' to \
1210 export it"
1211 .into(),
1212 ));
1213 }
1214 return Err(SqlError::Unsupported(
1215 "navigation inside a predicate (SQL rows are flat)".into(),
1216 ));
1217 }
1218 let p = self
1219 .kid(o, "projection")
1220 .ok_or_else(|| SqlError::Unsupported("an empty path operand".into()))?;
1221 let col = self.projection_col(p)?;
1222 let col = self.sql_ident(&col, "column name")?;
1223 Ok(match qual {
1224 Some(q) => format!("{q}.{col}"),
1225 None => col,
1226 })
1227 }
1228 "context" => {
1229 let p = self
1233 .kid(o, "projection")
1234 .ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
1235 let col = self.projection_col(p)?;
1236 let col = self.sql_ident(&col, "column name")?;
1237 if self.in_on {
1238 return Err(SqlError::Unsupported(
1239 "a '$*' reference inside the ON (the driver is '$$')".into(),
1240 ));
1241 }
1242 let Some(join) = self.join_sql.clone() else {
1243 return Err(SqlError::Unsupported(
1244 "a '$*' reference outside a correlation join".into(),
1245 ));
1246 };
1247 match self.prop(o, "index") {
1248 Some(Value::Int(1)) => Ok(format!("{join}.{col}")),
1249 None => Err(SqlError::Unsupported("a bare '$*' reference".into())),
1250 Some(v) => Err(SqlError::Unsupported(format!(
1251 "pushdown: $*{v} beyond a two-branch correlation"
1252 ))),
1253 }
1254 }
1255 "outer" => {
1256 if !self.in_on {
1260 return Err(SqlError::Unsupported(
1261 "a '$$' reference outside the join's ON".into(),
1262 ));
1263 }
1264 let kids = self.arbor.children(o);
1265 let inner = *kids
1266 .first()
1267 .ok_or_else(|| SqlError::Unsupported("an empty '$$' reference".into()))?;
1268 if self.kind(inner) != "path" || self.kid(inner, "step").is_some() {
1269 return Err(SqlError::Unsupported(
1270 "a '$$' reference beyond a plain column ($$::col)".into(),
1271 ));
1272 }
1273 let p = self
1274 .kid(inner, "projection")
1275 .ok_or_else(|| SqlError::Unsupported("a bare '$$' reference".into()))?;
1276 let col = self.projection_col(p)?;
1277 let col = self.sql_ident(&col, "column name")?;
1278 Ok(format!("{}.{col}", self.from_sql.clone()))
1279 }
1280 "arith" => {
1281 let op = self.prop_s(o, "op");
1282 let kids = self.arbor.children(o);
1283 let l = self.operand(kids[0], qual)?;
1284 let r = self.operand(kids[1], qual)?;
1285 Ok(match op.as_str() {
1286 "+" | "-" | "*" => format!("({l} {op} {r})"),
1287 "div" => format!("({l} / {r})"),
1288 "mod" => format!("({l} % {r})"),
1289 other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
1290 })
1291 }
1292 other => Err(SqlError::Unsupported(format!(
1293 "the '{other}' operand (registers, topics, and captures are Quarb-side state)"
1294 ))),
1295 }
1296 }
1297
1298 fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
1299 match self.prop_s(p, "kind").as_str() {
1300 "property" => match self.prop(p, "key") {
1301 Some(k) => Ok(k.to_string()),
1302 None => Err(SqlError::Unsupported(
1303 "the bare '::' projection (name the column)".into(),
1304 )),
1305 },
1306 other => Err(SqlError::Unsupported(format!(
1307 "the {other} metadata projection"
1308 ))),
1309 }
1310 }
1311
1312 fn is_text_literal(&self, o: NodeId) -> bool {
1314 self.kind(o) == "literal" && self.prop_s(o, "type") == "text"
1315 }
1316
1317 fn json_path(&self, o: NodeId) -> Option<(String, Vec<String>)> {
1324 if self.kind(o) != "path" {
1325 return None;
1326 }
1327 let steps = self.kids(o, "step");
1328 if steps.len() < 2 {
1329 return None;
1330 }
1331 let proj = self.kid(o, "projection")?;
1334 if self.prop_s(proj, "kind") != "property" || self.prop(proj, "key").is_some() {
1335 return None;
1336 }
1337 let mut names = Vec::with_capacity(steps.len());
1338 for s in &steps {
1339 if self.prop_s(*s, "axis") != "/"
1340 || self.prop_s(*s, "matcher-kind") != "name"
1341 || self.kid(*s, "predicate").is_some()
1342 {
1343 return None;
1344 }
1345 let name = self.prop_s(*s, "matcher");
1346 let plain = !name.is_empty()
1349 && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
1350 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
1351 if !plain {
1352 return None;
1353 }
1354 names.push(name);
1355 }
1356 let col = names.remove(0);
1357 Some((col, names))
1358 }
1359
1360 fn alias(&mut self, name: &str) -> Result<String, SqlError> {
1364 if is_plain_ident(name) {
1365 return Ok(name.to_string());
1366 }
1367 if self.strict {
1368 return Err(SqlError::Unsupported(format!(
1369 "pushdown: field name {name:?} needs SQL quoting \
1370 (dialects disagree); rename the field"
1371 )));
1372 }
1373 self.notes
1374 .push(format!("field name {name:?} double-quoted (ANSI)"));
1375 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1376 }
1377
1378 fn sql_ident(&mut self, name: &str, what: &str) -> Result<String, SqlError> {
1385 if is_plain_ident(name) {
1386 return Ok(name.to_string());
1387 }
1388 if self.strict {
1389 return Err(SqlError::Unsupported(format!(
1390 "pushdown: {what} {name:?} is not a plain SQL identifier"
1391 )));
1392 }
1393 self.notes
1394 .push(format!("{what} {name:?} double-quoted (ANSI)"));
1395 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
1396 }
1397
1398 fn split_join_pred(
1402 &mut self,
1403 p: NodeId,
1404 rtable: &str,
1405 on: &mut Vec<String>,
1406 wheres: &mut Vec<String>,
1407 ) -> Result<(), SqlError> {
1408 if self.prop_s(p, "kind") != "expr" {
1409 return Err(SqlError::Unsupported(
1410 "a positional predicate on the joined side".into(),
1411 ));
1412 }
1413 fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
1416 if ex.kind(e) == "and" {
1417 for c in ex.arbor.children(e) {
1418 conjuncts(ex, c, out);
1419 }
1420 } else {
1421 out.push(e);
1422 }
1423 }
1424 let mut parts = Vec::new();
1425 for c in self.arbor.children(p) {
1426 conjuncts(self, c, &mut parts);
1427 }
1428 for e in parts {
1429 let uses_driver = self.subtree_has(e, "outer");
1430 self.in_on = true;
1431 let cond = self.pred_expr(e, Some(rtable));
1432 self.in_on = false;
1433 let cond = cond?;
1434 if uses_driver && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
1435 self.collect_join_cols(e)?;
1442 on.push(cond);
1443 } else {
1444 wheres.push(cond);
1445 }
1446 }
1447 Ok(())
1448 }
1449
1450 fn collect_join_cols(&mut self, e: NodeId) -> Result<(), SqlError> {
1454 if self.kind(e) == "outer" {
1457 return Ok(());
1458 }
1459 if self.kind(e) == "path"
1460 && self.kid(e, "step").is_none()
1461 && let Some(p) = self.kid(e, "projection")
1462 {
1463 let col = self.projection_col(p)?;
1464 self.join_on_cols.push(col);
1465 }
1466 for c in self.arbor.children(e) {
1467 self.collect_join_cols(c)?;
1468 }
1469 Ok(())
1470 }
1471
1472 fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
1473 if self.kind(n) == kind {
1474 return true;
1475 }
1476 self.arbor
1477 .children(n)
1478 .into_iter()
1479 .any(|c| self.subtree_has(c, kind))
1480 }
1481
1482 fn pipeline(
1484 &mut self,
1485 q: NodeId,
1486 sel: &mut Select,
1487 join: Option<(&str, &str)>,
1488 ) -> Result<(), SqlError> {
1489 let Some(pipe) = self.kid(q, "pipeline") else {
1490 return Ok(());
1491 };
1492 let stages: Vec<NodeId> = self.arbor.children(pipe);
1493 let mut i = 0;
1494 let mut pending_col: Option<String> = None;
1496 let mut grouped: Option<(String, String, String)> = None;
1498
1499 while i < stages.len() {
1500 let s = stages[i];
1501 match self.kind(s).as_str() {
1502 "expr" => {
1503 let kids = self.arbor.children(s);
1504 pending_col = Some(self.operand(kids[0], join.map(|(l, _)| l))?);
1505 }
1506 "func" => {
1507 let name = self.prop_s(s, "name");
1508 match name.as_str() {
1509 "rec" | "record" => {
1510 sel.select = self.record_fields(s, join)?;
1511 }
1512 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1515 let (key, _, _) = grouped.as_ref().ok_or_else(|| {
1516 SqlError::Unsupported(format!(
1517 "'| {name}' outside a group (use '@| {name}')"
1518 ))
1519 })?;
1520 let col = if name == "count" {
1524 pending_col = None;
1525 None
1526 } else {
1527 pending_col.take()
1528 };
1529 let agg = sql_agg(&name, col)?;
1530 grouped = Some((key.clone(), agg.clone(), agg));
1531 }
1532 other => {
1533 if matches!(other, "sort" | "sort_by" | "unique" | "top" | "bottom") {
1539 return Err(SqlError::Semantics(format!(
1540 "'{other}' means the backend's collation and \
1541 duplicate semantics; one engine-side ordering \
1542 keeps it identical everywhere"
1543 )));
1544 }
1545 return Err(SqlError::Unsupported(format!(
1546 "the '{other}' pipeline function"
1547 )));
1548 }
1549 }
1550 }
1551 "push" => {
1552 if let Some((k, agg, _)) = grouped.take() {
1554 let alias = self.prop_s(s, "name");
1555 grouped = Some((k, agg, alias));
1556 } else {
1557 return Err(SqlError::Unsupported(
1558 "a register push (Quarb-side state)".into(),
1559 ));
1560 }
1561 }
1562 "filter" => {
1563 if grouped.is_some() {
1564 let cond = self.having_cond(s)?;
1567 sel.having = Some(cond);
1568 } else {
1569 return Err(SqlError::Unsupported(
1570 "a mid-pipeline filter (put it in the row predicate)".into(),
1571 ));
1572 }
1573 }
1574 "recall" => {
1575 if self.prop_s(s, "ref") != "%." {
1577 return Err(SqlError::Unsupported(
1578 "a register recall (Quarb-side state)".into(),
1579 ));
1580 }
1581 let (key, agg, alias) = grouped.clone().ok_or_else(|| {
1582 SqlError::Unsupported("'%.', with nothing grouped".into())
1583 })?;
1584 let alias = self.alias(&alias)?;
1585 sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
1586 if let Some(h) = sel.having.take() {
1589 sel.having = Some(h.replace("__AGG__", &agg));
1590 }
1591 }
1592 "agg" => {
1593 let name = self.prop_s(s, "name");
1594 match name.as_str() {
1595 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
1596 let col = if name == "count" {
1600 pending_col = None;
1601 None
1602 } else {
1603 pending_col.take()
1604 };
1605 sel.select = vec![sql_agg(&name, col)?];
1606 self.aggregate = true;
1607 }
1608 "group" => {
1609 if self.strict {
1610 return Err(SqlError::Semantics(
1611 "GROUP BY result order is unordered in SQL".into(),
1612 ));
1613 }
1614 self.notes.push(
1615 "GROUP BY: SQL keeps a NULL-key group; Quarb's group \
1616 drops null keys"
1617 .to_string(),
1618 );
1619 let key = self.group_key(s, join)?;
1620 sel.group_by = Some(key.clone());
1621 grouped = Some((key, String::new(), String::new()));
1622 }
1623 "sort_by" => {
1624 if self.strict {
1625 return Err(SqlError::Semantics(
1626 "ORDER BY collations differ per engine".into(),
1627 ));
1628 }
1629 if sel.limit.is_some() {
1634 return Err(SqlError::Unsupported(
1635 "a sort after a positional selection (SQL orders \
1636 before LIMIT)"
1637 .into(),
1638 ));
1639 }
1640 if sel.order_by.is_some() {
1641 return Err(SqlError::Unsupported(
1642 "a second sort (SQL has a single ORDER BY)".into(),
1643 ));
1644 }
1645 let kids = self.arbor.children(s);
1646 let e = self.operand(kids[0], join.map(|(l, _)| l))?;
1647 sel.order_by = Some((e, false));
1648 }
1649 "reverse" => match &mut sel.order_by {
1650 Some((_, desc)) => *desc = true,
1651 None => {
1652 return Err(SqlError::Unsupported(
1653 "reverse without an ORDER BY (rows are unordered)".into(),
1654 ));
1655 }
1656 },
1657 "top" => {
1658 if self.strict {
1659 return Err(SqlError::Semantics(
1660 "ORDER BY collations differ per engine".into(),
1661 ));
1662 }
1663 if sel.limit.is_some() {
1664 return Err(SqlError::Unsupported(
1665 "'top' after a positional selection (SQL orders \
1666 before LIMIT)"
1667 .into(),
1668 ));
1669 }
1670 if sel.order_by.is_some() {
1671 return Err(SqlError::Unsupported(
1672 "a second sort (SQL has a single ORDER BY)".into(),
1673 ));
1674 }
1675 let kids = self.arbor.children(s);
1676 let n = self.prop_s(kids[0], "value");
1677 let e = self.operand(kids[1], join.map(|(l, _)| l))?;
1678 sel.order_by = Some((e, true));
1679 sel.limit = Some(n);
1680 }
1681 "unique" => {
1682 if self.strict {
1683 return Err(SqlError::Unsupported(
1684 "pushdown: DISTINCT result order is unordered in SQL".into(),
1685 ));
1686 }
1687 if sel.limit.is_some() {
1690 return Err(SqlError::Unsupported(
1691 "'unique' after a positional selection (SQL applies \
1692 DISTINCT before LIMIT)"
1693 .into(),
1694 ));
1695 }
1696 if let Some(c) = pending_col.take() {
1697 sel.select = vec![c];
1698 }
1699 sel.distinct = true;
1700 }
1701 other => {
1702 return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
1703 }
1704 }
1705 }
1706 "select" => {
1707 if self.strict {
1708 return Err(SqlError::Unsupported(
1709 "pushdown: LIMIT without a guaranteed order".into(),
1710 ));
1711 }
1712 if sel.limit.is_some() {
1714 return Err(SqlError::Unsupported(
1715 "a second positional selection".into(),
1716 ));
1717 }
1718 let p = self.arbor.children(s)[0];
1719 match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
1720 ("range", Some(Value::Int(n)))
1721 if self.prop(p, "from").is_none() && n > 0 =>
1722 {
1723 sel.limit = Some(n.to_string());
1724 }
1725 _ => {
1726 return Err(SqlError::Unsupported(
1727 "positional selection beyond '@| [..n]'".into(),
1728 ));
1729 }
1730 }
1731 }
1732 other => {
1733 return Err(SqlError::Unsupported(format!(
1734 "the '{other}' stage (windows, subcontexts, and registers are \
1735 Quarb-side state)"
1736 )));
1737 }
1738 }
1739 i += 1;
1740 }
1741 if let Some(c) = pending_col
1743 && sel.select.is_empty()
1744 {
1745 sel.select = vec![c];
1746 }
1747 Ok(())
1748 }
1749
1750 fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
1751 let kids = self.arbor.children(s);
1752 let key = kids
1755 .iter()
1756 .rev()
1757 .find(|&&k| self.kind(k) != "literal")
1758 .ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
1759 self.operand(*key, join.map(|(l, _)| l))
1760 }
1761
1762 fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
1764 let kids = self.arbor.children(s);
1765 if kids.len() != 1 || self.kind(kids[0]) != "compare" {
1766 return Err(SqlError::Unsupported(
1767 "HAVING translates for a single comparison".into(),
1768 ));
1769 }
1770 let e = kids[0];
1771 let op = self.prop_s(e, "op");
1772 let cmp_kids = self.arbor.children(e);
1773 let l = match self.kind(cmp_kids[0]).as_str() {
1774 "topic" | "recall" => "__AGG__".to_string(),
1775 _ => {
1776 return Err(SqlError::Unsupported(
1777 "HAVING compares the aggregate ($_ or its register)".into(),
1778 ));
1779 }
1780 };
1781 let r = self.operand(cmp_kids[1], None)?;
1782 Ok(format!("{l} {op} {r}"))
1783 }
1784
1785 fn record_fields(
1787 &mut self,
1788 s: NodeId,
1789 join: Option<(&str, &str)>,
1790 ) -> Result<Vec<String>, SqlError> {
1791 let kids = self.arbor.children(s);
1792 let mut fields = Vec::new();
1793 let mut i = 0;
1794 while i < kids.len() {
1795 let k = kids[i];
1796 if self.kind(k) == "literal" {
1797 let name = self.prop_s(k, "value");
1798 let value = self.operand(kids[i + 1], join.map(|(l, _)| l))?;
1799 let name = self.alias(&name)?;
1800 fields.push(format!("{value} AS {name}"));
1801 i += 2;
1802 } else {
1803 let value = self.operand(k, join.map(|(l, _)| l))?;
1804 fields.push(value);
1805 i += 1;
1806 }
1807 }
1808 Ok(fields)
1809 }
1810}
1811
1812fn sql_agg(name: &str, col: Option<String>) -> Result<String, SqlError> {
1813 let f = match name {
1814 "count" => "COUNT",
1815 "sum" => "SUM",
1816 "mean" | "avg" => "AVG",
1817 "min" => "MIN",
1818 "max" => "MAX",
1819 _ => unreachable!("checked by caller"),
1820 };
1821 match col {
1822 Some(c) => Ok(format!("{f}({c})")),
1823 None if f == "COUNT" => Ok("COUNT(*)".to_string()),
1826 None => Err(SqlError::Unsupported(format!(
1827 "'{name}' over row nodes (project a column first: '| ::col @| {name}')"
1828 ))),
1829 }
1830}
1831
1832#[cfg(test)]
1833mod null_and_literal_tests {
1834 use super::{Dialect, export, partial_pushdown, pushdown};
1835
1836 const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
1839
1840 #[test]
1841 fn json_column_path_pushdown_per_dialect() {
1842 let q = "/orders/*[/data/meta/tier:: = 'gold']::id";
1846 let sql = |d| pushdown(q, Some(d)).unwrap().sql;
1847 assert_eq!(
1848 sql(Dialect::Postgres),
1849 "SELECT id FROM orders WHERE ((data::jsonb #>> '{meta,tier}') = 'gold')"
1850 );
1851 assert_eq!(
1852 sql(Dialect::MySql),
1853 "SELECT id FROM orders WHERE (JSON_VALID(data) AND \
1854 JSON_UNQUOTE(JSON_EXTRACT(data, '$.meta.tier')) = 'gold')"
1855 );
1856 assert_eq!(
1857 sql(Dialect::Sqlite),
1858 "SELECT id FROM orders WHERE (json_valid(data) AND \
1859 json_extract(data, '$.meta.tier') = 'gold')"
1860 );
1861 assert_eq!(
1862 sql(Dialect::Mssql),
1863 "SELECT id FROM orders WHERE (ISJSON(data) = 1 AND \
1864 JSON_VALUE(data, '$.meta.tier') = 'gold')"
1865 );
1866 assert_eq!(
1867 sql(Dialect::Oracle),
1868 "SELECT id FROM orders WHERE (JSON_VALUE(data, '$.meta.tier') = 'gold')"
1869 );
1870 assert!(pushdown(q, None).is_none());
1873 }
1874
1875 #[test]
1876 fn json_pushdown_only_string_equality() {
1877 let d = Some(Dialect::Sqlite);
1881 assert!(pushdown("/o/*[/data/n:: > 2]::id", d).is_none());
1882 assert!(pushdown("/o/*[/data/tier:: != 'gold']::id", d).is_none());
1883 assert!(pushdown("/o/*[/data/items/*/sku:: = 'A1']::id", d).is_none());
1884 }
1885
1886 #[test]
1887 fn json_pushdown_refuses_numeric_looking_literal() {
1888 let q = "/orders/*[/data/total:: = '150']::id";
1892 assert!(pushdown(q, Some(Dialect::Sqlite)).is_none());
1893 assert!(pushdown(q, Some(Dialect::Postgres)).is_none());
1894 }
1895
1896 #[test]
1897 fn partial_json_prefilters_sqlite() {
1898 let p = partial_pushdown(
1900 "/orders/*[/payload/customer/geo/city:: = 'Lyon']::id",
1901 Some(Dialect::Sqlite),
1902 )
1903 .unwrap();
1904 assert_eq!(p.table, "orders");
1905 assert_eq!(
1906 p.where_sql,
1907 "(json_valid(payload) AND \
1908 json_extract(payload, '$.customer.geo.city') = 'Lyon')"
1909 );
1910 let p = partial_pushdown(
1912 "/orders/*[/payload/total:: > 150]::id",
1913 Some(Dialect::Sqlite),
1914 )
1915 .unwrap();
1916 assert_eq!(
1917 p.where_sql,
1918 "(json_valid(payload) AND (json_extract(payload, '$.total') > 150 \
1919 OR json_type(payload, '$.total') = 'text'))"
1920 );
1921 let p = partial_pushdown("/orders/*[/payload/gift]::id", Some(Dialect::Sqlite)).unwrap();
1923 assert_eq!(
1924 p.where_sql,
1925 "(json_valid(payload) AND json_type(payload, '$.gift') IS NOT NULL)"
1926 );
1927 let p = partial_pushdown(
1929 "/orders/*[/payload/status:: != 'x']::id",
1930 Some(Dialect::Sqlite),
1931 )
1932 .unwrap();
1933 assert_eq!(
1934 p.where_sql,
1935 "(json_valid(payload) AND json_type(payload, '$.status') IS NOT NULL)"
1936 );
1937 let p = partial_pushdown(
1939 "/orders/*[150 < /payload/total::]::id",
1940 Some(Dialect::Sqlite),
1941 )
1942 .unwrap();
1943 assert!(p.where_sql.contains("> 150"), "{}", p.where_sql);
1944 assert!(partial_pushdown("/orders/*[/payload/gift]::id", None).is_none());
1946 }
1947
1948 #[test]
1949 fn partial_json_prefilters_postgres() {
1950 let p = partial_pushdown(
1951 "/orders/*[/payload/total:: > 150]::id",
1952 Some(Dialect::Postgres),
1953 )
1954 .unwrap();
1955 assert_eq!(
1956 p.where_sql,
1957 "(CASE WHEN jsonb_typeof(payload::jsonb #> '{total}') = 'number' \
1958 THEN ((payload::jsonb #>> '{total}'))::float8 > 150 \
1959 WHEN jsonb_typeof(payload::jsonb #> '{total}') = 'string' \
1960 THEN true ELSE false END)"
1961 );
1962 let p =
1963 partial_pushdown("/orders/*[/payload/gift]::id", Some(Dialect::Postgres)).unwrap();
1964 assert_eq!(
1965 p.where_sql,
1966 "(jsonb_typeof(payload::jsonb #> '{gift}') IS NOT NULL)"
1967 );
1968 }
1969
1970 #[test]
1971 fn partial_skips_untranslatable_keeps_prefix() {
1972 let p = partial_pushdown(
1976 "/orders/*[::status = 'shipped'][/payload/line_items/*/sku:: = 'X']::id",
1977 Some(Dialect::Sqlite),
1978 )
1979 .unwrap();
1980 assert_eq!(p.where_sql, "status = 'shipped'");
1981 }
1982
1983 #[test]
1984 fn null_literal_compares_use_is_null() {
1985 assert_eq!(
1989 export("/t/*[::x = null] | ::x").unwrap().query,
1990 "SELECT x FROM t WHERE x IS NULL"
1991 );
1992 assert_eq!(
1993 export("/t/*[::x != null] | ::x").unwrap().query,
1994 "SELECT x FROM t WHERE x IS NOT NULL"
1995 );
1996 assert_eq!(
1997 pushdown("/t/*[::x = null] | ::x", None).unwrap().sql,
1998 "SELECT x FROM t WHERE x IS NULL"
1999 );
2000 assert_eq!(
2001 pushdown("/t/*[::x != null] | ::x", None).unwrap().sql,
2002 "SELECT x FROM t WHERE x IS NOT NULL"
2003 );
2004 }
2005
2006 #[test]
2007 fn ne_and_not_refuse_pushdown_but_display_diverges() {
2008 assert!(pushdown("/t/*[::x != 5] | ::x", None).is_none());
2012 assert!(pushdown("/t/*[!::x = 5] | ::x", None).is_none());
2013 assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}"), None).is_none());
2014 assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}"), None).is_none());
2015 let t = export("/t/*[::x != 5] | ::x").unwrap();
2017 assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
2018 assert!(t.notes.iter().any(|n| n.contains("NULL")));
2019 }
2020
2021 #[test]
2022 fn unescapable_text_literal_refuses_pushdown() {
2023 assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path", None).is_none());
2027 assert!(pushdown("/t/*[::name = \"it's\"] | ::name", None).is_none());
2028 assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}"), None).is_none());
2029 assert_eq!(
2030 pushdown("/t/*[::name = \"rare\"] | ::name", None).unwrap().sql,
2031 "SELECT name FROM t WHERE name = 'rare'"
2032 );
2033 }
2034}