1use crate::{SqlError, Translation};
32use quarb::reflect::QueryArbor;
33use quarb::{AstAdapter, NodeId, Value};
34
35pub fn export(quarb: &str) -> Result<Translation, SqlError> {
37 let arbor =
38 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
39 refuse_groups(&arbor)?;
40 let mut ex = Exporter {
41 arbor,
42 notes: Vec::new(),
43 strict: false,
44 from_table: String::new(),
45 join_table: None,
46 aggregate: false,
47 };
48 let query = ex.query()?;
49 Ok(Translation {
50 query,
51 notes: ex.notes,
52 })
53}
54
55pub struct Pushdown {
61 pub sql: String,
62 pub order_table: Option<String>,
63}
64
65pub fn pushdown(quarb: &str) -> Option<Pushdown> {
70 pushdown_explained(quarb).ok()
71}
72
73pub fn pushdown_explained(quarb: &str) -> Result<Pushdown, SqlError> {
76 let arbor =
77 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
78 refuse_groups(&arbor)?;
79 let mut ex = Exporter {
80 arbor,
81 notes: Vec::new(),
82 strict: true,
83 from_table: String::new(),
84 join_table: None,
85 aggregate: false,
86 };
87 let sql = ex.query()?;
88 let order_table = if ex.aggregate {
89 None
90 } else {
91 Some(ex.join_table.clone().unwrap_or(ex.from_table.clone()))
95 };
96 Ok(Pushdown { sql, order_table })
97}
98
99#[derive(Default)]
101struct Select {
102 select: Vec<String>,
103 distinct: bool,
104 from: String,
105 join: Option<(String, String)>, wheres: Vec<String>,
107 group_by: Option<String>,
108 having: Option<String>,
109 order_by: Option<(String, bool)>, limit: Option<String>,
111}
112
113impl Select {
114 fn render(&self) -> String {
115 let mut out = String::from("SELECT ");
116 if self.distinct {
117 out.push_str("DISTINCT ");
118 }
119 if self.select.is_empty() {
120 out.push('*');
121 } else {
122 out.push_str(&self.select.join(", "));
123 }
124 out.push_str(&format!(" FROM {}", self.from));
125 if let Some((t, on)) = &self.join {
126 out.push_str(&format!(" JOIN {t} ON {on}"));
127 }
128 if !self.wheres.is_empty() {
129 out.push_str(&format!(" WHERE {}", self.wheres.join(" AND ")));
130 }
131 if let Some(g) = &self.group_by {
132 out.push_str(&format!(" GROUP BY {g}"));
133 }
134 if let Some(h) = &self.having {
135 out.push_str(&format!(" HAVING {h}"));
136 }
137 if let Some((e, desc)) = &self.order_by {
138 out.push_str(&format!(" ORDER BY {e}"));
139 if *desc {
140 out.push_str(" DESC");
141 }
142 }
143 if let Some(n) = &self.limit {
144 out.push_str(&format!(" LIMIT {n}"));
145 }
146 out
147 }
148}
149
150pub struct Partial {
155 pub table: String,
156 pub where_sql: String,
157}
158
159pub fn partial_pushdown(quarb: &str) -> Option<Partial> {
173 partial_pushdown_explained(quarb).ok()
174}
175
176pub fn partial_pushdown_explained(quarb: &str) -> Result<Partial, SqlError> {
178 let arbor =
179 QueryArbor::parse(quarb).map_err(|e| SqlError::Syntax(format!("parsing Quarb: {e}")))?;
180 refuse_groups(&arbor)?;
181 let mut ex = Exporter {
182 arbor,
183 notes: Vec::new(),
184 strict: true,
185 from_table: String::new(),
186 join_table: None,
187 aggregate: false,
188 };
189 ex.partial()
190}
191
192fn refuse_groups(arbor: &QueryArbor) -> Result<(), SqlError> {
198 let mut stack = vec![arbor.root()];
199 while let Some(n) = stack.pop() {
200 if arbor.name(n).as_deref() == Some("group") {
201 return Err(SqlError::Unsupported(
202 "path patterns (groups and quantifiers)".into(),
203 ));
204 }
205 stack.extend(arbor.children(n));
206 }
207 Ok(())
208}
209
210struct Exporter {
211 arbor: QueryArbor,
212 notes: Vec<String>,
213 strict: bool,
217 from_table: String,
218 join_table: Option<String>,
219 aggregate: bool,
220}
221
222impl Exporter {
223 fn kids(&self, n: NodeId, kind: &str) -> Vec<NodeId> {
224 self.arbor
225 .children(n)
226 .into_iter()
227 .filter(|&c| self.arbor.name(c).as_deref() == Some(kind))
228 .collect()
229 }
230
231 fn kid(&self, n: NodeId, kind: &str) -> Option<NodeId> {
232 self.kids(n, kind).into_iter().next()
233 }
234
235 fn prop(&self, n: NodeId, key: &str) -> Option<Value> {
236 self.arbor.property(n, key)
237 }
238
239 fn prop_s(&self, n: NodeId, key: &str) -> String {
240 self.prop(n, key).map(|v| v.to_string()).unwrap_or_default()
241 }
242
243 fn kind(&self, n: NodeId) -> String {
244 self.arbor.name(n).unwrap_or_default()
245 }
246
247 fn is_null_literal(&self, n: NodeId) -> bool {
249 self.kind(n) == "literal" && self.prop_s(n, "type") == "null"
250 }
251
252 fn partial(&mut self) -> Result<Partial, SqlError> {
254 let root = self.arbor.root();
255 let q = self
256 .kid(root, "query")
257 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
258 if self.kid(q, "query").is_some() {
259 return Err(SqlError::Unsupported(
260 "partial pushdown: correlations address other tables".into(),
261 ));
262 }
263 let branches = self.kids(q, "branch");
264 if branches.len() != 1 {
265 return Err(SqlError::Unsupported(
266 "partial pushdown: a branch union".into(),
267 ));
268 }
269 let (table, preds) = self.table_branch(branches[0])?;
270
271 let all = self.walk_all(root);
273 let mut table_mentions = 0;
274 for n in &all {
275 match self.kind(*n).as_str() {
276 "step" => {
277 let axis = self.prop_s(*n, "axis");
278 if matches!(axis.as_str(), "->" | "<-" | "~>" | "<~") {
279 return Err(SqlError::Unsupported(
280 "partial pushdown: crosslink/resolution axes could reach \
281 the filtered table"
282 .into(),
283 ));
284 }
285 if self.prop_s(*n, "matcher") == table {
286 table_mentions += 1;
287 }
288 }
289 "projection" if self.prop_s(*n, "kind") != "property" => {
290 return Err(SqlError::Unsupported(
291 "partial pushdown: metadata would observe the filtering \
292 (::;n-rows, :::index)"
293 .into(),
294 ));
295 }
296 _ => {}
297 }
298 }
299 if table_mentions != 1 {
300 return Err(SqlError::Unsupported(
301 "partial pushdown: the table is reached more than once".into(),
302 ));
303 }
304
305 let mut conds = Vec::new();
308 for p in preds {
309 if self.prop_s(p, "kind") != "expr" {
310 break;
311 }
312 conds.push(self.predicate_cond(p, None)?);
313 }
314 if conds.is_empty() {
315 return Err(SqlError::Unsupported(
316 "partial pushdown: no leading expression predicates to push".into(),
317 ));
318 }
319 Ok(Partial {
320 table,
321 where_sql: conds.join(" AND "),
322 })
323 }
324
325 fn walk_all(&self, n: NodeId) -> Vec<NodeId> {
326 let mut out = vec![n];
327 let mut i = 0;
328 while i < out.len() {
329 out.extend(self.arbor.children(out[i]));
330 i += 1;
331 }
332 out
333 }
334
335 fn query(&mut self) -> Result<String, SqlError> {
336 let root = self.arbor.root();
337 let q = self
338 .kid(root, "query")
339 .ok_or_else(|| SqlError::Unsupported("empty query".into()))?;
340 let mut sel = Select::default();
341
342 let corrs = self.kids(q, "query");
344 let branches = self.kids(q, "branch");
345 if branches.len() != 1 {
346 return Err(SqlError::Unsupported(
347 "a branch union (SQL has UNION, but result shapes differ; export one branch)"
348 .into(),
349 ));
350 }
351
352 if corrs.len() > 1 {
353 return Err(SqlError::Unsupported(
354 "more than one correlation context".into(),
355 ));
356 }
357 if let Some(corr) = corrs.first() {
358 let (ltable, lpreds) = self.table_branch(self.kids(*corr, "branch")[0])?;
360 if !lpreds.is_empty() {
361 return Err(SqlError::Unsupported(
362 "predicates on the correlation context (put them on the joined side)".into(),
363 ));
364 }
365 let (rtable, rpreds) = self.table_branch(branches[0])?;
366 let mut on = Vec::new();
369 let mut wheres = Vec::new();
370 for p in rpreds {
371 self.split_join_pred(p, <able, &rtable, &mut on, &mut wheres)?;
372 }
373 if on.is_empty() {
374 return Err(SqlError::Unsupported(
375 "a correlation without a '$*1' equality (no JOIN condition)".into(),
376 ));
377 }
378 self.notes.push(
379 "JOIN: Quarb's binding is existential (one row per joined-side row); \
380 SQL multiplies rows when several left rows match"
381 .to_string(),
382 );
383 sel.from = ltable.clone();
384 self.from_table = ltable.clone();
385 self.join_table = Some(rtable.clone());
386 sel.join = Some((rtable.clone(), on.join(" AND ")));
387 sel.wheres = wheres;
388 self.pipeline(q, &mut sel, Some((<able, &rtable)))?;
389 } else {
390 let (table, preds) = self.table_branch(branches[0])?;
391 sel.from = table.clone();
392 self.from_table = table.clone();
393 for p in preds {
394 let cond = self.predicate_cond(p, None)?;
395 sel.wheres.push(cond);
396 }
397 if let Some(proj) = self.kid(branches[0], "projection") {
399 sel.select.push(self.projection_col(proj)?);
400 }
401 self.pipeline(q, &mut sel, None)?;
402 }
403 Ok(sel.render())
404 }
405
406 fn table_branch(&mut self, b: NodeId) -> Result<(String, Vec<NodeId>), SqlError> {
409 let steps = self.kids(b, "step");
410 if steps.len() != 2 {
411 return Err(SqlError::Unsupported(
412 "navigation beyond /table/* (SQL sees tables and rows)".into(),
413 ));
414 }
415 let (t, rows) = (steps[0], steps[1]);
416 if self.prop_s(t, "axis") != "/"
417 || self.prop_s(t, "matcher-kind") != "name"
418 || self.prop_s(rows, "axis") != "/"
419 || self.prop_s(rows, "matcher-kind") != "any"
420 {
421 return Err(SqlError::Unsupported(
422 "navigation beyond /table/* (SQL sees tables and rows)".into(),
423 ));
424 }
425 if self.kid(t, "predicate").is_some() {
426 return Err(SqlError::Unsupported("a predicate on the table hop".into()));
427 }
428 Ok((self.prop_s(t, "matcher"), self.kids(rows, "predicate")))
429 }
430
431 fn predicate_cond(&mut self, p: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
434 if self.prop_s(p, "kind") != "expr" {
435 return Err(SqlError::Unsupported(
436 "a positional predicate on rows (SQL rows are unordered; ORDER BY + LIMIT)".into(),
437 ));
438 }
439 let parts: Vec<String> = self
440 .arbor
441 .children(p)
442 .into_iter()
443 .map(|c| self.pred_expr(c, qual))
444 .collect::<Result<_, _>>()?;
445 Ok(parts.join(" AND "))
446 }
447
448 fn pred_expr(&mut self, e: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
449 match self.kind(e).as_str() {
450 "and" | "or" => {
451 let op = self.kind(e).to_uppercase();
452 let kids: Vec<String> = self
453 .arbor
454 .children(e)
455 .into_iter()
456 .map(|c| self.pred_expr(c, qual))
457 .collect::<Result<_, _>>()?;
458 Ok(format!("({})", kids.join(&format!(" {op} "))))
459 }
460 "not" => {
461 if self.strict {
467 return Err(SqlError::Unsupported(
468 "pushdown: 'not(...)' drops the NULL rows Quarb keeps \
469 (SQL NOT propagates UNKNOWN)"
470 .into(),
471 ));
472 }
473 let inner: Vec<String> = self
474 .arbor
475 .children(e)
476 .into_iter()
477 .map(|c| self.pred_expr(c, qual))
478 .collect::<Result<_, _>>()?;
479 Ok(format!("NOT ({})", inner.join(" AND ")))
480 }
481 "parens" => {
482 let inner: Vec<String> = self
483 .arbor
484 .children(e)
485 .into_iter()
486 .map(|c| self.pred_expr(c, qual))
487 .collect::<Result<_, _>>()?;
488 Ok(format!("({})", inner.join(" AND ")))
489 }
490 "compare" => {
491 let op = self.prop_s(e, "op");
492 let kids = self.arbor.children(e);
493 if matches!(op.as_str(), "=" | "!=")
502 && (self.is_null_literal(kids[0]) || self.is_null_literal(kids[1]))
503 {
504 let other = if self.is_null_literal(kids[0]) {
505 kids[1]
506 } else {
507 kids[0]
508 };
509 let col = self.operand(other, qual)?;
510 return Ok(if op == "=" {
511 format!("{col} IS NULL")
512 } else {
513 format!("{col} IS NOT NULL")
514 });
515 }
516 let l = self.operand(kids[0], qual)?;
517 let r = self.operand(kids[1], qual)?;
518 Ok(match op.as_str() {
519 "=" => format!("{l} = {r}"),
520 "!=" => {
521 if self.strict {
529 return Err(SqlError::Unsupported(
530 "pushdown: '!=' drops the NULL rows Quarb keeps \
531 (SQL '<>' is UNKNOWN for NULL; use '!= null' \
532 for IS NOT NULL)"
533 .into(),
534 ));
535 }
536 self.notes.push(
537 "'!=' → '<>': Quarb keeps rows whose column is NULL; \
538 SQL's '<>' drops them (use '!= null' for IS NOT NULL)"
539 .to_string(),
540 );
541 format!("{l} <> {r}")
542 }
543 "<" | "<=" | ">" | ">=" => format!("{l} {op} {r}"),
544 "*=" => {
545 if self.strict {
546 return Err(SqlError::Unsupported(
547 "pushdown: LIKE case folding differs per engine".into(),
548 ));
549 }
550 self.notes.push(
551 "*= → LIKE: Quarb's substring test is case-sensitive; LIKE \
552 folds case on SQLite/MySQL but not PostgreSQL"
553 .to_string(),
554 );
555 let pat = r.trim_matches('\'').replace('%', "\\%").replace('_', "\\_");
556 format!("{l} LIKE '%{pat}%'")
557 }
558 "=~" | "!~" => {
559 return Err(SqlError::Unsupported(
560 "regex matching (REGEXP dialects disagree; use *= or spell \
561 the SQL by hand)"
562 .into(),
563 ));
564 }
565 other => {
566 return Err(SqlError::Unsupported(format!("the '{other}' comparison")));
567 }
568 })
569 }
570 _ => {
572 if self.strict {
573 return Err(SqlError::Unsupported(
574 "pushdown: truthiness diverges (0 and '' are falsy in Quarb)".into(),
575 ));
576 }
577 self.notes.push(
578 "truthiness: '[::c]' exports as IS NOT NULL, but Quarb also treats \
579 0 and '' as falsy"
580 .to_string(),
581 );
582 Ok(format!("{} IS NOT NULL", self.operand(e, qual)?))
583 }
584 }
585 }
586
587 fn operand(&mut self, o: NodeId, qual: Option<&str>) -> Result<String, SqlError> {
588 match self.kind(o).as_str() {
589 "literal" => {
590 let v = self.prop(o, "value").unwrap_or(Value::Null);
591 match self.prop_s(o, "type").as_str() {
592 "text" => {
593 let s = v.to_string();
594 if self.strict && (s.contains('\'') || s.contains('\\')) {
605 return Err(SqlError::Unsupported(
606 "pushdown: a text literal with a quote or backslash \
607 has no escaping portable across SQL dialects"
608 .into(),
609 ));
610 }
611 Ok(format!("'{}'", s.replace('\'', "''")))
612 }
613 "null" => Ok("NULL".to_string()),
614 _ => Ok(v.to_string()),
615 }
616 }
617 "path" => {
618 if self.kid(o, "step").is_some() {
619 let s = self.kids(o, "step")[0];
622 if self.prop_s(s, "axis") == "~>" {
623 return Err(SqlError::Unsupported(
624 "a '~>' resolution chain: the foreign-key targets live in \
625 the schema, not the query — spell the join with '<=>' to \
626 export it"
627 .into(),
628 ));
629 }
630 return Err(SqlError::Unsupported(
631 "navigation inside a predicate (SQL rows are flat)".into(),
632 ));
633 }
634 let p = self
635 .kid(o, "projection")
636 .ok_or_else(|| SqlError::Unsupported("an empty path operand".into()))?;
637 let col = self.projection_col(p)?;
638 Ok(match qual {
639 Some(q) => format!("{q}.{col}"),
640 None => col,
641 })
642 }
643 "context" => {
644 let p = self
646 .kid(o, "projection")
647 .ok_or_else(|| SqlError::Unsupported("a bare '$*' reference".into()))?;
648 Ok(format!("__LEFT__.{}", self.projection_col(p)?))
649 }
650 "arith" => {
651 let op = self.prop_s(o, "op");
652 let kids = self.arbor.children(o);
653 let l = self.operand(kids[0], qual)?;
654 let r = self.operand(kids[1], qual)?;
655 Ok(match op.as_str() {
656 "+" | "-" | "*" => format!("({l} {op} {r})"),
657 "div" => format!("({l} / {r})"),
658 "mod" => format!("({l} % {r})"),
659 other => return Err(SqlError::Unsupported(format!("'{other}' arithmetic"))),
660 })
661 }
662 other => Err(SqlError::Unsupported(format!(
663 "the '{other}' operand (registers, topics, and captures are Quarb-side state)"
664 ))),
665 }
666 }
667
668 fn projection_col(&mut self, p: NodeId) -> Result<String, SqlError> {
669 match self.prop_s(p, "kind").as_str() {
670 "property" => match self.prop(p, "key") {
671 Some(k) => Ok(k.to_string()),
672 None => Err(SqlError::Unsupported(
673 "the bare '::' projection (name the column)".into(),
674 )),
675 },
676 other => Err(SqlError::Unsupported(format!(
677 "the {other} metadata projection"
678 ))),
679 }
680 }
681
682 fn split_join_pred(
685 &mut self,
686 p: NodeId,
687 ltable: &str,
688 rtable: &str,
689 on: &mut Vec<String>,
690 wheres: &mut Vec<String>,
691 ) -> Result<(), SqlError> {
692 if self.prop_s(p, "kind") != "expr" {
693 return Err(SqlError::Unsupported(
694 "a positional predicate on the joined side".into(),
695 ));
696 }
697 fn conjuncts(ex: &Exporter, e: NodeId, out: &mut Vec<NodeId>) {
700 if ex.kind(e) == "and" {
701 for c in ex.arbor.children(e) {
702 conjuncts(ex, c, out);
703 }
704 } else {
705 out.push(e);
706 }
707 }
708 let mut parts = Vec::new();
709 for c in self.arbor.children(p) {
710 conjuncts(self, c, &mut parts);
711 }
712 for e in parts {
713 let uses_ctx = self.subtree_has(e, "context");
714 let cond = self.pred_expr(e, Some(rtable))?;
715 let cond = cond.replace("__LEFT__", ltable);
716 if uses_ctx && self.kind(e) == "compare" && self.prop_s(e, "op") == "=" {
717 on.push(cond);
718 } else {
719 wheres.push(cond);
720 }
721 }
722 Ok(())
723 }
724
725 fn subtree_has(&self, n: NodeId, kind: &str) -> bool {
726 if self.kind(n) == kind {
727 return true;
728 }
729 self.arbor
730 .children(n)
731 .into_iter()
732 .any(|c| self.subtree_has(c, kind))
733 }
734
735 fn pipeline(
737 &mut self,
738 q: NodeId,
739 sel: &mut Select,
740 join: Option<(&str, &str)>,
741 ) -> Result<(), SqlError> {
742 let Some(pipe) = self.kid(q, "pipeline") else {
743 return Ok(());
744 };
745 let stages: Vec<NodeId> = self.arbor.children(pipe);
746 let mut i = 0;
747 let mut pending_col: Option<String> = None;
749 let mut grouped: Option<(String, String, String)> = None;
751
752 while i < stages.len() {
753 let s = stages[i];
754 match self.kind(s).as_str() {
755 "expr" => {
756 let kids = self.arbor.children(s);
757 pending_col = Some(self.operand(kids[0], join.map(|(_, r)| r))?);
758 }
759 "func" => {
760 let name = self.prop_s(s, "name");
761 match name.as_str() {
762 "rec" | "record" => {
763 sel.select = self.record_fields(s, join)?;
764 }
765 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
768 let (key, _, _) = grouped.as_ref().ok_or_else(|| {
769 SqlError::Unsupported(format!(
770 "'| {name}' outside a group (use '@| {name}')"
771 ))
772 })?;
773 let agg = sql_agg(&name, pending_col.take());
774 grouped = Some((key.clone(), agg.clone(), agg));
775 }
776 other => {
777 return Err(SqlError::Unsupported(format!(
778 "the '{other}' pipeline function"
779 )));
780 }
781 }
782 }
783 "push" => {
784 if let Some((k, agg, _)) = grouped.take() {
786 let alias = self.prop_s(s, "name");
787 grouped = Some((k, agg, alias));
788 } else {
789 return Err(SqlError::Unsupported(
790 "a register push (Quarb-side state)".into(),
791 ));
792 }
793 }
794 "filter" => {
795 if grouped.is_some() {
796 let cond = self.having_cond(s)?;
799 sel.having = Some(cond);
800 } else {
801 return Err(SqlError::Unsupported(
802 "a mid-pipeline filter (put it in the row predicate)".into(),
803 ));
804 }
805 }
806 "recall" => {
807 if self.prop_s(s, "ref") != "%." {
809 return Err(SqlError::Unsupported(
810 "a register recall (Quarb-side state)".into(),
811 ));
812 }
813 let (key, agg, alias) = grouped.clone().ok_or_else(|| {
814 SqlError::Unsupported("'%.', with nothing grouped".into())
815 })?;
816 sel.select = vec![key.clone(), format!("{agg} AS {alias}")];
817 if let Some(h) = sel.having.take() {
820 sel.having = Some(h.replace("__AGG__", &agg));
821 }
822 }
823 "agg" => {
824 let name = self.prop_s(s, "name");
825 match name.as_str() {
826 "count" | "sum" | "mean" | "avg" | "min" | "max" => {
827 let col = if name == "count" {
831 pending_col = None;
832 None
833 } else {
834 pending_col.take()
835 };
836 sel.select = vec![sql_agg(&name, col)];
837 self.aggregate = true;
838 }
839 "group" => {
840 if self.strict {
841 return Err(SqlError::Unsupported(
842 "pushdown: GROUP BY result order is unordered in SQL".into(),
843 ));
844 }
845 let key = self.group_key(s, join)?;
846 sel.group_by = Some(key.clone());
847 grouped = Some((key, String::new(), String::new()));
848 }
849 "sort_by" => {
850 if self.strict {
851 return Err(SqlError::Unsupported(
852 "pushdown: ORDER BY collations differ per engine".into(),
853 ));
854 }
855 let kids = self.arbor.children(s);
856 let e = self.operand(kids[0], join.map(|(_, r)| r))?;
857 sel.order_by = Some((e, false));
858 }
859 "reverse" => match &mut sel.order_by {
860 Some((_, desc)) => *desc = true,
861 None => {
862 return Err(SqlError::Unsupported(
863 "reverse without an ORDER BY (rows are unordered)".into(),
864 ));
865 }
866 },
867 "top" => {
868 if self.strict {
869 return Err(SqlError::Unsupported(
870 "pushdown: ORDER BY collations differ per engine".into(),
871 ));
872 }
873 let kids = self.arbor.children(s);
874 let n = self.prop_s(kids[0], "value");
875 let e = self.operand(kids[1], join.map(|(_, r)| r))?;
876 sel.order_by = Some((e, true));
877 sel.limit = Some(n);
878 }
879 "unique" => {
880 if self.strict {
881 return Err(SqlError::Unsupported(
882 "pushdown: DISTINCT result order is unordered in SQL".into(),
883 ));
884 }
885 if let Some(c) = pending_col.take() {
886 sel.select = vec![c];
887 }
888 sel.distinct = true;
889 }
890 other => {
891 return Err(SqlError::Unsupported(format!("the '{other}' aggregate")));
892 }
893 }
894 }
895 "select" => {
896 if self.strict {
897 return Err(SqlError::Unsupported(
898 "pushdown: LIMIT without a guaranteed order".into(),
899 ));
900 }
901 let p = self.arbor.children(s)[0];
903 match (self.prop_s(p, "kind").as_str(), self.prop(p, "to")) {
904 ("range", Some(Value::Int(n)))
905 if self.prop(p, "from").is_none() && n > 0 =>
906 {
907 sel.limit = Some(n.to_string());
908 }
909 _ => {
910 return Err(SqlError::Unsupported(
911 "positional selection beyond '@| [..n]'".into(),
912 ));
913 }
914 }
915 }
916 other => {
917 return Err(SqlError::Unsupported(format!(
918 "the '{other}' stage (windows, subcontexts, and registers are \
919 Quarb-side state)"
920 )));
921 }
922 }
923 i += 1;
924 }
925 if let Some(c) = pending_col
927 && sel.select.is_empty()
928 {
929 sel.select = vec![c];
930 }
931 Ok(())
932 }
933
934 fn group_key(&mut self, s: NodeId, join: Option<(&str, &str)>) -> Result<String, SqlError> {
935 let kids = self.arbor.children(s);
936 let key = kids
939 .iter()
940 .rev()
941 .find(|&&k| self.kind(k) != "literal")
942 .ok_or_else(|| SqlError::Unsupported("a literal group key".into()))?;
943 self.operand(*key, join.map(|(_, r)| r))
944 }
945
946 fn having_cond(&mut self, s: NodeId) -> Result<String, SqlError> {
948 let kids = self.arbor.children(s);
949 if kids.len() != 1 || self.kind(kids[0]) != "compare" {
950 return Err(SqlError::Unsupported(
951 "HAVING translates for a single comparison".into(),
952 ));
953 }
954 let e = kids[0];
955 let op = self.prop_s(e, "op");
956 let cmp_kids = self.arbor.children(e);
957 let l = match self.kind(cmp_kids[0]).as_str() {
958 "topic" | "recall" => "__AGG__".to_string(),
959 _ => {
960 return Err(SqlError::Unsupported(
961 "HAVING compares the aggregate ($_ or its register)".into(),
962 ));
963 }
964 };
965 let r = self.operand(cmp_kids[1], None)?;
966 Ok(format!("{l} {op} {r}"))
967 }
968
969 fn record_fields(
971 &mut self,
972 s: NodeId,
973 join: Option<(&str, &str)>,
974 ) -> Result<Vec<String>, SqlError> {
975 let kids = self.arbor.children(s);
976 let mut fields = Vec::new();
977 let mut i = 0;
978 while i < kids.len() {
979 let k = kids[i];
980 if self.kind(k) == "literal" {
981 let name = self.prop_s(k, "value");
982 let value = self.operand(kids[i + 1], join.map(|(_, r)| r))?;
983 let value = match join {
984 Some((l, _)) => value.replace("__LEFT__", l),
985 None => value,
986 };
987 fields.push(format!("{value} AS {name}"));
988 i += 2;
989 } else {
990 let value = self.operand(k, join.map(|(_, r)| r))?;
991 let value = match join {
992 Some((l, _)) => value.replace("__LEFT__", l),
993 None => value,
994 };
995 fields.push(value);
996 i += 1;
997 }
998 }
999 Ok(fields)
1000 }
1001}
1002
1003fn sql_agg(name: &str, col: Option<String>) -> String {
1004 let f = match name {
1005 "count" => "COUNT",
1006 "sum" => "SUM",
1007 "mean" | "avg" => "AVG",
1008 "min" => "MIN",
1009 "max" => "MAX",
1010 _ => unreachable!("checked by caller"),
1011 };
1012 match col {
1013 Some(c) => format!("{f}({c})"),
1014 None => {
1015 if f == "COUNT" {
1016 "COUNT(*)".to_string()
1017 } else {
1018 format!("{f}(*)")
1019 }
1020 }
1021 }
1022}
1023
1024#[cfg(test)]
1025mod null_and_literal_tests {
1026 use super::{export, partial_pushdown, pushdown};
1027
1028 const GROUPED: &str = " | ::x @| group(\"g\", ::x) | count | .n | %.";
1031
1032 #[test]
1033 fn null_literal_compares_use_is_null() {
1034 assert_eq!(
1038 export("/t/*[::x = null] | ::x").unwrap().query,
1039 "SELECT x FROM t WHERE x IS NULL"
1040 );
1041 assert_eq!(
1042 export("/t/*[::x != null] | ::x").unwrap().query,
1043 "SELECT x FROM t WHERE x IS NOT NULL"
1044 );
1045 assert_eq!(
1046 pushdown("/t/*[::x = null] | ::x").unwrap().sql,
1047 "SELECT x FROM t WHERE x IS NULL"
1048 );
1049 assert_eq!(
1050 pushdown("/t/*[::x != null] | ::x").unwrap().sql,
1051 "SELECT x FROM t WHERE x IS NOT NULL"
1052 );
1053 }
1054
1055 #[test]
1056 fn ne_and_not_refuse_pushdown_but_display_diverges() {
1057 assert!(pushdown("/t/*[::x != 5] | ::x").is_none());
1061 assert!(pushdown("/t/*[!::x = 5] | ::x").is_none());
1062 assert!(partial_pushdown(&format!("/t/*[::x != 5]{GROUPED}")).is_none());
1063 assert!(partial_pushdown(&format!("/t/*[!::x = 5]{GROUPED}")).is_none());
1064 let t = export("/t/*[::x != 5] | ::x").unwrap();
1066 assert_eq!(t.query, "SELECT x FROM t WHERE x <> 5");
1067 assert!(t.notes.iter().any(|n| n.contains("NULL")));
1068 }
1069
1070 #[test]
1071 fn unescapable_text_literal_refuses_pushdown() {
1072 assert!(pushdown("/files/*[::path = \"C:\\temp\"] | ::path").is_none());
1076 assert!(pushdown("/t/*[::name = \"it's\"] | ::name").is_none());
1077 assert!(partial_pushdown(&format!("/t/*[::name = \"it's\"]{GROUPED}")).is_none());
1078 assert_eq!(
1079 pushdown("/t/*[::name = \"rare\"] | ::name").unwrap().sql,
1080 "SELECT name FROM t WHERE name = 'rare'"
1081 );
1082 }
1083}