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
733 .kid(o, "projection")
734 .ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
735 Ok(format!("__LEFT__.{}", self.projection_col(p)?))
736 }
737 "arith" => {
738 let op = self.prop_s(o, "op");
739 let kids = self.arbor.children(o);
740 let l = self.operand(kids[0], qual)?;
741 let r = self.operand(kids[1], qual)?;
742 Ok(match op.as_str() {
743 "+" | "-" | "*" => format!("({l} {op} {r})"),
744 "div" => format!("({l} / {r})"),
745 "mod" => format!("({l} % {r})"),
746 other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
747 })
748 }
749 other => Err(SqlError::Unsupported(format!(
750 "the '{other}' operand (registers, topics, and captures are Quarb-side state)"
751 ))),
752 }
753 }
754
755 fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
756 match self.prop_s(p, "kind").as_str() {
757 "property" => match self.prop(p, "key") {
758 Some(k) => Ok(k.to_string()),
759 None => Err(SqlError::Unsupported(
760 "the bare '::' projection (name the column)".into(),
761 )),
762 },
763 other => Err(SqlError::Unsupported(format!(
764 "the {other} metadata projection"
765 ))),
766 }
767 }
768
769 fn alias(&mut self, name: &str) -> Result<String, SqlError> {
775 let plain = !name.is_empty()
776 && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
777 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
778 && !ALIAS_KEYWORDS.contains(&name.to_ascii_lowercase().as_str());
779 if plain {
780 return Ok(name.to_string());
781 }
782 if self.strict {
783 return Err(SqlError::Unsupported(format!(
784 "pushdown: field name {name:?} needs SQL quoting \
785 (dialects disagree); rename the field"
786 )));
787 }
788 self.notes
789 .push(format!("field name {name:?} double-quoted (ANSI)"));
790 Ok(format!("\"{}\"", name.replace('"', "\"\"")))
791 }
792
793 fn split_join_pred(
794 &mut self,
795 p: NodeId,
796 ltable: &str,
797 rtable: &str,
798 on: &mut Vec<String>,
799 wheres: &mut Vec<String>,
800 ) -> Result<(), SqlError> {
801 if self.prop_s(p, "kind") != "expr" {
802 return Err(SqlError::Unsupported(
803 "a positional predicate on the joined side".into(),
804 ));
805 }
806 fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
809 if ex.kind(e) == "and" {
810 for c in ex.arbor.children(e) {
811 conjuncts(ex, c, out);
812 }
813 } else {
814 out.push(e);
815 }
816 }
817 let mut parts = Vec::new();
818 for c in self.arbor.children(p) {
819 conjuncts(self, c, &mut parts);
820 }
821 for e in parts {
822 let uses_ctx = self.subtree_has(e, "context");
823 let raw = self.pred_expr(e, Some(rtable))?;
824 let cond = raw.replace("__LEFT__", ltable);
825 if uses_ctx && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
826 for cap in raw.split("__LEFT__.").skip(1) {
832 let col: String = cap
833 .chars()
834 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
835 .collect();
836 if !col.is_empty() {
837 self.join_on_left_cols.push(col);
838 }
839 }
840 on.push(cond);
841 } else {
842 wheres.push(cond);
843 }
844 }
845 Ok(())
846 }
847
848 fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
849 if self.kind(n) == kind {
850 return true;
851 }
852 self.arbor
853 .children(n)
854 .into_iter()
855 .any(|c| self.subtree_has(c, kind))
856 }
857
858 fn pipeline(
860 &mut self,
861 q: NodeId,
862 sel: &mut Select,
863 join: Option<(&str, &str)>,
864 ) -> Result<(), SqlError> {
865 let Some(pipe) = self.kid(q, "pipeline") else {
866 return Ok(());
867 };
868 let stages: Vec<NodeId> = self.arbor.children(pipe);
869 let mut i = 0;
870 let mut pending_col: Option<String> = None;
872 let mut grouped: Option<(String, String, String)> = None;
874
875 while i < stages.len() {
876 let s = stages[i];
877 match self.kind(s).as_str() {
878 "expr" => {
879 let kids = self.arbor.children(s);
880 pending_col = Some(self.operand(kids[0], join.map(|(_, r)| r))?);
881 }
882 "func" => {
883 let name = self.prop_s(s, "name");
884 match name.as_str() {
885 "rec" | "record" => {
886 sel.select = self.record_fields(s, join)?;
887 }
888 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
891 let (key, _, _) = grouped.as_ref().ok_or_else(|| {
892 SqlError::Unsupported(format!(
893 "'| {name}' outside a group (use '@| {name}')"
894 ))
895 })?;
896 let agg = sql_agg(&name, pending_col.take());
897 grouped = Some((key.clone(), agg.clone(), agg));
898 }
899 other => {
900 return Err(SqlError::Unsupported(format!(
901 "the '{other}' pipeline function"
902 )));
903 }
904 }
905 }
906 "push" => {
907 if let Some((k, agg, _)) = grouped.take() {
909 let alias = self.prop_s(s, "name");
910 grouped = Some((k, agg, alias));
911 } else {
912 return Err(SqlError::Unsupported(
913 "a register push (Quarb-side state)".into(),
914 ));
915 }
916 }
917 "filter" => {
918 if grouped.is_some() {
919 let cond = self.having_cond(s)?;
922 sel.having = Some(cond);
923 } else {
924 return Err(SqlError::Unsupported(
925 "a mid-pipeline filter (put it in the row predicate)".into(),
926 ));
927 }
928 }
929 "recall" => {
930 if self.prop_s(s, "ref") != "%." {
932 return Err(SqlError::Unsupported(
933 "a register recall (Quarb-side state)".into(),
934 ));
935 }
936 let (key, agg, alias) = grouped.clone().ok_or_else(|| {
937 SqlError::Unsupported("'%.', with nothing grouped".into())
938 })?;
939 let alias = self.alias(&alias)?;
940 sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
941 if let Some(h) = sel.having.take() {
944 sel.having = Some(h.replace("__AGG__", &agg));
945 }
946 }
947 "agg" => {
948 let name = self.prop_s(s, "name");
949 match name.as_str() {
950 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
951 let col = if name == "count" {
955 pending_col = None;
956 None
957 } else {
958 pending_col.take()
959 };
960 sel.select = vec![sql_agg(&name, col)];
961 self.aggregate = true;
962 }
963 "group" => {
964 if self.strict {
965 return Err(SqlError::Unsupported(
966 "pushdown: GROUP BY result order is unordered in SQL".into(),
967 ));
968 }
969 let key = self.group_key(s, join)?;
970 sel.group_by = Some(key.clone());
971 grouped = Some((key, String::new(), String::new()));
972 }
973 "sort_by" => {
974 if self.strict {
975 return Err(SqlError::Unsupported(
976 "pushdown: ORDER BY collations differ per engine".into(),
977 ));
978 }
979 let kids = self.arbor.children(s);
980 let e = self.operand(kids[0], join.map(|(_, r)| r))?;
981 sel.order_by = Some((e, false));
982 }
983 "reverse" => match &mut sel.order_by {
984 Some((_, desc)) => *desc = true,
985 None => {
986 return Err(SqlError::Unsupported(
987 "reverse without an ORDER BY (rows are unordered)".into(),
988 ));
989 }
990 },
991 "top" => {
992 if self.strict {
993 return Err(SqlError::Unsupported(
994 "pushdown: ORDER BY collations differ per engine".into(),
995 ));
996 }
997 let kids = self.arbor.children(s);
998 let n = self.prop_s(kids[0], "value");
999 let e = self.operand(kids[1], join.map(|(_, r)| r))?;
1000 sel.order_by = Some((e, true));
1001 sel.limit = Some(n);
1002 }
1003 "unique" => {
1004 if self.strict {
1005 return Err(SqlError::Unsupported(
1006 "pushdown: DISTINCT result order is unordered in SQL".into(),
1007 ));
1008 }
1009 if let Some(c) = pending_col.take() {
1010 sel.select = vec![c];
1011 }
1012 sel.distinct = true;
1013 }
1014 other => {
1015 return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
1016 }
1017 }
1018 }
1019 "select" => {
1020 if self.strict {
1021 return Err(SqlError::Unsupported(
1022 "pushdown: LIMIT without a guaranteed order".into(),
1023 ));
1024 }
1025 let p = self.arbor.children(s)[0];
1027 match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
1028 ("range", Some(Value::Int(n)))
1029 if self.prop(p, "from").is_none() && n > 0 =>
1030 {
1031 sel.limit = Some(n.to_string());
1032 }
1033 _ => {
1034 return Err(SqlError::Unsupported(
1035 "positional selection beyond '@| [..n]'".into(),
1036 ));
1037 }
1038 }
1039 }
1040 other => {
1041 return Err(SqlError::Unsupported(format!(
1042 "the '{other}' stage (windows, subcontexts, and registers are \
1043 Quarb-side state)"
1044 )));
1045 }
1046 }
1047 i += 1;
1048 }
1049 if let Some(c) = pending_col
1051 && sel.select.is_empty()
1052 {
1053 sel.select = vec![c];
1054 }
1055 Ok(())
1056 }
1057
1058 fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
1059 let kids = self.arbor.children(s);
1060 let key = kids
1063 .iter()
1064 .rev()
1065 .find(|&&k| self.kind(k) != "literal")
1066 .ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
1067 self.operand(*key, join.map(|(_, r)| r))
1068 }
1069
1070 fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
1072 let kids = self.arbor.children(s);
1073 if kids.len() != 1 || self.kind(kids[0]) != "compare" {
1074 return Err(SqlError::Unsupported(
1075 "HAVING translates for a single comparison".into(),
1076 ));
1077 }
1078 let e = kids[0];
1079 let op = self.prop_s(e, "op");
1080 let cmp_kids = self.arbor.children(e);
1081 let l = match self.kind(cmp_kids[0]).as_str() {
1082 "topic" | "recall" => "__AGG__".to_string(),
1083 _ => {
1084 return Err(SqlError::Unsupported(
1085 "HAVING compares the aggregate ($_ or its register)".into(),
1086 ));
1087 }
1088 };
1089 let r = self.operand(cmp_kids[1], None)?;
1090 Ok(format!("{l} {op} {r}"))
1091 }
1092
1093 fn record_fields(
1095 &mut self,
1096 s: NodeId,
1097 join: Option<(&str, &str)>,
1098 ) -> Result<Vec<String>, SqlError> {
1099 let kids = self.arbor.children(s);
1100 let mut fields = Vec::new();
1101 let mut i = 0;
1102 while i < kids.len() {
1103 let k = kids[i];
1104 if self.kind(k) == "literal" {
1105 let name = self.prop_s(k, "value");
1106 let value = self.operand(kids[i + 1], join.map(|(_, r)| r))?;
1107 let value = match join {
1108 Some((l, _)) => value.replace("__LEFT__", l),
1109 None => value,
1110 };
1111 let name = self.alias(&name)?;
1112 fields.push(format!("{value} AS {name}"));
1113 i += 2;
1114 } else {
1115 let value = self.operand(k, join.map(|(_, r)| r))?;
1116 let value = match join {
1117 Some((l, _)) => value.replace("__LEFT__", l),
1118 None => value,
1119 };
1120 fields.push(value);
1121 i += 1;
1122 }
1123 }
1124 Ok(fields)
1125 }
1126}
1127
1128fn sql_agg(name: &str, col: Option<String>) -> String {
1129 let f = match name {
1130 "count" => "COUNT",
1131 "sum" => "SUM",
1132 "mean" | "avg" => "AVG",
1133 "min" => "MIN",
1134 "max" => "MAX",
1135 _ => unreachable!("checked by caller"),
1136 };
1137 match col {
1138 Some(c) => format!("{f}({c})"),
1139 None => {
1140 if f == "COUNT" {
1141 "COUNT(*)".to_string()
1142 } else {
1143 format!("{f}(*)")
1144 }
1145 }
1146 }
1147}
1148
1149#[cfg(test)]
1150mod null_and_literal_tests {
1151 use super::{export, partial_pushdown, pushdown};
1152
1153 const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
1156
1157 #[test]
1158 fn null_literal_compares_use_is_null() {
1159 assert_eq!(
1163 export("/t/*[::x = null] | ::x").unwrap().query,
1164 "SELECT x FROM t WHERE x IS NULL"
1165 );
1166 assert_eq!(
1167 export("/t/*[::x != null] | ::x").unwrap().query,
1168 "SELECT x FROM t WHERE x IS NOT NULL"
1169 );
1170 assert_eq!(
1171 pushdown("/t/*[::x = null] | ::x").unwrap().sql,
1172 "SELECT x FROM t WHERE x IS NULL"
1173 );
1174 assert_eq!(
1175 pushdown("/t/*[::x != null] | ::x").unwrap().sql,
1176 "SELECT x FROM t WHERE x IS NOT NULL"
1177 );
1178 }
1179
1180 #[test]
1181 fn ne_and_not_refuse_pushdown_but_display_diverges() {
1182 assert!(pushdown("/t/*[::x != 5] | ::x").is_none());
1186 assert!(pushdown("/t/*[!::x = 5] | ::x").is_none());
1187 assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}")).is_none());
1188 assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}")).is_none());
1189 let t = export("/t/*[::x != 5] | ::x").unwrap();
1191 assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
1192 assert!(t.notes.iter().any(|n| n.contains("NULL")));
1193 }
1194
1195 #[test]
1196 fn unescapable_text_literal_refuses_pushdown() {
1197 assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path").is_none());
1201 assert!(pushdown("/t/*[::name = \"it's\"] | ::name").is_none());
1202 assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}")).is_none());
1203 assert_eq!(
1204 pushdown("/t/*[::name = \"rare\"] | ::name").unwrap().sql,
1205 "SELECT name FROM t WHERE name = 'rare'"
1206 );
1207 }
1208}