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 from_table: String::new(),
46 join_on_left_cols: Vec::new(),
47 join_table: None,
48 aggregate: false,
49 };
50 let query = ex.query()?;
51 Ok(Translation {
52 query,
53 notes: ex.notes,
54 })
55}
56
57fn refuse_marker(quarb: &str) -> Result<(), SqlError> {
63 if quarb.contains("__LEFT__") {
64 return Err(SqlError::Unsupported(
65 "query text contains the reserved marker \"__LEFT__\"".into(),
66 ));
67 }
68 Ok(())
69}
70
71pub struct Pushdown {
77 pub sql: String,
78 pub order_table: Option<String>,
79 pub join_left: Option<(String, Vec<String>)>,
87}
88
89pub fn pushdown(quarb: &str) -> Option<Pushdown> {
94 pushdown_explained(quarb).ok()
95}
96
97pub fn pushdown_explained(quarb: &str) -> Result<Pushdown, SqlError> {
100 refuse_marker(quarb)?;
101 let arbor =
102 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
103 refuse_groups(&arbor)?;
104 let mut ex = Exporter {
105 arbor,
106 notes: Vec::new(),
107 strict: true,
108 from_table: String::new(),
109 join_on_left_cols: Vec::new(),
110 join_table: None,
111 aggregate: false,
112 };
113 let sql = ex.query()?;
114 let order_table = if ex.aggregate {
115 None
116 } else {
117 Some(ex.join_table.clone().unwrap_or(ex.from_table.clone()))
121 };
122 let join_left = ex
123 .join_table
124 .is_some()
125 .then(|| (ex.from_table.clone(), ex.join_on_left_cols.clone()));
126 Ok(Pushdown {
127 sql,
128 order_table,
129 join_left,
130 })
131}
132
133const ALIAS_KEYWORDS: &[&str] = &[
137 "all",
138 "and",
139 "as",
140 "asc",
141 "by",
142 "case",
143 "cross",
144 "desc",
145 "distinct",
146 "else",
147 "end",
148 "except",
149 "exists",
150 "from",
151 "group",
152 "having",
153 "in",
154 "index",
155 "inner",
156 "intersect",
157 "into",
158 "is",
159 "join",
160 "left",
161 "like",
162 "limit",
163 "not",
164 "null",
165 "offset",
166 "on",
167 "or",
168 "order",
169 "outer",
170 "right",
171 "select",
172 "set",
173 "table",
174 "then",
175 "union",
176 "unique",
177 "update",
178 "using",
179 "values",
180 "when",
181 "where",
182];
183
184#[derive(Default)]
186struct Select {
187 select: Vec<String>,
188 distinct: bool,
189 from: String,
190 join: Option<(String, String)>, wheres: Vec<String>,
192 group_by: Option<String>,
193 having: Option<String>,
194 order_by: Option<(String, bool)>, limit: Option<String>,
196}
197
198impl Select {
199 fn render(&self) -> String {
200 let mut out = String::from("SELECT ");
201 if self.distinct {
202 out.push_str("DISTINCT ");
203 }
204 if self.select.is_empty() {
205 out.push('*');
206 } else {
207 out.push_str(&self.select.join(", "));
208 }
209 out.push_str(&format!(" FROM {}", self.from));
210 if let Some((t, on)) = &self.join {
211 out.push_str(&format!(" JOIN {t} ON {on}"));
212 }
213 if !self.wheres.is_empty() {
214 out.push_str(&format!(" WHERE {}", self.wheres.join(" AND ")));
215 }
216 if let Some(g) = &self.group_by {
217 out.push_str(&format!(" GROUP BY {g}"));
218 }
219 if let Some(h) = &self.having {
220 out.push_str(&format!(" HAVING {h}"));
221 }
222 if let Some((e, desc)) = &self.order_by {
223 out.push_str(&format!(" ORDER BY {e}"));
224 if *desc {
225 out.push_str(" DESC");
226 }
227 }
228 if let Some(n) = &self.limit {
229 out.push_str(&format!(" LIMIT {n}"));
230 }
231 out
232 }
233}
234
235pub struct Partial {
240 pub table: String,
241 pub where_sql: String,
242}
243
244pub fn partial_pushdown(quarb: &str) -> Option<Partial> {
258 partial_pushdown_explained(quarb).ok()
259}
260
261pub fn partial_pushdown_explained(quarb: &str) -> Result<Partial, SqlError> {
263 let arbor =
264 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
265 refuse_groups(&arbor)?;
266 let mut ex = Exporter {
267 arbor,
268 notes: Vec::new(),
269 strict: true,
270 from_table: String::new(),
271 join_on_left_cols: Vec::new(),
272 join_table: None,
273 aggregate: false,
274 };
275 ex.partial()
276}
277
278fn refuse_groups(arbor: &QueryArbor) -> Result<(), SqlError> {
284 let mut stack = vec![arbor.root()];
285 while let Some(n) = stack.pop() {
286 if arbor.name(n).as_deref() == Some("group") {
287 return Err(SqlError::Unsupported(
288 "path patterns (groups and quantifiers)".into(),
289 ));
290 }
291 stack.extend(arbor.children(n));
292 }
293 Ok(())
294}
295
296struct Exporter {
297 arbor: QueryArbor,
298 notes: Vec<String>,
299 strict: bool,
303 from_table: String,
304 join_on_left_cols: Vec<String>,
305 join_table: Option<String>,
306 aggregate: bool,
307}
308
309impl Exporter {
310 fn kids(&self, n: NodeId, kind: &str) -> Vec<NodeId> {
311 self.arbor
312 .children(n)
313 .into_iter()
314 .filter(|&c| self.arbor.name(c).as_deref() == Some(kind))
315 .collect()
316 }
317
318 fn kid(&self, n: NodeId, kind: &str) -> Option<NodeId> {
319 self.kids(n, kind).into_iter().next()
320 }
321
322 fn prop(&self, n: NodeId, key: &str) -> Option<Value> {
323 self.arbor.property(n, key)
324 }
325
326 fn prop_s(&self, n: NodeId, key: &str) -> String {
327 self.prop(n, key).map(|v| v.to_string()).unwrap_or_default()
328 }
329
330 fn kind(&self, n: NodeId) -> String {
331 self.arbor.name(n).unwrap_or_default()
332 }
333
334 fn is_null_literal(&self, n: NodeId) -> bool {
336 self.kind(n) == "literal" && self.prop_s(n, "type") == "null"
337 }
338
339 fn partial(&mut self) -> Result<Partial, SqlError> {
341 let root = self.arbor.root();
342 let q = self
343 .kid(root, "query")
344 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
345 if self.kid(q, "query").is_some() {
346 return Err(SqlError::Unsupported(
347 "partial pushdown: correlations address other tables".into(),
348 ));
349 }
350 let branches = self.kids(q, "branch");
351 if branches.len() != 1 {
352 return Err(SqlError::Unsupported(
353 "partial pushdown: a branch union".into(),
354 ));
355 }
356 let (table, preds) = self.table_branch(branches[0])?;
357
358 let all = self.walk_all(root);
360 let mut table_mentions = 0;
361 for n in &all {
362 match self.kind(*n).as_str() {
363 "step" => {
364 let axis = self.prop_s(*n, "axis");
365 if matches!(axis.as_str(), "->" | "<-" | "~>" | "<~") {
366 return Err(SqlError::Unsupported(
367 "partial pushdown: crosslink/resolution axes could reach \
368 the filtered table"
369 .into(),
370 ));
371 }
372 if self.prop_s(*n, "matcher") == table {
373 table_mentions += 1;
374 }
375 }
376 "projection" if self.prop_s(*n, "kind") != "property" => {
377 return Err(SqlError::Unsupported(
378 "partial pushdown: metadata would observe the filtering \
379 (::;n-rows, :::index)"
380 .into(),
381 ));
382 }
383 _ => {}
384 }
385 }
386 if table_mentions != 1 {
387 return Err(SqlError::Unsupported(
388 "partial pushdown: the table is reached more than once".into(),
389 ));
390 }
391
392 let mut conds = Vec::new();
395 for p in preds {
396 if self.prop_s(p, "kind") != "expr" {
397 break;
398 }
399 conds.push(self.predicate_cond(p, None)?);
400 }
401 if conds.is_empty() {
402 return Err(SqlError::Unsupported(
403 "partial pushdown: no leading expression predicates to push".into(),
404 ));
405 }
406 Ok(Partial {
407 table,
408 where_sql: conds.join(" AND "),
409 })
410 }
411
412 fn walk_all(&self, n: NodeId) -> Vec<NodeId> {
413 let mut out = vec![n];
414 let mut i = 0;
415 while i < out.len() {
416 out.extend(self.arbor.children(out[i]));
417 i += 1;
418 }
419 out
420 }
421
422 fn query(&mut self) -> Result<String, SqlError> {
423 let root = self.arbor.root();
424 let q = self
425 .kid(root, "query")
426 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
427 let mut sel = Select::default();
428
429 let corrs = self.kids(q, "query");
431 let branches = self.kids(q, "branch");
432 if branches.len() != 1 {
433 return Err(SqlError::Unsupported(
434 "a branch union (SQL has UNION, but result shapes differ; export one branch)"
435 .into(),
436 ));
437 }
438
439 if corrs.len() > 1 {
440 return Err(SqlError::Unsupported(
441 "more than one correlation context".into(),
442 ));
443 }
444 if let Some(corr) = corrs.first() {
445 let (ltable, lpreds) = self.table_branch(self.kids(*corr, "branch")[0])?;
447 if !lpreds.is_empty() {
448 return Err(SqlError::Unsupported(
449 "predicates on the correlation context (put them on the joined side)".into(),
450 ));
451 }
452 let (rtable, rpreds) = self.table_branch(branches[0])?;
453 let mut on = Vec::new();
456 let mut wheres = Vec::new();
457 for p in rpreds {
458 self.split_join_pred(p, <able, &rtable, &mut on, &mut wheres)?;
459 }
460 if on.is_empty() {
461 return Err(SqlError::Unsupported(
462 "a correlation without a '$*1' equality (no JOIN condition)".into(),
463 ));
464 }
465 self.notes.push(
466 "JOIN: Quarb's binding is existential (one row per joined-side row); \
467 SQL multiplies rows when several left rows match"
468 .to_string(),
469 );
470 sel.from = ltable.clone();
471 self.from_table = ltable.clone();
472 self.join_table = Some(rtable.clone());
473 sel.join = Some((rtable.clone(), on.join(" AND ")));
474 sel.wheres = wheres;
475 self.pipeline(q, &mut sel, Some((<able, &rtable)))?;
476 } else {
477 let (table, preds) = self.table_branch(branches[0])?;
478 sel.from = table.clone();
479 self.from_table = table.clone();
480 for p in preds {
481 let cond = self.predicate_cond(p, None)?;
482 sel.wheres.push(cond);
483 }
484 if let Some(proj) = self.kid(branches[0], "projection") {
486 sel.select.push(self.projection_col(proj)?);
487 }
488 self.pipeline(q, &mut sel, None)?;
489 }
490 Ok(sel.render())
491 }
492
493 fn table_branch(&mut self, b: NodeId) -> Result<(String, Vec<NodeId>), SqlError> {
496 let steps = self.kids(b, "step");
497 if steps.len() != 2 {
498 return Err(SqlError::Unsupported(
499 "navigation beyond /table/* (SQL sees tables and rows)".into(),
500 ));
501 }
502 let (t, rows) = (steps[0], steps[1]);
503 if self.prop_s(t, "axis") != "/"
504 || self.prop_s(t, "matcher-kind") != "name"
505 || self.prop_s(rows, "axis") != "/"
506 || self.prop_s(rows, "matcher-kind") != "any"
507 {
508 return Err(SqlError::Unsupported(
509 "navigation beyond /table/* (SQL sees tables and rows)".into(),
510 ));
511 }
512 if self.kid(t, "predicate").is_some() {
513 return Err(SqlError::Unsupported("a predicate on the table hop".into()));
514 }
515 Ok((self.prop_s(t, "matcher"), self.kids(rows, "predicate")))
516 }
517
518 fn predicate_cond(&mut self, p: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
521 if self.prop_s(p, "kind") != "expr" {
522 return Err(SqlError::Unsupported(
523 "a positional predicate on rows (SQL rows are unordered; ORDER BY + LIMIT)".into(),
524 ));
525 }
526 let parts: Vec<String> = self
527 .arbor
528 .children(p)
529 .into_iter()
530 .map(|c| self.pred_expr(c, qual))
531 .collect::<Result<_, _>>()?;
532 Ok(parts.join(" AND "))
533 }
534
535 fn pred_expr(&mut self, e: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
536 match self.kind(e).as_str() {
537 "and" | "or" => {
538 let op = self.kind(e).to_uppercase();
539 let kids: Vec<String> = self
540 .arbor
541 .children(e)
542 .into_iter()
543 .map(|c| self.pred_expr(c, qual))
544 .collect::<Result<_, _>>()?;
545 Ok(format!("({})", kids.join(&format!(" {op} "))))
546 }
547 "not" => {
548 if self.strict {
554 return Err(SqlError::Unsupported(
555 "pushdown: 'not(...)' drops the NULL rows Quarb keeps \
556 (SQL NOT propagates UNKNOWN)"
557 .into(),
558 ));
559 }
560 let inner: Vec<String> = self
561 .arbor
562 .children(e)
563 .into_iter()
564 .map(|c| self.pred_expr(c, qual))
565 .collect::<Result<_, _>>()?;
566 Ok(format!("NOT ({})", inner.join(" AND ")))
567 }
568 "parens" => {
569 let inner: Vec<String> = self
570 .arbor
571 .children(e)
572 .into_iter()
573 .map(|c| self.pred_expr(c, qual))
574 .collect::<Result<_, _>>()?;
575 Ok(format!("({})", inner.join(" AND ")))
576 }
577 "compare" => {
578 let op = self.prop_s(e, "op");
579 let kids = self.arbor.children(e);
580 if matches!(op.as_str(), "=" | "!=")
589 && (self.is_null_literal(kids[0]) || self.is_null_literal(kids[1]))
590 {
591 let other = if self.is_null_literal(kids[0]) {
592 kids[1]
593 } else {
594 kids[0]
595 };
596 let col = self.operand(other, qual)?;
597 return Ok(if op == "=" {
598 format!("{col} IS NULL")
599 } else {
600 format!("{col} IS NOT NULL")
601 });
602 }
603 let l = self.operand(kids[0], qual)?;
604 let r = self.operand(kids[1], qual)?;
605 Ok(match op.as_str() {
606 "=" => format!("{l} = {r}"),
607 "!=" => {
608 if self.strict {
616 return Err(SqlError::Unsupported(
617 "pushdown: '!=' drops the NULL rows Quarb keeps \
618 (SQL '<>' is UNKNOWN for NULL; use '!= null' \
619 for IS NOT NULL)"
620 .into(),
621 ));
622 }
623 self.notes.push(
624 "'!=' → '<>': Quarb keeps rows whose column is NULL; \
625 SQL's '<>' drops them (use '!= null' for IS NOT NULL)"
626 .to_string(),
627 );
628 format!("{l} <> {r}")
629 }
630 "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
631 "*=" => {
632 if self.strict {
633 return Err(SqlError::Unsupported(
634 "pushdown: LIKE case folding differs per engine".into(),
635 ));
636 }
637 self.notes.push(
638 "*= → LIKE: Quarb's substring test is case-sensitive; LIKE \
639 folds case on SQLite/MySQL but not PostgreSQL"
640 .to_string(),
641 );
642 let pat = r.trim_matches('\'').replace('%', "\\%").replace('_', "\\_");
643 format!("{l} LIKE '%{pat}%'")
644 }
645 "=~" | "!~" => {
646 return Err(SqlError::Unsupported(
647 "regex matching (REGEXP dialects disagree; use *= or spell \
648 the SQL by hand)"
649 .into(),
650 ));
651 }
652 other => {
653 return Err(SqlError::Unsupported(format!("the '{other}' comparison")));
654 }
655 })
656 }
657 _ => {
659 if self.strict {
660 return Err(SqlError::Unsupported(
661 "pushdown: truthiness diverges (0 and '' are falsy in Quarb)".into(),
662 ));
663 }
664 self.notes.push(
665 "truthiness: '[::c]' exports as IS NOT NULL, but Quarb also treats \
666 0 and '' as falsy"
667 .to_string(),
668 );
669 Ok(format!("{} IS NOT NULL", self.operand(e, qual)?))
670 }
671 }
672 }
673
674 fn operand(&mut self, o: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
675 match self.kind(o).as_str() {
676 "literal" => {
677 let v = self.prop(o, "value").unwrap_or(Value::Null);
678 match self.prop_s(o, "type").as_str() {
679 "text" => {
680 let s = v.to_string();
681 if self.strict && (s.contains('\'') || s.contains('\\')) {
692 return Err(SqlError::Unsupported(
693 "pushdown: a text literal with a quote or backslash \
694 has no escaping portable across SQL dialects"
695 .into(),
696 ));
697 }
698 Ok(format!("'{}'", s.replace('\'', "''")))
699 }
700 "null" => Ok("NULL".to_string()),
701 _ => Ok(v.to_string()),
702 }
703 }
704 "path" => {
705 if self.kid(o, "step").is_some() {
706 let s = self.kids(o, "step")[0];
709 if self.prop_s(s, "axis") == "~>" {
710 return Err(SqlError::Unsupported(
711 "a '~>' resolution chain: the foreign-key targets live in \
712 the schema, not the query — spell the join with '<=>' to \
713 export it"
714 .into(),
715 ));
716 }
717 return Err(SqlError::Unsupported(
718 "navigation inside a predicate (SQL rows are flat)".into(),
719 ));
720 }
721 let p = self
722 .kid(o, "projection")
723 .ok_or_else(|| SqlError::Unsupported("an empty path operand".into()))?;
724 let col = self.projection_col(p)?;
725 Ok(match qual {
726 Some(q) => format!("{q}.{col}"),
727 None => col,
728 })
729 }
730 "context" => {
731 let p = self
736 .kid(o, "projection")
737 .ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
738 let col = self.projection_col(p)?;
739 match self.prop(o, "index") {
740 None | Some(Value::Int(1)) => Ok(format!("__LEFT__.{col}")),
741 Some(Value::Int(2)) => match qual {
742 Some(q) => Ok(format!("{q}.{col}")),
743 None => Err(SqlError::Unsupported(
744 "$*2 outside a correlation join".into(),
745 )),
746 },
747 Some(v) => Err(SqlError::Unsupported(format!(
748 "pushdown: $*{v} beyond a two-branch correlation"
749 ))),
750 }
751 }
752 "arith" => {
753 let op = self.prop_s(o, "op");
754 let kids = self.arbor.children(o);
755 let l = self.operand(kids[0], qual)?;
756 let r = self.operand(kids[1], qual)?;
757 Ok(match op.as_str() {
758 "+" | "-" | "*" => format!("({l} {op} {r})"),
759 "div" => format!("({l} / {r})"),
760 "mod" => format!("({l} % {r})"),
761 other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
762 })
763 }
764 other => Err(SqlError::Unsupported(format!(
765 "the '{other}' operand (registers, topics, and captures are Quarb-side state)"
766 ))),
767 }
768 }
769
770 fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
771 match self.prop_s(p, "kind").as_str() {
772 "property" => match self.prop(p, "key") {
773 Some(k) => Ok(k.to_string()),
774 None => Err(SqlError::Unsupported(
775 "the bare '::' projection (name the column)".into(),
776 )),
777 },
778 other => Err(SqlError::Unsupported(format!(
779 "the {other} metadata projection"
780 ))),
781 }
782 }
783
784 fn alias(&mut self, name: &str) -> Result<String, SqlError> {
790 let plain = !name.is_empty()
791 && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
792 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
793 && !ALIAS_KEYWORDS.contains(&name.to_ascii_lowercase().as_str());
794 if plain {
795 return Ok(name.to_string());
796 }
797 if self.strict {
798 return Err(SqlError::Unsupported(format!(
799 "pushdown: field name {name:?} needs SQL quoting \
800 (dialects disagree); rename the field"
801 )));
802 }
803 self.notes
804 .push(format!("field name {name:?} double-quoted (ANSI)"));
805 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
806 }
807
808 fn split_join_pred(
809 &mut self,
810 p: NodeId,
811 ltable: &str,
812 rtable: &str,
813 on: &mut Vec<String>,
814 wheres: &mut Vec<String>,
815 ) -> Result<(), SqlError> {
816 if self.prop_s(p, "kind") != "expr" {
817 return Err(SqlError::Unsupported(
818 "a positional predicate on the joined side".into(),
819 ));
820 }
821 fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
824 if ex.kind(e) == "and" {
825 for c in ex.arbor.children(e) {
826 conjuncts(ex, c, out);
827 }
828 } else {
829 out.push(e);
830 }
831 }
832 let mut parts = Vec::new();
833 for c in self.arbor.children(p) {
834 conjuncts(self, c, &mut parts);
835 }
836 for e in parts {
837 let uses_ctx = self.subtree_has(e, "context");
838 let raw = self.pred_expr(e, Some(rtable))?;
839 let cond = raw.replace("__LEFT__", ltable);
840 if uses_ctx && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
841 for cap in raw.split("__LEFT__.").skip(1) {
847 let col: String = cap
848 .chars()
849 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
850 .collect();
851 if !col.is_empty() {
852 self.join_on_left_cols.push(col);
853 }
854 }
855 on.push(cond);
856 } else {
857 wheres.push(cond);
858 }
859 }
860 Ok(())
861 }
862
863 fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
864 if self.kind(n) == kind {
865 return true;
866 }
867 self.arbor
868 .children(n)
869 .into_iter()
870 .any(|c| self.subtree_has(c, kind))
871 }
872
873 fn pipeline(
875 &mut self,
876 q: NodeId,
877 sel: &mut Select,
878 join: Option<(&str, &str)>,
879 ) -> Result<(), SqlError> {
880 let Some(pipe) = self.kid(q, "pipeline") else {
881 return Ok(());
882 };
883 let stages: Vec<NodeId> = self.arbor.children(pipe);
884 let mut i = 0;
885 let mut pending_col: Option<String> = None;
887 let mut grouped: Option<(String, String, String)> = None;
889
890 while i < stages.len() {
891 let s = stages[i];
892 match self.kind(s).as_str() {
893 "expr" => {
894 let kids = self.arbor.children(s);
895 pending_col = Some(self.operand(kids[0], join.map(|(_, r)| r))?);
896 }
897 "func" => {
898 let name = self.prop_s(s, "name");
899 match name.as_str() {
900 "rec" | "record" => {
901 sel.select = self.record_fields(s, join)?;
902 }
903 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
906 let (key, _, _) = grouped.as_ref().ok_or_else(|| {
907 SqlError::Unsupported(format!(
908 "'| {name}' outside a group (use '@| {name}')"
909 ))
910 })?;
911 let agg = sql_agg(&name, pending_col.take());
912 grouped = Some((key.clone(), agg.clone(), agg));
913 }
914 other => {
915 return Err(SqlError::Unsupported(format!(
916 "the '{other}' pipeline function"
917 )));
918 }
919 }
920 }
921 "push" => {
922 if let Some((k, agg, _)) = grouped.take() {
924 let alias = self.prop_s(s, "name");
925 grouped = Some((k, agg, alias));
926 } else {
927 return Err(SqlError::Unsupported(
928 "a register push (Quarb-side state)".into(),
929 ));
930 }
931 }
932 "filter" => {
933 if grouped.is_some() {
934 let cond = self.having_cond(s)?;
937 sel.having = Some(cond);
938 } else {
939 return Err(SqlError::Unsupported(
940 "a mid-pipeline filter (put it in the row predicate)".into(),
941 ));
942 }
943 }
944 "recall" => {
945 if self.prop_s(s, "ref") != "%." {
947 return Err(SqlError::Unsupported(
948 "a register recall (Quarb-side state)".into(),
949 ));
950 }
951 let (key, agg, alias) = grouped.clone().ok_or_else(|| {
952 SqlError::Unsupported("'%.', with nothing grouped".into())
953 })?;
954 let alias = self.alias(&alias)?;
955 sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
956 if let Some(h) = sel.having.take() {
959 sel.having = Some(h.replace("__AGG__", &agg));
960 }
961 }
962 "agg" => {
963 let name = self.prop_s(s, "name");
964 match name.as_str() {
965 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
966 let col = if name == "count" {
970 pending_col = None;
971 None
972 } else {
973 pending_col.take()
974 };
975 sel.select = vec![sql_agg(&name, col)];
976 self.aggregate = true;
977 }
978 "group" => {
979 if self.strict {
980 return Err(SqlError::Unsupported(
981 "pushdown: GROUP BY result order is unordered in SQL".into(),
982 ));
983 }
984 let key = self.group_key(s, join)?;
985 sel.group_by = Some(key.clone());
986 grouped = Some((key, String::new(), String::new()));
987 }
988 "sort_by" => {
989 if self.strict {
990 return Err(SqlError::Unsupported(
991 "pushdown: ORDER BY collations differ per engine".into(),
992 ));
993 }
994 let kids = self.arbor.children(s);
995 let e = self.operand(kids[0], join.map(|(_, r)| r))?;
996 sel.order_by = Some((e, false));
997 }
998 "reverse" => match &mut sel.order_by {
999 Some((_, desc)) => *desc = true,
1000 None => {
1001 return Err(SqlError::Unsupported(
1002 "reverse without an ORDER BY (rows are unordered)".into(),
1003 ));
1004 }
1005 },
1006 "top" => {
1007 if self.strict {
1008 return Err(SqlError::Unsupported(
1009 "pushdown: ORDER BY collations differ per engine".into(),
1010 ));
1011 }
1012 let kids = self.arbor.children(s);
1013 let n = self.prop_s(kids[0], "value");
1014 let e = self.operand(kids[1], join.map(|(_, r)| r))?;
1015 sel.order_by = Some((e, true));
1016 sel.limit = Some(n);
1017 }
1018 "unique" => {
1019 if self.strict {
1020 return Err(SqlError::Unsupported(
1021 "pushdown: DISTINCT result order is unordered in SQL".into(),
1022 ));
1023 }
1024 if let Some(c) = pending_col.take() {
1025 sel.select = vec![c];
1026 }
1027 sel.distinct = true;
1028 }
1029 other => {
1030 return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
1031 }
1032 }
1033 }
1034 "select" => {
1035 if self.strict {
1036 return Err(SqlError::Unsupported(
1037 "pushdown: LIMIT without a guaranteed order".into(),
1038 ));
1039 }
1040 let p = self.arbor.children(s)[0];
1042 match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
1043 ("range", Some(Value::Int(n)))
1044 if self.prop(p, "from").is_none() && n > 0 =>
1045 {
1046 sel.limit = Some(n.to_string());
1047 }
1048 _ => {
1049 return Err(SqlError::Unsupported(
1050 "positional selection beyond '@| [..n]'".into(),
1051 ));
1052 }
1053 }
1054 }
1055 other => {
1056 return Err(SqlError::Unsupported(format!(
1057 "the '{other}' stage (windows, subcontexts, and registers are \
1058 Quarb-side state)"
1059 )));
1060 }
1061 }
1062 i += 1;
1063 }
1064 if let Some(c) = pending_col
1066 && sel.select.is_empty()
1067 {
1068 sel.select = vec![c];
1069 }
1070 Ok(())
1071 }
1072
1073 fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
1074 let kids = self.arbor.children(s);
1075 let key = kids
1078 .iter()
1079 .rev()
1080 .find(|&&k| self.kind(k) != "literal")
1081 .ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
1082 self.operand(*key, join.map(|(_, r)| r))
1083 }
1084
1085 fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
1087 let kids = self.arbor.children(s);
1088 if kids.len() != 1 || self.kind(kids[0]) != "compare" {
1089 return Err(SqlError::Unsupported(
1090 "HAVING translates for a single comparison".into(),
1091 ));
1092 }
1093 let e = kids[0];
1094 let op = self.prop_s(e, "op");
1095 let cmp_kids = self.arbor.children(e);
1096 let l = match self.kind(cmp_kids[0]).as_str() {
1097 "topic" | "recall" => "__AGG__".to_string(),
1098 _ => {
1099 return Err(SqlError::Unsupported(
1100 "HAVING compares the aggregate ($_ or its register)".into(),
1101 ));
1102 }
1103 };
1104 let r = self.operand(cmp_kids[1], None)?;
1105 Ok(format!("{l} {op} {r}"))
1106 }
1107
1108 fn record_fields(
1110 &mut self,
1111 s: NodeId,
1112 join: Option<(&str, &str)>,
1113 ) -> Result<Vec<String>, SqlError> {
1114 let kids = self.arbor.children(s);
1115 let mut fields = Vec::new();
1116 let mut i = 0;
1117 while i < kids.len() {
1118 let k = kids[i];
1119 if self.kind(k) == "literal" {
1120 let name = self.prop_s(k, "value");
1121 let value = self.operand(kids[i + 1], join.map(|(_, r)| r))?;
1122 let value = match join {
1123 Some((l, _)) => value.replace("__LEFT__", l),
1124 None => value,
1125 };
1126 let name = self.alias(&name)?;
1127 fields.push(format!("{value} AS {name}"));
1128 i += 2;
1129 } else {
1130 let value = self.operand(k, join.map(|(_, r)| r))?;
1131 let value = match join {
1132 Some((l, _)) => value.replace("__LEFT__", l),
1133 None => value,
1134 };
1135 fields.push(value);
1136 i += 1;
1137 }
1138 }
1139 Ok(fields)
1140 }
1141}
1142
1143fn sql_agg(name: &str, col: Option<String>) -> String {
1144 let f = match name {
1145 "count" => "COUNT",
1146 "sum" => "SUM",
1147 "mean" | "avg" => "AVG",
1148 "min" => "MIN",
1149 "max" => "MAX",
1150 _ => unreachable!("checked by caller"),
1151 };
1152 match col {
1153 Some(c) => format!("{f}({c})"),
1154 None => {
1155 if f == "COUNT" {
1156 "COUNT(*)".to_string()
1157 } else {
1158 format!("{f}(*)")
1159 }
1160 }
1161 }
1162}
1163
1164#[cfg(test)]
1165mod null_and_literal_tests {
1166 use super::{export, partial_pushdown, pushdown};
1167
1168 const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
1171
1172 #[test]
1173 fn null_literal_compares_use_is_null() {
1174 assert_eq!(
1178 export("/t/*[::x = null] | ::x").unwrap().query,
1179 "SELECT x FROM t WHERE x IS NULL"
1180 );
1181 assert_eq!(
1182 export("/t/*[::x != null] | ::x").unwrap().query,
1183 "SELECT x FROM t WHERE x IS NOT NULL"
1184 );
1185 assert_eq!(
1186 pushdown("/t/*[::x = null] | ::x").unwrap().sql,
1187 "SELECT x FROM t WHERE x IS NULL"
1188 );
1189 assert_eq!(
1190 pushdown("/t/*[::x != null] | ::x").unwrap().sql,
1191 "SELECT x FROM t WHERE x IS NOT NULL"
1192 );
1193 }
1194
1195 #[test]
1196 fn ne_and_not_refuse_pushdown_but_display_diverges() {
1197 assert!(pushdown("/t/*[::x != 5] | ::x").is_none());
1201 assert!(pushdown("/t/*[!::x = 5] | ::x").is_none());
1202 assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}")).is_none());
1203 assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}")).is_none());
1204 let t = export("/t/*[::x != 5] | ::x").unwrap();
1206 assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
1207 assert!(t.notes.iter().any(|n| n.contains("NULL")));
1208 }
1209
1210 #[test]
1211 fn unescapable_text_literal_refuses_pushdown() {
1212 assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path").is_none());
1216 assert!(pushdown("/t/*[::name = \"it's\"] | ::name").is_none());
1217 assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}")).is_none());
1218 assert_eq!(
1219 pushdown("/t/*[::name = \"rare\"] | ::name").unwrap().sql,
1220 "SELECT name FROM t WHERE name = 'rare'"
1221 );
1222 }
1223}