1use crate::ast::*;
2use crate::parser::{parse, ParseError};
3use crate::plan::*;
4use powdb_storage::stored_json_path::StoredJsonPathV1;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub(crate) enum RangeTarget {
8 Column(String),
9 JsonPath(StoredJsonPathV1),
10}
11
12pub(crate) type RangeBound = (RangeTarget, Option<(Expr, bool)>, Option<(Expr, bool)>);
14
15#[derive(Debug)]
17pub enum PlanError {
18 Parse(ParseError),
20 Semantic(String),
22}
23
24impl PlanError {
25 pub fn message(&self) -> String {
27 self.to_string()
28 }
29}
30
31impl std::fmt::Display for PlanError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Self::Parse(e) => write!(f, "{e}"),
35 Self::Semantic(message) => write!(f, "{message}"),
36 }
37 }
38}
39
40impl std::error::Error for PlanError {
41 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
42 match self {
43 Self::Parse(e) => Some(e),
44 Self::Semantic(_) => None,
45 }
46 }
47}
48
49impl From<ParseError> for PlanError {
50 fn from(e: ParseError) -> Self {
51 PlanError::Parse(e)
52 }
53}
54
55pub fn plan(input: &str) -> Result<PlanNode, PlanError> {
56 let stmt = parse(input)?;
57 plan_statement(stmt)
58}
59
60pub fn plan_statement(stmt: Statement) -> Result<PlanNode, PlanError> {
61 match stmt {
62 Statement::Query(q) => plan_query(q),
63 Statement::Insert(ins) => plan_insert(ins),
64 Statement::UpdateQuery(upd) => plan_update(upd),
65 Statement::DeleteQuery(del) => plan_delete(del),
66 Statement::CreateType(ct) => plan_create_type(ct),
67 Statement::CreateLink(cl) => Ok(PlanNode::CreateLink {
68 owner: cl.owner,
69 name: cl.name,
70 target: cl.target,
71 local_key: cl.local_key,
72 target_key: cl.target_key,
73 }),
74 Statement::DropLink(dl) => Ok(PlanNode::DropLink {
75 owner: dl.owner,
76 name: dl.name,
77 if_exists: dl.if_exists,
78 }),
79 Statement::AlterTable(at) => Ok(PlanNode::AlterTable {
80 table: at.table,
81 action: at.action,
82 }),
83 Statement::DropTable(dt) => Ok(PlanNode::DropTable {
84 name: dt.table,
85 if_exists: dt.if_exists,
86 }),
87 Statement::CreateView(cv) => Ok(PlanNode::CreateView {
88 name: cv.name,
89 query_text: cv.query_text,
90 }),
91 Statement::RefreshView(rv) => Ok(PlanNode::RefreshView { name: rv.name }),
92 Statement::DropView(dv) => Ok(PlanNode::DropView {
93 name: dv.name,
94 if_exists: dv.if_exists,
95 }),
96 Statement::ListTypes => Ok(PlanNode::ListTypes),
97 Statement::Describe(table) => Ok(PlanNode::Describe { table }),
98 Statement::ListLinks => Ok(PlanNode::ListLinks),
99 Statement::Union(u) => {
100 let left = plan_statement(*u.left)?;
101 let right = plan_statement(*u.right)?;
102 Ok(PlanNode::Union {
103 left: Box::new(left),
104 right: Box::new(right),
105 all: u.all,
106 })
107 }
108 Statement::Upsert(ups) => plan_upsert(ups),
109 Statement::Begin => Ok(PlanNode::Begin),
110 Statement::Commit => Ok(PlanNode::Commit),
111 Statement::Rollback => Ok(PlanNode::Rollback),
112 Statement::Explain(inner) => {
113 let inner_plan = plan_statement(*inner)?;
114 Ok(PlanNode::Explain {
115 input: Box::new(inner_plan),
116 })
117 }
118 }
119}
120
121fn plan_query(mut q: QueryExpr) -> Result<PlanNode, PlanError> {
122 if let Some(agg) = q.aggregation.as_ref() {
127 if matches!(
128 agg.argument,
129 Some(Expr::LinkPath { .. }) | Some(Expr::NestedQuery(_))
130 ) {
131 return Err(PlanError::Semantic(
132 "aggregates over a nested or link projection are not supported: \
133 aggregate a plain column instead (e.g. `sum(Order { .total })`)"
134 .into(),
135 ));
136 }
137 }
138 if q.projection.as_ref().is_some_and(|proj| {
141 proj.iter()
142 .any(|pf| matches!(pf.expr, Expr::NestedQuery(_) | Expr::LinkPath { .. }))
143 }) {
144 return plan_nested_query(q);
145 }
146 if !q.joins.is_empty() {
152 return plan_joined_query(q);
153 }
154 let visible = q.alias.clone().unwrap_or_else(|| q.source.clone());
159 if let Some(filter) = q.filter.as_mut() {
160 resolve_scan_qualifiers(filter, &visible)?;
161 }
162 if let Some(proj) = q.projection.as_mut() {
163 for pf in proj.iter_mut() {
164 resolve_scan_qualifiers(&mut pf.expr, &visible)?;
165 }
166 }
167 if let Some(order) = q.order.as_mut() {
168 for key in order.keys.iter_mut() {
169 resolve_scan_qualifiers(&mut key.expr, &visible)?;
170 }
171 }
172 if let Some(group) = q.group_by.as_mut() {
173 for key in group.keys.iter_mut() {
174 resolve_scan_qualifiers(&mut key.expr, &visible)?;
175 }
176 if let Some(having) = group.having.as_mut() {
177 resolve_scan_qualifiers(having, &visible)?;
178 }
179 }
180 if let Some(agg) = q.aggregation.as_mut() {
181 if let Some(arg) = agg.argument.as_mut() {
182 resolve_scan_qualifiers(arg, &visible)?;
183 }
184 }
185 let source_aliases = std::collections::HashSet::from([q.source.clone()]);
186 let ordered_expr_scan = try_extract_ordered_expr_index_scan(&q);
198 let (source, filter) = if let Some(scan) = ordered_expr_scan {
199 q.order = None;
203 q.limit = None;
204 q.offset = None;
205 (scan, None)
206 } else {
207 match q.filter {
208 Some(pred) => match try_extract_eq_index_key(&q.source, &pred) {
209 Some(index_scan) => (index_scan, None),
210 None => match try_extract_range_index_keys(&q.source, &pred) {
211 Some(range_scan) => (range_scan, None),
212 None => (
213 PlanNode::SeqScan {
214 table: q.source.clone(),
215 },
216 Some(pred),
217 ),
218 },
219 },
220 None => (
221 PlanNode::SeqScan {
222 table: q.source.clone(),
223 },
224 None,
225 ),
226 }
227 };
228 let mut node = source;
229
230 if let Some(pred) = filter {
231 node = PlanNode::Filter {
232 input: Box::new(node),
233 predicate: pred,
234 };
235 }
236
237 if let Some(group) = q.group_by {
240 let mut grouped_order = q.order;
241 let mut proj_fields: Vec<ProjectField> = q
242 .projection
243 .map(|proj| {
244 proj.into_iter()
245 .map(|pf| ProjectField {
246 alias: pf.alias,
247 expr: pf.expr,
248 })
249 .collect()
250 })
251 .unwrap_or_default();
252 let mut having = group.having;
253 let aggregates = extract_aggregates(&mut proj_fields, &mut having, &source_aliases)?;
254 rewrite_group_order_keys(grouped_order.as_mut(), &proj_fields, &group.keys);
255 rewrite_group_key_references(&mut proj_fields, &mut having, &group.keys);
256
257 node = PlanNode::GroupBy {
258 input: Box::new(node),
259 keys: group.keys,
260 aggregates,
261 having,
262 };
263
264 if !proj_fields.is_empty() {
265 node = PlanNode::Project {
266 input: Box::new(node),
267 fields: proj_fields,
268 };
269 }
270
271 if q.distinct {
274 node = PlanNode::Distinct {
275 input: Box::new(node),
276 };
277 }
278
279 if let Some(order) = grouped_order {
280 node = PlanNode::Sort {
281 input: Box::new(node),
282 keys: order
283 .keys
284 .into_iter()
285 .map(|k| SortKey {
286 expr: k.expr,
287 descending: k.descending,
288 })
289 .collect(),
290 };
291 }
292 return Ok(slice_layer(node, q.offset, q.limit));
293 }
294
295 if let Some(order) = q.order {
296 node = PlanNode::Sort {
297 input: Box::new(node),
298 keys: order
299 .keys
300 .into_iter()
301 .map(|k| SortKey {
302 expr: k.expr,
303 descending: k.descending,
304 })
305 .collect(),
306 };
307 }
308
309 node = projected_tail(node, q.projection, q.distinct, q.offset, q.limit);
310
311 if let Some(agg) = q.aggregation {
312 let provenance_alias = symmetric_provenance_alias(
313 agg.function,
314 agg.argument.as_ref(),
315 agg.mode,
316 &source_aliases,
317 )?;
318 node = PlanNode::Aggregate {
319 input: Box::new(node),
320 function: agg.function,
321 argument: agg.argument,
322 mode: agg.mode,
323 provenance_alias,
324 };
325 }
326
327 Ok(node)
328}
329
330fn project_layer(input: PlanNode, projection: Vec<ProjectionField>) -> PlanNode {
334 let mut fields: Vec<ProjectField> = projection
335 .into_iter()
336 .map(|pf| ProjectField {
337 alias: pf.alias,
338 expr: pf.expr,
339 })
340 .collect();
341 let windows = extract_windows(&mut fields);
342 let input = if windows.is_empty() {
343 input
344 } else {
345 PlanNode::Window {
346 input: Box::new(input),
347 windows,
348 }
349 };
350 PlanNode::Project {
351 input: Box::new(input),
352 fields,
353 }
354}
355
356fn slice_layer(mut input: PlanNode, offset: Option<Expr>, limit: Option<Expr>) -> PlanNode {
361 if let Some(count) = offset {
362 input = PlanNode::Offset {
363 input: Box::new(input),
364 count,
365 };
366 }
367 if let Some(count) = limit {
368 input = PlanNode::Limit {
369 input: Box::new(input),
370 count,
371 };
372 }
373 input
374}
375
376fn projected_tail(
390 input: PlanNode,
391 projection: Option<Vec<ProjectionField>>,
392 distinct: bool,
393 offset: Option<Expr>,
394 limit: Option<Expr>,
395) -> PlanNode {
396 if distinct {
397 let mut node = match projection {
398 Some(projection) => project_layer(input, projection),
399 None => input,
400 };
401 node = PlanNode::Distinct {
402 input: Box::new(node),
403 };
404 slice_layer(node, offset, limit)
405 } else {
406 let node = slice_layer(input, offset, limit);
407 match projection {
408 Some(projection) => project_layer(node, projection),
409 None => node,
410 }
411 }
412}
413
414fn resolve_scan_qualifiers(expr: &mut Expr, visible: &str) -> Result<(), PlanError> {
435 match expr {
436 Expr::QualifiedField { qualifier, field } => {
437 if qualifier == visible {
438 *expr = Expr::Field(std::mem::take(field));
439 Ok(())
440 } else {
441 Err(PlanError::Semantic(format!(
442 "no such column: `{qualifier}.{field}` (the only table in this \
443 query is `{visible}`)"
444 )))
445 }
446 }
447 Expr::Field(_) | Expr::Literal(_) | Expr::Param(_) | Expr::ValueLit(_) | Expr::Null => {
448 Ok(())
449 }
450 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
451 resolve_scan_qualifiers(left, visible)?;
452 resolve_scan_qualifiers(right, visible)
453 }
454 Expr::UnaryOp(_, inner)
455 | Expr::Cast(inner, _)
456 | Expr::FunctionCall(_, inner, _)
457 | Expr::JsonPath { base: inner, .. } => resolve_scan_qualifiers(inner, visible),
458 Expr::ScalarFunc(_, args) => {
459 for arg in args.iter_mut() {
460 resolve_scan_qualifiers(arg, visible)?;
461 }
462 Ok(())
463 }
464 Expr::InList { expr, list, .. } => {
465 resolve_scan_qualifiers(expr, visible)?;
466 for item in list.iter_mut() {
467 resolve_scan_qualifiers(item, visible)?;
468 }
469 Ok(())
470 }
471 Expr::Case { whens, else_expr } => {
472 for (cond, res) in whens.iter_mut() {
473 resolve_scan_qualifiers(cond, visible)?;
474 resolve_scan_qualifiers(res, visible)?;
475 }
476 if let Some(e) = else_expr {
477 resolve_scan_qualifiers(e, visible)?;
478 }
479 Ok(())
480 }
481 Expr::Window {
482 args,
483 partition_by,
484 order_by,
485 ..
486 } => {
487 for a in args.iter_mut() {
488 resolve_scan_qualifiers(a, visible)?;
489 }
490 for p in partition_by.iter_mut() {
491 resolve_scan_qualifiers(p, visible)?;
492 }
493 for k in order_by.iter_mut() {
494 resolve_scan_qualifiers(&mut k.expr, visible)?;
495 }
496 Ok(())
497 }
498 Expr::InSubquery { .. }
502 | Expr::ExistsSubquery { .. }
503 | Expr::NestedQuery(_)
504 | Expr::LinkPath { .. } => Ok(()),
505 }
506}
507
508fn plan_nested_query(q: QueryExpr) -> Result<PlanNode, PlanError> {
515 if q.aggregation.is_some() {
516 return Err(PlanError::Semantic(
520 "aggregates over a nested or link projection are not supported: the \
521 aggregate would ignore the projection and count parent rows; \
522 aggregate the parent table directly (e.g. `count(Order)`) or run \
523 the projection without an aggregate"
524 .into(),
525 ));
526 }
527 if !q.joins.is_empty() || q.group_by.is_some() || q.distinct {
528 return Err(PlanError::Semantic(
529 "nested projections require a plain aliased table scan (no joins, \
530 group, distinct, or aggregation)"
531 .into(),
532 ));
533 }
534 let parent_alias = q.alias.unwrap_or_else(|| q.source.clone());
535 let mut node = PlanNode::AliasScan {
536 table: q.source,
537 alias: parent_alias.clone(),
538 };
539 if let Some(pred) = q.filter {
540 node = PlanNode::Filter {
541 input: Box::new(node),
542 predicate: pred,
543 };
544 }
545 if let Some(order) = q.order {
546 node = PlanNode::Sort {
547 input: Box::new(node),
548 keys: order
549 .keys
550 .into_iter()
551 .map(|k| SortKey {
552 expr: k.expr,
553 descending: k.descending,
554 })
555 .collect(),
556 };
557 }
558 if let Some(off) = q.offset {
559 node = PlanNode::Offset {
560 input: Box::new(node),
561 count: off,
562 };
563 }
564 if let Some(lim) = q.limit {
565 node = PlanNode::Limit {
566 input: Box::new(node),
567 count: lim,
568 };
569 }
570 let fields = q
571 .projection
572 .expect("plan_nested_query is only called with a projection")
573 .into_iter()
574 .map(|pf| match pf.expr {
575 Expr::NestedQuery(nested) => {
576 let name = pf.alias.ok_or_else(|| {
577 PlanError::Semantic("nested projection field requires a name".into())
578 })?;
579 resolve_nested_projection(name, *nested, &parent_alias)
580 .map(|nested| NestedProjectField::Nested(Box::new(nested)))
581 }
582 Expr::LinkPath {
583 outer_alias,
584 links,
585 column,
586 } => {
587 if outer_alias != parent_alias {
588 return Err(PlanError::Semantic(format!(
589 "link path starts at unknown alias `{outer_alias}`; \
590 the outer scan is aliased `{parent_alias}`"
591 )));
592 }
593 let name = pf.alias.unwrap_or_else(|| {
594 let mut n = outer_alias.clone();
595 for link in &links {
596 n.push('.');
597 n.push_str(link);
598 }
599 n.push('.');
600 n.push_str(&column);
601 n
602 });
603 Ok(NestedProjectField::Link(Box::new(ScalarLinkField {
604 name,
605 outer_alias,
606 links,
607 column,
608 resolved: None,
609 })))
610 }
611 expr => Ok(NestedProjectField::Plain(ProjectField {
612 alias: pf.alias,
613 expr,
614 })),
615 })
616 .collect::<Result<Vec<_>, PlanError>>()?;
617 Ok(PlanNode::NestedProject {
618 input: Box::new(node),
619 fields,
620 })
621}
622
623fn resolve_nested_projection(
629 name: String,
630 nested: NestedQuery,
631 parent_alias: &str,
632) -> Result<NestedProjection, PlanError> {
633 resolve_nested_projection_inner(name, nested, parent_alias, true)
634}
635
636fn resolve_nested_projection_inner(
640 name: String,
641 nested: NestedQuery,
642 parent_alias: &str,
643 qualify_parent_key: bool,
644) -> Result<NestedProjection, PlanError> {
645 let via_link = nested.via_link.clone();
651 let mut conjuncts = Vec::new();
652 split_and_chain(nested.filter, &mut conjuncts);
653 let mut residual: Option<Expr> = None;
654
655 let (child_key, parent_key) = if via_link.is_some() {
656 for conjunct in conjuncts {
657 if matches!(conjunct, Expr::Literal(Literal::Bool(true))) {
659 continue;
660 }
661 let rewritten =
662 rewrite_residual_condition(conjunct, &name, &nested.alias, parent_alias)?;
663 residual = Some(match residual {
664 Some(existing) => {
665 Expr::BinaryOp(Box::new(existing), BinOp::And, Box::new(rewritten))
666 }
667 None => rewritten,
668 });
669 }
670 (String::new(), String::new())
672 } else {
673 let correlation_of = |expr: &Expr| -> Option<(String, String)> {
675 let Expr::BinaryOp(left, BinOp::Eq, right) = expr else {
676 return None;
677 };
678 let side = |expr: &Expr| match expr {
679 Expr::QualifiedField { qualifier, field } => {
680 Some((qualifier.clone(), field.clone()))
681 }
682 _ => None,
683 };
684 let ((lq, lf), (rq, rf)) = (side(left)?, side(right)?);
685 if lq == nested.alias && rq == parent_alias {
686 Some((lf, rf))
687 } else if rq == nested.alias && lq == parent_alias {
688 Some((rf, lf))
689 } else {
690 None
691 }
692 };
693 let mut correlation: Option<(String, String)> = None;
694 for conjunct in conjuncts {
695 match correlation_of(&conjunct) {
696 Some(keys) if correlation.is_none() => correlation = Some(keys),
697 Some(_) => {
698 return Err(PlanError::Semantic(format!(
699 "nested projection `{name}` links `{child}` to `{parent}` more than \
700 once; exactly one correlation predicate \
701 ({child}.<col> = {parent}.<col>) is supported",
702 child = nested.alias,
703 parent = parent_alias,
704 )))
705 }
706 None => {
707 let rewritten =
708 rewrite_residual_condition(conjunct, &name, &nested.alias, parent_alias)?;
709 residual = Some(match residual {
710 Some(existing) => {
711 Expr::BinaryOp(Box::new(existing), BinOp::And, Box::new(rewritten))
712 }
713 None => rewritten,
714 });
715 }
716 }
717 }
718 let Some(correlation) = correlation else {
719 return Err(PlanError::Semantic(format!(
720 "nested projection `{name}` requires an equi-correlation predicate linking \
721 `{child}` to the outer query ({child}.<col> = {parent}.<col>) somewhere in \
722 its filter",
723 child = nested.alias,
724 parent = parent_alias,
725 )));
726 };
727 correlation
728 };
729 let order = nested
730 .order
731 .map(|clause| {
732 clause
733 .keys
734 .into_iter()
735 .map(|key| {
736 let column = match &key.expr {
737 Expr::Field(field) => field.clone(),
738 Expr::QualifiedField { qualifier, field } if *qualifier == nested.alias => {
739 field.clone()
740 }
741 _ => {
742 return Err(PlanError::Semantic(format!(
743 "nested projection `{name}` order keys must be plain \
744 columns of `{child}` (`{child}.<col>` or `.<col>`)",
745 child = nested.alias,
746 )))
747 }
748 };
749 Ok((column, key.descending))
750 })
751 .collect::<Result<Vec<_>, PlanError>>()
752 })
753 .transpose()?
754 .unwrap_or_default();
755 let fields = nested
756 .fields
757 .into_iter()
758 .map(|pf| {
759 if let Expr::NestedQuery(inner) = pf.expr {
760 let inner_name = pf.alias.ok_or_else(|| {
761 PlanError::Semantic(
762 "nested projection field requires a name \
763 (`<name>: <Table> as <alias> ...`)"
764 .into(),
765 )
766 })?;
767 return resolve_nested_projection_inner(inner_name, *inner, &nested.alias, false)
770 .map(|inner| NestedField::Nested(Box::new(inner)));
771 }
772 let column = match &pf.expr {
773 Expr::Field(field) => field.clone(),
774 Expr::QualifiedField { qualifier, field } if *qualifier == nested.alias => {
775 field.clone()
776 }
777 _ => {
778 return Err(PlanError::Semantic(format!(
779 "nested projection `{name}` fields must be plain columns of `{}` \
780 (`{}.<col>` or `.<col>`) or a deeper nested projection",
781 nested.alias, nested.alias
782 )))
783 }
784 };
785 let key = pf.alias.unwrap_or_else(|| column.clone());
786 Ok(NestedField::Scalar { key, column })
787 })
788 .collect::<Result<Vec<_>, PlanError>>()?;
789 let is_via_link = via_link.is_some();
790 Ok(NestedProjection {
791 name,
792 table: nested.source,
793 via_link,
794 alias: nested.alias,
795 parent_alias: parent_alias.to_string(),
796 child_key,
797 parent_key: if is_via_link {
801 parent_key
802 } else if qualify_parent_key {
803 format!("{parent_alias}.{parent_key}")
804 } else {
805 parent_key
806 },
807 residual,
808 order,
809 limit: nested.limit,
810 offset: nested.offset,
811 offset_before_limit: nested.offset_before_limit,
812 fields,
813 })
814}
815
816fn split_and_chain(expr: Expr, out: &mut Vec<Expr>) {
818 match expr {
819 Expr::BinaryOp(left, BinOp::And, right) => {
820 split_and_chain(*left, out);
821 split_and_chain(*right, out);
822 }
823 other => out.push(other),
824 }
825}
826
827fn rewrite_residual_condition(
833 expr: Expr,
834 name: &str,
835 child_alias: &str,
836 parent_alias: &str,
837) -> Result<Expr, PlanError> {
838 let rewrite = |inner: Box<Expr>| -> Result<Box<Expr>, PlanError> {
839 Ok(Box::new(rewrite_residual_condition(
840 *inner,
841 name,
842 child_alias,
843 parent_alias,
844 )?))
845 };
846 match expr {
847 Expr::QualifiedField { qualifier, field } => {
848 if qualifier == child_alias {
849 Ok(Expr::Field(field))
850 } else if qualifier == parent_alias {
851 Err(PlanError::Semantic(format!(
852 "nested projection `{name}` filter references outer alias \
853 `{parent_alias}` (`{parent_alias}.{field}`) outside the correlation \
854 predicate; move that condition to the outer query's filter"
855 )))
856 } else {
857 Err(PlanError::Semantic(format!(
858 "nested projection `{name}` filter references unknown alias \
859 `{qualifier}`; only columns of `{child_alias}` may be used"
860 )))
861 }
862 }
863 Expr::Field(_) | Expr::Literal(_) | Expr::Param(_) | Expr::ValueLit(_) | Expr::Null => {
864 Ok(expr)
865 }
866 Expr::BinaryOp(left, op, right) => Ok(Expr::BinaryOp(rewrite(left)?, op, rewrite(right)?)),
867 Expr::UnaryOp(op, inner) => Ok(Expr::UnaryOp(op, rewrite(inner)?)),
868 Expr::Coalesce(left, right) => Ok(Expr::Coalesce(rewrite(left)?, rewrite(right)?)),
869 Expr::Cast(inner, ty) => Ok(Expr::Cast(rewrite(inner)?, ty)),
870 Expr::ScalarFunc(func, args) => Ok(Expr::ScalarFunc(
871 func,
872 args.into_iter()
873 .map(|arg| rewrite_residual_condition(arg, name, child_alias, parent_alias))
874 .collect::<Result<Vec<_>, _>>()?,
875 )),
876 Expr::InList {
877 expr,
878 list,
879 negated,
880 } => Ok(Expr::InList {
881 expr: rewrite(expr)?,
882 list: list
883 .into_iter()
884 .map(|item| rewrite_residual_condition(item, name, child_alias, parent_alias))
885 .collect::<Result<Vec<_>, _>>()?,
886 negated,
887 }),
888 Expr::Case { whens, else_expr } => Ok(Expr::Case {
889 whens: whens
890 .into_iter()
891 .map(|(cond, result)| Ok((rewrite(cond)?, rewrite(result)?)))
892 .collect::<Result<Vec<_>, PlanError>>()?,
893 else_expr: else_expr.map(rewrite).transpose()?,
894 }),
895 Expr::JsonPath { base, segments } => Ok(Expr::JsonPath {
896 base: rewrite(base)?,
897 segments,
898 }),
899 Expr::InSubquery { .. } | Expr::ExistsSubquery { .. } => Err(PlanError::Semantic(format!(
900 "nested projection `{name}` filter cannot contain a subquery; \
901 filter the outer query or the child columns directly"
902 ))),
903 Expr::FunctionCall(..) => Err(PlanError::Semantic(format!(
904 "nested projection `{name}` filter cannot contain an aggregate function"
905 ))),
906 Expr::Window { .. } => Err(PlanError::Semantic(format!(
907 "nested projection `{name}` filter cannot contain a window function"
908 ))),
909 Expr::NestedQuery(_) => Err(PlanError::Semantic(format!(
910 "nested projection `{name}` filter cannot contain another nested projection"
911 ))),
912 Expr::LinkPath { .. } => Err(PlanError::Semantic(format!(
913 "nested projection `{name}` filter cannot contain a link traversal; \
914 link paths are only valid as projection fields"
915 ))),
916 }
917}
918
919fn plan_joined_query(mut q: QueryExpr) -> Result<PlanNode, PlanError> {
941 let primary_alias = q.alias.clone().unwrap_or_else(|| q.source.clone());
942 let mut aliases = std::collections::HashSet::new();
943 aliases.insert(primary_alias.clone());
944 let mut node = PlanNode::AliasScan {
945 table: q.source.clone(),
946 alias: primary_alias,
947 };
948
949 for join in q.joins {
950 let right_alias = join.alias.unwrap_or_else(|| join.source.clone());
951 if !aliases.insert(right_alias.clone()) {
952 return Err(ParseError::Syntax {
953 message: format!(
954 "duplicate source alias `{right_alias}` in join; every joined source needs a unique alias"
955 ),
956 position: None,
957 }
958 .into());
959 }
960 let right = PlanNode::AliasScan {
961 table: join.source,
962 alias: right_alias,
963 };
964 match join.kind {
965 JoinKind::Inner | JoinKind::LeftOuter | JoinKind::Cross => {
966 node = PlanNode::NestedLoopJoin {
967 left: Box::new(node),
968 right: Box::new(right),
969 on: join.on,
970 kind: join.kind,
971 };
972 }
973 JoinKind::RightOuter => {
974 node = PlanNode::NestedLoopJoin {
976 left: Box::new(right),
977 right: Box::new(node),
978 on: join.on,
979 kind: JoinKind::LeftOuter,
980 };
981 }
982 }
983 }
984
985 if let Some(pred) = q.filter {
986 node = PlanNode::Filter {
987 input: Box::new(node),
988 predicate: pred,
989 };
990 }
991
992 if q.group_by.is_none() {
993 if let Some(order) = q.order.take() {
994 node = PlanNode::Sort {
995 input: Box::new(node),
996 keys: order
997 .keys
998 .into_iter()
999 .map(|k| SortKey {
1000 expr: k.expr,
1001 descending: k.descending,
1002 })
1003 .collect(),
1004 };
1005 }
1006 }
1007
1008 if let Some(group) = q.group_by {
1010 let mut grouped_order = q.order;
1011 let mut proj_fields: Vec<ProjectField> = q
1012 .projection
1013 .map(|proj| {
1014 proj.into_iter()
1015 .map(|pf| ProjectField {
1016 alias: pf.alias,
1017 expr: pf.expr,
1018 })
1019 .collect()
1020 })
1021 .unwrap_or_default();
1022 let mut having = group.having;
1023 let aggregates = extract_aggregates(&mut proj_fields, &mut having, &aliases)?;
1024 rewrite_group_order_keys(grouped_order.as_mut(), &proj_fields, &group.keys);
1025 rewrite_group_key_references(&mut proj_fields, &mut having, &group.keys);
1026
1027 node = PlanNode::GroupBy {
1028 input: Box::new(node),
1029 keys: group.keys,
1030 aggregates,
1031 having,
1032 };
1033
1034 if !proj_fields.is_empty() {
1035 node = PlanNode::Project {
1036 input: Box::new(node),
1037 fields: proj_fields,
1038 };
1039 }
1040 if q.distinct {
1043 node = PlanNode::Distinct {
1044 input: Box::new(node),
1045 };
1046 }
1047 if let Some(order) = grouped_order {
1048 node = PlanNode::Sort {
1049 input: Box::new(node),
1050 keys: order
1051 .keys
1052 .into_iter()
1053 .map(|key| SortKey {
1054 expr: key.expr,
1055 descending: key.descending,
1056 })
1057 .collect(),
1058 };
1059 }
1060 return Ok(slice_layer(node, q.offset, q.limit));
1064 }
1065
1066 node = projected_tail(node, q.projection, q.distinct, q.offset, q.limit);
1067
1068 if let Some(agg) = q.aggregation {
1069 let provenance_alias =
1070 symmetric_provenance_alias(agg.function, agg.argument.as_ref(), agg.mode, &aliases)?;
1071 node = PlanNode::Aggregate {
1072 input: Box::new(node),
1073 function: agg.function,
1074 argument: agg.argument,
1075 mode: agg.mode,
1076 provenance_alias,
1077 };
1078 }
1079
1080 Ok(node)
1081}
1082
1083fn plan_insert(ins: InsertExpr) -> Result<PlanNode, PlanError> {
1084 Ok(PlanNode::Insert {
1085 table: ins.target,
1086 rows: ins.rows,
1087 returning: ins.returning,
1088 })
1089}
1090
1091fn plan_update(mut upd: UpdateExpr) -> Result<PlanNode, PlanError> {
1092 let visible = upd.alias.clone().unwrap_or_else(|| upd.source.clone());
1096 if let Some(filter) = upd.filter.as_mut() {
1097 resolve_scan_qualifiers(filter, &visible)?;
1098 }
1099 for assign in upd.assignments.iter_mut() {
1100 resolve_scan_qualifiers(&mut assign.value, &visible)?;
1101 }
1102 let source = match upd.filter {
1107 Some(pred) => match try_extract_eq_index_key(&upd.source, &pred) {
1108 Some(index_scan) => index_scan,
1109 None => match try_extract_range_index_keys(&upd.source, &pred) {
1110 Some(range_scan) => range_scan,
1111 None => PlanNode::Filter {
1112 input: Box::new(PlanNode::SeqScan {
1113 table: upd.source.clone(),
1114 }),
1115 predicate: pred,
1116 },
1117 },
1118 },
1119 None => PlanNode::SeqScan {
1120 table: upd.source.clone(),
1121 },
1122 };
1123 Ok(PlanNode::Update {
1124 input: Box::new(source),
1125 table: upd.source,
1126 assignments: upd.assignments,
1127 returning: upd.returning,
1128 })
1129}
1130
1131fn plan_delete(mut del: DeleteExpr) -> Result<PlanNode, PlanError> {
1132 let visible = del.alias.clone().unwrap_or_else(|| del.source.clone());
1136 if let Some(filter) = del.filter.as_mut() {
1137 resolve_scan_qualifiers(filter, &visible)?;
1138 }
1139 let source = match del.filter {
1140 Some(pred) => match try_extract_eq_index_key(&del.source, &pred) {
1141 Some(index_scan) => index_scan,
1142 None => match try_extract_range_index_keys(&del.source, &pred) {
1143 Some(range_scan) => range_scan,
1144 None => PlanNode::Filter {
1145 input: Box::new(PlanNode::SeqScan {
1146 table: del.source.clone(),
1147 }),
1148 predicate: pred,
1149 },
1150 },
1151 },
1152 None => PlanNode::SeqScan {
1153 table: del.source.clone(),
1154 },
1155 };
1156 Ok(PlanNode::Delete {
1157 input: Box::new(source),
1158 table: del.source,
1159 returning: del.returning,
1160 })
1161}
1162
1163fn plan_upsert(ups: UpsertExpr) -> Result<PlanNode, PlanError> {
1164 Ok(PlanNode::Upsert {
1165 table: ups.target,
1166 key_column: ups.key_column,
1167 assignments: ups.assignments,
1168 on_conflict: ups.on_conflict,
1169 })
1170}
1171
1172fn plan_create_type(ct: CreateTypeExpr) -> Result<PlanNode, PlanError> {
1173 let fields = ct
1174 .fields
1175 .into_iter()
1176 .map(|f| crate::plan::CreateField {
1177 name: f.name,
1178 type_name: f.type_name,
1179 required: f.required,
1180 unique: f.unique,
1181 default: f.default,
1182 auto: f.auto,
1183 })
1184 .collect();
1185 Ok(PlanNode::CreateTable {
1186 name: ct.name,
1187 fields,
1188 if_not_exists: ct.if_not_exists,
1189 })
1190}
1191
1192pub(crate) fn try_extract_eq_index_key(table: &str, pred: &Expr) -> Option<PlanNode> {
1202 let (lhs, op, rhs) = match pred {
1203 Expr::BinaryOp(lhs, op, rhs) => (lhs.as_ref(), *op, rhs.as_ref()),
1204 _ => return None,
1205 };
1206 if op != BinOp::Eq {
1207 return None;
1208 }
1209 match (lhs, rhs) {
1210 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some(PlanNode::ExprIndexScan {
1211 table: table.to_string(),
1212 path: stored_json_path(path)?,
1213 key: rhs.clone(),
1214 }),
1215 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some(PlanNode::ExprIndexScan {
1216 table: table.to_string(),
1217 path: stored_json_path(path)?,
1218 key: lhs.clone(),
1219 }),
1220 (Expr::Field(name), Expr::Literal(_)) => Some(PlanNode::IndexScan {
1221 table: table.to_string(),
1222 column: name.clone(),
1223 key: rhs.clone(),
1224 }),
1225 (Expr::Literal(_), Expr::Field(name)) => Some(PlanNode::IndexScan {
1226 table: table.to_string(),
1227 column: name.clone(),
1228 key: lhs.clone(),
1229 }),
1230 _ => None,
1231 }
1232}
1233
1234fn stored_json_path(expr: &Expr) -> Option<StoredJsonPathV1> {
1235 JsonPathIdentityV1::from_expr(expr)?.bind_table_local(None)
1236}
1237
1238pub(crate) fn extract_single_bound(pred: &Expr) -> Option<RangeBound> {
1241 let (lhs, op, rhs) = match pred {
1242 Expr::BinaryOp(lhs, op, rhs) => (lhs.as_ref(), *op, rhs.as_ref()),
1243 _ => return None,
1244 };
1245 match op {
1246 BinOp::Gt => match (lhs, rhs) {
1248 (Expr::Field(name), Expr::Literal(_)) => Some((
1249 RangeTarget::Column(name.clone()),
1250 Some((rhs.clone(), false)),
1251 None,
1252 )),
1253 (Expr::Literal(_), Expr::Field(name)) => {
1254 Some((
1256 RangeTarget::Column(name.clone()),
1257 None,
1258 Some((lhs.clone(), false)),
1259 ))
1260 }
1261 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1262 RangeTarget::JsonPath(stored_json_path(path)?),
1263 Some((rhs.clone(), false)),
1264 None,
1265 )),
1266 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1267 RangeTarget::JsonPath(stored_json_path(path)?),
1268 None,
1269 Some((lhs.clone(), false)),
1270 )),
1271 _ => None,
1272 },
1273 BinOp::Gte => match (lhs, rhs) {
1275 (Expr::Field(name), Expr::Literal(_)) => Some((
1276 RangeTarget::Column(name.clone()),
1277 Some((rhs.clone(), true)),
1278 None,
1279 )),
1280 (Expr::Literal(_), Expr::Field(name)) => Some((
1281 RangeTarget::Column(name.clone()),
1282 None,
1283 Some((lhs.clone(), true)),
1284 )),
1285 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1286 RangeTarget::JsonPath(stored_json_path(path)?),
1287 Some((rhs.clone(), true)),
1288 None,
1289 )),
1290 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1291 RangeTarget::JsonPath(stored_json_path(path)?),
1292 None,
1293 Some((lhs.clone(), true)),
1294 )),
1295 _ => None,
1296 },
1297 BinOp::Lt => match (lhs, rhs) {
1299 (Expr::Field(name), Expr::Literal(_)) => Some((
1300 RangeTarget::Column(name.clone()),
1301 None,
1302 Some((rhs.clone(), false)),
1303 )),
1304 (Expr::Literal(_), Expr::Field(name)) => Some((
1305 RangeTarget::Column(name.clone()),
1306 Some((lhs.clone(), false)),
1307 None,
1308 )),
1309 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1310 RangeTarget::JsonPath(stored_json_path(path)?),
1311 None,
1312 Some((rhs.clone(), false)),
1313 )),
1314 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1315 RangeTarget::JsonPath(stored_json_path(path)?),
1316 Some((lhs.clone(), false)),
1317 None,
1318 )),
1319 _ => None,
1320 },
1321 BinOp::Lte => match (lhs, rhs) {
1323 (Expr::Field(name), Expr::Literal(_)) => Some((
1324 RangeTarget::Column(name.clone()),
1325 None,
1326 Some((rhs.clone(), true)),
1327 )),
1328 (Expr::Literal(_), Expr::Field(name)) => Some((
1329 RangeTarget::Column(name.clone()),
1330 Some((lhs.clone(), true)),
1331 None,
1332 )),
1333 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1334 RangeTarget::JsonPath(stored_json_path(path)?),
1335 None,
1336 Some((rhs.clone(), true)),
1337 )),
1338 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1339 RangeTarget::JsonPath(stored_json_path(path)?),
1340 Some((lhs.clone(), true)),
1341 None,
1342 )),
1343 _ => None,
1344 },
1345 _ => None,
1346 }
1347}
1348
1349fn try_extract_range_index_keys(table: &str, pred: &Expr) -> Option<PlanNode> {
1372 if let Expr::BinaryOp(lhs, BinOp::And, rhs) = pred {
1375 if let (Some((col1, s1, e1)), Some((col2, s2, e2))) =
1376 (extract_single_bound(lhs), extract_single_bound(rhs))
1377 {
1378 if col1 == col2 {
1379 if let (Some(start), None, None, Some(end)) = (s1, e1, s2, e2) {
1380 return Some(range_scan_for_target(table, col1, Some(start), Some(end)));
1381 }
1382 }
1383 }
1384 }
1385
1386 if let Some((col, start, end)) = extract_single_bound(pred) {
1388 return Some(range_scan_for_target(table, col, start, end));
1389 }
1390
1391 None
1392}
1393
1394pub(crate) fn range_scan_for_target(
1395 table: &str,
1396 target: RangeTarget,
1397 start: Option<(Expr, bool)>,
1398 end: Option<(Expr, bool)>,
1399) -> PlanNode {
1400 match target {
1401 RangeTarget::Column(column) => PlanNode::RangeScan {
1402 table: table.to_string(),
1403 column,
1404 start,
1405 end,
1406 },
1407 RangeTarget::JsonPath(path) => PlanNode::ExprRangeScan {
1408 table: table.to_string(),
1409 path,
1410 start,
1411 end,
1412 },
1413 }
1414}
1415
1416fn try_extract_ordered_expr_index_scan(query: &QueryExpr) -> Option<PlanNode> {
1421 if query.alias.is_some()
1422 || !query.joins.is_empty()
1423 || query.filter.is_some()
1424 || query.group_by.is_some()
1425 || query.distinct
1426 || query.aggregation.is_some()
1427 || query.projection.as_ref().is_some_and(|fields| {
1428 fields
1429 .iter()
1430 .any(|field| matches!(field.expr, Expr::Window { .. }))
1431 })
1432 {
1433 return None;
1434 }
1435 let order = query.order.as_ref()?;
1436 let [key] = order.keys.as_slice() else {
1437 return None;
1438 };
1439 let path = stored_json_path(&key.expr)?;
1440 let limit = query.limit.as_ref()?;
1441 if !matches!(limit, Expr::Literal(Literal::Int(value)) if *value >= 0) {
1442 return None;
1443 }
1444 if !query
1445 .offset
1446 .as_ref()
1447 .is_none_or(|offset| matches!(offset, Expr::Literal(Literal::Int(value)) if *value >= 0))
1448 {
1449 return None;
1450 }
1451 Some(PlanNode::OrderedExprIndexScan {
1452 table: query.source.clone(),
1453 path,
1454 descending: key.descending,
1455 limit: limit.clone(),
1456 offset: query.offset.clone(),
1457 })
1458}
1459
1460fn extract_windows(proj_fields: &mut [ProjectField]) -> Vec<WindowDef> {
1465 let mut defs = Vec::new();
1466 let mut counter = 0usize;
1467 for f in proj_fields.iter_mut() {
1468 if let Expr::Window {
1469 function,
1470 args,
1471 mode,
1472 partition_by,
1473 order_by,
1474 } = &f.expr
1475 {
1476 let output_name = format!("__win_{counter}");
1477 defs.push(WindowDef {
1478 function: *function,
1479 args: args.clone(),
1480 mode: *mode,
1481 partition_by: partition_by.clone(),
1482 order_by: order_by
1483 .iter()
1484 .map(|k| SortKey {
1485 expr: k.expr.clone(),
1486 descending: k.descending,
1487 })
1488 .collect(),
1489 output_name: output_name.clone(),
1490 });
1491 f.expr = Expr::Field(output_name);
1492 counter += 1;
1493 }
1494 }
1495 defs
1496}
1497
1498fn extract_aggregates(
1504 proj_fields: &mut [ProjectField],
1505 having: &mut Option<Expr>,
1506 source_aliases: &std::collections::HashSet<String>,
1507) -> Result<Vec<GroupAgg>, PlanError> {
1508 let mut aggs: Vec<GroupAgg> = Vec::new();
1509 let mut counter = 0usize;
1510 for f in proj_fields.iter_mut() {
1511 if f.alias.is_none() && contains_aggregate(&f.expr) {
1518 f.alias = Some(projection_output_name(&f.expr));
1519 }
1520 rewrite_agg_expr(&mut f.expr, &mut aggs, &mut counter, source_aliases)?;
1521 }
1522 if let Some(h) = having {
1523 rewrite_agg_expr(h, &mut aggs, &mut counter, source_aliases)?;
1524 }
1525 Ok(aggs)
1526}
1527
1528fn rewrite_group_key_references(
1529 fields: &mut [ProjectField],
1530 having: &mut Option<Expr>,
1531 keys: &[GroupKey],
1532) {
1533 for field in fields {
1534 rewrite_group_key_expr(&mut field.expr, keys);
1535 }
1536 if let Some(having) = having {
1537 rewrite_group_key_expr(having, keys);
1538 }
1539}
1540
1541fn rewrite_group_order_keys(
1542 order: Option<&mut OrderClause>,
1543 projection: &[ProjectField],
1544 keys: &[GroupKey],
1545) {
1546 let Some(order) = order else {
1547 return;
1548 };
1549 for order_key in &mut order.keys {
1550 let Some(group_key) = keys.iter().find(|key| key.expr == order_key.expr) else {
1551 continue;
1552 };
1553 let projected_name = projection
1554 .iter()
1555 .find(|field| field.expr == group_key.expr)
1556 .and_then(|field| field.alias.clone())
1557 .unwrap_or_else(|| group_key.output_name());
1558 order_key.expr = Expr::Field(projected_name);
1559 }
1560}
1561
1562fn rewrite_group_key_expr(expr: &mut Expr, keys: &[GroupKey]) {
1563 if let Some(key) = keys.iter().find(|key| key.expr == *expr) {
1564 *expr = Expr::Field(key.output_name());
1565 return;
1566 }
1567 match expr {
1568 Expr::FunctionCall(..) => {}
1572 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1573 rewrite_group_key_expr(left, keys);
1574 rewrite_group_key_expr(right, keys);
1575 }
1576 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) => rewrite_group_key_expr(inner, keys),
1577 Expr::ScalarFunc(_, args) => {
1578 for arg in args {
1579 rewrite_group_key_expr(arg, keys);
1580 }
1581 }
1582 Expr::InList { expr, list, .. } => {
1583 rewrite_group_key_expr(expr, keys);
1584 for item in list {
1585 rewrite_group_key_expr(item, keys);
1586 }
1587 }
1588 Expr::Case { whens, else_expr } => {
1589 for (condition, result) in whens {
1590 rewrite_group_key_expr(condition, keys);
1591 rewrite_group_key_expr(result, keys);
1592 }
1593 if let Some(expr) = else_expr {
1594 rewrite_group_key_expr(expr, keys);
1595 }
1596 }
1597 _ => {}
1598 }
1599}
1600
1601fn contains_aggregate(expr: &Expr) -> bool {
1604 match expr {
1605 Expr::FunctionCall(..) => true,
1606 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1607 contains_aggregate(left) || contains_aggregate(right)
1608 }
1609 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
1610 contains_aggregate(inner)
1611 }
1612 Expr::ScalarFunc(_, args) => args.iter().any(contains_aggregate),
1613 Expr::InList { expr, list, .. } => {
1614 contains_aggregate(expr) || list.iter().any(contains_aggregate)
1615 }
1616 Expr::InSubquery { expr, .. } => contains_aggregate(expr),
1617 Expr::Case { whens, else_expr } => {
1618 whens
1619 .iter()
1620 .any(|(when, then)| contains_aggregate(when) || contains_aggregate(then))
1621 || else_expr.as_deref().is_some_and(contains_aggregate)
1622 }
1623 _ => false,
1624 }
1625}
1626
1627fn rewrite_agg_expr(
1628 expr: &mut Expr,
1629 aggs: &mut Vec<GroupAgg>,
1630 counter: &mut usize,
1631 source_aliases: &std::collections::HashSet<String>,
1632) -> Result<(), PlanError> {
1633 match expr {
1634 Expr::FunctionCall(func, inner, mode) => {
1635 let output = find_or_insert_agg(aggs, *func, inner, *mode, counter, source_aliases)?;
1636 *expr = Expr::Field(output);
1637 }
1638 Expr::BinaryOp(l, _, r) => {
1639 rewrite_agg_expr(l, aggs, counter, source_aliases)?;
1640 rewrite_agg_expr(r, aggs, counter, source_aliases)?;
1641 }
1642 Expr::UnaryOp(_, inner) => rewrite_agg_expr(inner, aggs, counter, source_aliases)?,
1643 Expr::Coalesce(l, r) => {
1644 rewrite_agg_expr(l, aggs, counter, source_aliases)?;
1645 rewrite_agg_expr(r, aggs, counter, source_aliases)?;
1646 }
1647 Expr::InList { expr: e, list, .. } => {
1648 rewrite_agg_expr(e, aggs, counter, source_aliases)?;
1649 for item in list {
1650 rewrite_agg_expr(item, aggs, counter, source_aliases)?;
1651 }
1652 }
1653 Expr::InSubquery { expr: e, .. } => {
1654 rewrite_agg_expr(e, aggs, counter, source_aliases)?;
1655 }
1656 _ => {}
1657 }
1658 Ok(())
1659}
1660
1661fn find_or_insert_agg(
1662 aggs: &mut Vec<GroupAgg>,
1663 func: AggFunc,
1664 argument: &Expr,
1665 mode: AggregateMode,
1666 counter: &mut usize,
1667 source_aliases: &std::collections::HashSet<String>,
1668) -> Result<String, PlanError> {
1669 for existing in aggs.iter() {
1670 if existing.function == func && existing.argument == *argument && existing.mode == mode {
1671 return Ok(existing.output_name.clone());
1672 }
1673 }
1674 let provenance_alias = symmetric_provenance_alias(func, Some(argument), mode, source_aliases)?;
1675 let output_name = format!("__agg_{counter}");
1676 aggs.push(GroupAgg {
1677 function: func,
1678 argument: argument.clone(),
1679 mode,
1680 provenance_alias,
1681 output_name: output_name.clone(),
1682 });
1683 *counter += 1;
1684 Ok(output_name)
1685}
1686
1687fn symmetric_provenance_alias(
1688 function: AggFunc,
1689 argument: Option<&Expr>,
1690 mode: AggregateMode,
1691 source_aliases: &std::collections::HashSet<String>,
1692) -> Result<Option<String>, PlanError> {
1693 if mode == AggregateMode::Raw
1694 || source_aliases.len() < 2
1695 || !matches!(function, AggFunc::Sum | AggFunc::Avg | AggFunc::Count)
1696 || (function == AggFunc::Count
1697 && argument.is_none_or(|argument| matches!(argument, Expr::Field(name) if name == "*")))
1698 {
1699 return Ok(None);
1700 }
1701 let Some(argument) = argument else {
1702 return Err(symmetric_aggregate_error(
1703 function,
1704 "does not reference a source row",
1705 ));
1706 };
1707
1708 let mut qualified = std::collections::HashSet::new();
1709 let mut has_unqualified = false;
1710 collect_expression_sources(argument, &mut qualified, &mut has_unqualified);
1711
1712 for alias in &qualified {
1713 if !source_aliases.contains(alias) {
1714 return Err(symmetric_aggregate_error(
1715 function,
1716 &format!("references unknown source alias '{alias}'"),
1717 ));
1718 }
1719 }
1720 if has_unqualified {
1721 if source_aliases.len() != 1 {
1722 return Err(symmetric_aggregate_error(
1723 function,
1724 "contains an ambiguous unqualified field",
1725 ));
1726 }
1727 qualified.extend(source_aliases.iter().cloned());
1728 }
1729 match qualified.len() {
1730 1 => Ok(qualified.into_iter().next()),
1731 0 => Err(symmetric_aggregate_error(
1732 function,
1733 "does not reference a source row",
1734 )),
1735 _ => Err(symmetric_aggregate_error(
1736 function,
1737 "references multiple source aliases",
1738 )),
1739 }
1740}
1741
1742fn symmetric_aggregate_error(function: AggFunc, reason: &str) -> PlanError {
1743 let name = format!("{function:?}").to_lowercase();
1744 PlanError::Semantic(format!(
1745 "symmetric {name} expression {reason}; reference exactly one source alias or use {name}(raw ...)"
1746 ))
1747}
1748
1749fn collect_expression_sources(
1750 expr: &Expr,
1751 qualified: &mut std::collections::HashSet<String>,
1752 has_unqualified: &mut bool,
1753) {
1754 match expr {
1755 Expr::Field(name) if name != "*" => *has_unqualified = true,
1756 Expr::QualifiedField { qualifier, .. } => {
1757 qualified.insert(qualifier.clone());
1758 }
1759 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1760 collect_expression_sources(left, qualified, has_unqualified);
1761 collect_expression_sources(right, qualified, has_unqualified);
1762 }
1763 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
1764 collect_expression_sources(inner, qualified, has_unqualified);
1765 }
1766 Expr::ScalarFunc(_, args) => {
1767 for argument in args {
1768 collect_expression_sources(argument, qualified, has_unqualified);
1769 }
1770 }
1771 Expr::InList { expr, list, .. } => {
1772 collect_expression_sources(expr, qualified, has_unqualified);
1773 for item in list {
1774 collect_expression_sources(item, qualified, has_unqualified);
1775 }
1776 }
1777 Expr::InSubquery { expr, .. } => {
1778 collect_expression_sources(expr, qualified, has_unqualified);
1779 }
1780 Expr::Case { whens, else_expr } => {
1781 for (condition, result) in whens {
1782 collect_expression_sources(condition, qualified, has_unqualified);
1783 collect_expression_sources(result, qualified, has_unqualified);
1784 }
1785 if let Some(expr) = else_expr {
1786 collect_expression_sources(expr, qualified, has_unqualified);
1787 }
1788 }
1789 Expr::Window {
1790 args,
1791 partition_by,
1792 order_by,
1793 ..
1794 } => {
1795 for expr in args.iter().chain(partition_by) {
1796 collect_expression_sources(expr, qualified, has_unqualified);
1797 }
1798 for key in order_by {
1799 collect_expression_sources(&key.expr, qualified, has_unqualified);
1800 }
1801 }
1802 Expr::FunctionCall(_, inner, _) => {
1803 collect_expression_sources(inner, qualified, has_unqualified);
1804 }
1805 Expr::LinkPath { outer_alias, .. } => {
1807 qualified.insert(outer_alias.clone());
1808 }
1809 Expr::ExistsSubquery { .. }
1810 | Expr::Field(_)
1811 | Expr::Literal(_)
1812 | Expr::Param(_)
1813 | Expr::ValueLit(_)
1814 | Expr::Null
1815 | Expr::NestedQuery(_) => {}
1816 }
1817}
1818
1819#[cfg(test)]
1820mod tests {
1821 use super::*;
1822 use crate::plan::PlanNode;
1823
1824 #[test]
1825 fn test_plan_simple_scan() {
1826 let plan = plan("User").unwrap();
1827 assert!(matches!(plan, PlanNode::SeqScan { table } if table == "User"));
1828 }
1829
1830 #[test]
1831 fn test_plan_filter() {
1832 let plan = plan("User filter .age > 30").unwrap();
1833 assert!(matches!(plan, PlanNode::RangeScan { .. }));
1834 }
1835
1836 #[test]
1837 fn test_plan_filter_with_projection() {
1838 let plan = plan("User filter .age > 30 { name, email }").unwrap();
1839 assert!(matches!(plan, PlanNode::Project { .. }));
1840 }
1841
1842 #[test]
1843 fn test_plan_insert() {
1844 let plan = plan(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
1845 assert!(matches!(plan, PlanNode::Insert { .. }));
1846 }
1847
1848 #[test]
1849 fn test_plan_order_limit() {
1850 let plan = plan("User order .name limit 10").unwrap();
1851 match plan {
1852 PlanNode::Limit { input, .. } => {
1853 assert!(matches!(*input, PlanNode::Sort { .. }));
1854 }
1855 _ => panic!("expected Limit(Sort(SeqScan))"),
1856 }
1857 }
1858
1859 #[test]
1860 fn test_plan_count() {
1861 let plan = plan("count(User)").unwrap();
1862 assert!(matches!(plan, PlanNode::Aggregate { .. }));
1863 }
1864
1865 #[test]
1866 fn single_source_aggregates_do_not_request_provenance() {
1867 for query in [
1868 "sum(User { .amount })",
1869 "avg(User { .amount })",
1870 "count(User { .amount })",
1871 ] {
1872 match plan(query).unwrap() {
1873 PlanNode::Aggregate {
1874 provenance_alias, ..
1875 } => assert!(
1876 provenance_alias.is_none(),
1877 "unexpected provenance for {query}"
1878 ),
1879 other => panic!("expected Aggregate for {query}, got {other:?}"),
1880 }
1881 }
1882
1883 match plan("User group .dept { total: sum(.amount) }").unwrap() {
1884 PlanNode::Project { input, .. } => match *input {
1885 PlanNode::GroupBy { aggregates, .. } => {
1886 assert!(aggregates[0].provenance_alias.is_none());
1887 }
1888 other => panic!("expected GroupBy, got {other:?}"),
1889 },
1890 other => panic!("expected Project(GroupBy), got {other:?}"),
1891 }
1892 }
1893
1894 #[test]
1895 fn join_provenance_is_limited_to_fanout_sensitive_aggregates() {
1896 let base = "Account as a join Entry as e on a.id = e.account_id group a.dept";
1897 for (function, expects_provenance) in [
1898 ("sum(a.balance)", true),
1899 ("avg(a.balance)", true),
1900 ("count(a.balance)", true),
1901 ("min(a.balance)", false),
1902 ("max(a.balance)", false),
1903 ("count(distinct a.balance)", false),
1904 ("count(*)", false),
1905 ] {
1906 let query = format!("{base} {{ value: {function} }}");
1907 match plan(&query).unwrap() {
1908 PlanNode::Project { input, .. } => match *input {
1909 PlanNode::GroupBy { aggregates, .. } => assert_eq!(
1910 aggregates[0].provenance_alias.as_deref(),
1911 expects_provenance.then_some("a"),
1912 "unexpected provenance selection for {function}"
1913 ),
1914 other => panic!("expected GroupBy for {function}, got {other:?}"),
1915 },
1916 other => panic!("expected Project(GroupBy) for {function}, got {other:?}"),
1917 }
1918 }
1919 }
1920
1921 #[test]
1922 fn test_plan_eq_becomes_index_scan() {
1923 let plan = plan("User filter .id = 42").unwrap();
1926 match plan {
1927 PlanNode::IndexScan { table, column, key } => {
1928 assert_eq!(table, "User");
1929 assert_eq!(column, "id");
1930 assert!(matches!(key, Expr::Literal(Literal::Int(42))));
1931 }
1932 other => panic!("expected IndexScan, got {other:?}"),
1933 }
1934 }
1935
1936 #[test]
1937 fn test_plan_eq_reversed_becomes_index_scan() {
1938 let plan = plan(r#"User filter "NYC" = .city"#).unwrap();
1940 assert!(matches!(plan, PlanNode::IndexScan { .. }));
1941 }
1942
1943 #[test]
1944 fn json_path_equality_and_reversed_equality_are_speculative_expression_scans() {
1945 for query in ["Post filter .data->age = 21", "Post filter 21 = .data->age"] {
1946 match plan(query).unwrap() {
1947 PlanNode::ExprIndexScan { table, path, key } => {
1948 assert_eq!(table, "Post");
1949 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1950 assert!(matches!(key, Expr::Literal(Literal::Int(21))));
1951 }
1952 other => panic!("expected ExprIndexScan for `{query}`, got {other:?}"),
1953 }
1954 }
1955 }
1956
1957 #[test]
1958 fn json_path_range_and_same_path_compound_bounds_are_speculative_scans() {
1959 for query in ["Post filter .data->age > 18", "Post filter 18 < .data->age"] {
1960 match plan(query).unwrap() {
1961 PlanNode::ExprRangeScan {
1962 path, start, end, ..
1963 } => {
1964 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1965 assert!(start.is_some());
1966 assert!(end.is_none());
1967 }
1968 other => panic!("expected ExprRangeScan for `{query}`, got {other:?}"),
1969 }
1970 }
1971
1972 match plan("Post filter .data->age >= 18 and .data->age < 65").unwrap() {
1973 PlanNode::ExprRangeScan {
1974 path, start, end, ..
1975 } => {
1976 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1977 assert_eq!(start, Some((Expr::Literal(Literal::Int(18)), true)));
1978 assert_eq!(end, Some((Expr::Literal(Literal::Int(65)), false)));
1979 }
1980 other => panic!("expected bounded ExprRangeScan, got {other:?}"),
1981 }
1982
1983 assert!(matches!(
1984 plan("Post filter .data->age >= 18 and .data->score < 65").unwrap(),
1985 PlanNode::Filter { .. }
1986 ));
1987 }
1988
1989 #[test]
1990 fn exact_single_path_order_limit_uses_ordered_expression_scan() {
1991 match plan("Post order .data->age desc limit 10 offset 2 { .id }").unwrap() {
1992 PlanNode::Project { input, .. } => match *input {
1993 PlanNode::OrderedExprIndexScan {
1994 table,
1995 path,
1996 descending,
1997 limit,
1998 offset,
1999 } => {
2000 assert_eq!(table, "Post");
2001 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
2002 assert!(descending);
2003 assert_eq!(limit, Expr::Literal(Literal::Int(10)));
2004 assert_eq!(offset, Some(Expr::Literal(Literal::Int(2))));
2005 }
2006 other => panic!("expected OrderedExprIndexScan, got {other:?}"),
2007 },
2008 other => panic!("expected Project(OrderedExprIndexScan), got {other:?}"),
2009 }
2010 }
2011
2012 #[test]
2013 fn incompatible_path_order_shapes_keep_generic_sort() {
2014 for query in [
2015 "Post order .data->age",
2016 "Post order .data->age, .id limit 10",
2017 "Post filter .data->active = true order .data->age limit 10",
2018 "Post order .data->age limit .id",
2019 ] {
2020 let planned = plan(query).unwrap();
2021 assert!(
2022 !plan_contains_ordered_expr_scan(&planned),
2023 "`{query}` must remain on the generic pipeline: {planned:?}"
2024 );
2025 }
2026 }
2027
2028 fn plan_contains_ordered_expr_scan(plan: &PlanNode) -> bool {
2029 match plan {
2030 PlanNode::OrderedExprIndexScan { .. } => true,
2031 PlanNode::Filter { input, .. }
2032 | PlanNode::Project { input, .. }
2033 | PlanNode::Sort { input, .. }
2034 | PlanNode::Limit { input, .. }
2035 | PlanNode::Offset { input, .. }
2036 | PlanNode::Aggregate { input, .. }
2037 | PlanNode::Distinct { input }
2038 | PlanNode::GroupBy { input, .. }
2039 | PlanNode::Update { input, .. }
2040 | PlanNode::Delete { input, .. }
2041 | PlanNode::Window { input, .. }
2042 | PlanNode::Explain { input } => plan_contains_ordered_expr_scan(input),
2043 PlanNode::NestedLoopJoin { left, right, .. } | PlanNode::Union { left, right, .. } => {
2044 plan_contains_ordered_expr_scan(left) || plan_contains_ordered_expr_scan(right)
2045 }
2046 _ => false,
2047 }
2048 }
2049
2050 #[test]
2051 fn test_plan_non_eq_stays_filter() {
2052 let plan = plan("User filter .age > 30").unwrap();
2054 match plan {
2055 PlanNode::RangeScan {
2056 column, start, end, ..
2057 } => {
2058 assert_eq!(column, "age");
2059 assert!(start.is_some(), "expected lower bound");
2060 assert!(end.is_none(), "expected no upper bound");
2061 let (_, inclusive) = start.unwrap();
2062 assert!(!inclusive, "expected exclusive lower bound for >");
2063 }
2064 other => panic!("expected RangeScan, got {other:?}"),
2065 }
2066 }
2067
2068 #[test]
2069 fn test_plan_index_scan_with_projection() {
2070 let plan = plan("User filter .id = 1 { .name }").unwrap();
2072 match plan {
2073 PlanNode::Project { input, .. } => {
2074 assert!(matches!(*input, PlanNode::IndexScan { .. }));
2075 }
2076 other => panic!("expected Project(IndexScan), got {other:?}"),
2077 }
2078 }
2079
2080 #[test]
2081 fn test_plan_update_by_pk_becomes_index_scan() {
2082 let plan = plan("User filter .id = 42 update { age := 31 }").unwrap();
2085 match plan {
2086 PlanNode::Update { input, .. } => {
2087 assert!(
2088 matches!(*input, PlanNode::IndexScan { .. }),
2089 "expected Update(IndexScan), got {input:?}"
2090 );
2091 }
2092 other => panic!("expected Update, got {other:?}"),
2093 }
2094 }
2095
2096 #[test]
2097 fn test_plan_update_range_stays_range_scan() {
2098 let plan = plan("User filter .age > 30 update { age := 31 }").unwrap();
2099 match plan {
2100 PlanNode::Update { input, .. } => {
2101 assert!(
2102 matches!(*input, PlanNode::RangeScan { .. }),
2103 "expected Update(RangeScan), got {input:?}"
2104 );
2105 }
2106 other => panic!("expected Update, got {other:?}"),
2107 }
2108 }
2109
2110 #[test]
2111 fn test_plan_delete_by_pk_becomes_index_scan() {
2112 let plan = plan("User filter .id = 7 delete").unwrap();
2113 match plan {
2114 PlanNode::Delete { input, .. } => {
2115 assert!(matches!(*input, PlanNode::IndexScan { .. }));
2116 }
2117 other => panic!("expected Delete, got {other:?}"),
2118 }
2119 }
2120
2121 #[test]
2122 fn test_plan_inner_join_builds_nested_loop() {
2123 let plan = plan("User as u join Order as o on u.id = o.user_id").unwrap();
2126 match plan {
2127 PlanNode::NestedLoopJoin {
2128 left,
2129 right,
2130 on,
2131 kind,
2132 } => {
2133 assert_eq!(kind, JoinKind::Inner);
2134 assert!(on.is_some());
2135 assert!(matches!(*left, PlanNode::AliasScan { .. }));
2136 assert!(matches!(*right, PlanNode::AliasScan { .. }));
2137 }
2138 other => panic!("expected NestedLoopJoin, got {other:?}"),
2139 }
2140 }
2141
2142 #[test]
2143 fn duplicate_join_aliases_are_rejected_before_execution() {
2144 let err = plan("A as x join A as x on x.id = x.id").unwrap_err();
2145 assert!(
2146 err.to_string().contains("duplicate source alias `x`"),
2147 "unexpected error: {err}"
2148 );
2149 }
2150
2151 #[test]
2152 fn test_plan_right_join_rewritten_as_left_with_swapped_inputs() {
2153 let plan = plan("User as u right join Order as o on u.id = o.user_id").unwrap();
2154 match plan {
2155 PlanNode::NestedLoopJoin {
2156 left, right, kind, ..
2157 } => {
2158 assert_eq!(kind, JoinKind::LeftOuter);
2159 match *left {
2161 PlanNode::AliasScan { table, .. } => assert_eq!(table, "Order"),
2162 other => panic!("expected AliasScan(Order), got {other:?}"),
2163 }
2164 match *right {
2165 PlanNode::AliasScan { table, .. } => assert_eq!(table, "User"),
2166 other => panic!("expected AliasScan(User), got {other:?}"),
2167 }
2168 }
2169 other => panic!("expected NestedLoopJoin, got {other:?}"),
2170 }
2171 }
2172
2173 #[test]
2174 fn test_plan_multi_join_is_left_deep() {
2175 let plan = plan(
2177 "User as u join Order as o on u.id = o.user_id \
2178 join Product as p on o.product_id = p.id",
2179 )
2180 .unwrap();
2181 match plan {
2182 PlanNode::NestedLoopJoin { left, right, .. } => {
2183 match *right {
2185 PlanNode::AliasScan { table, .. } => assert_eq!(table, "Product"),
2186 other => panic!("expected AliasScan(Product), got {other:?}"),
2187 }
2188 assert!(matches!(*left, PlanNode::NestedLoopJoin { .. }));
2190 }
2191 other => panic!("expected NestedLoopJoin, got {other:?}"),
2192 }
2193 }
2194
2195 #[test]
2196 fn test_plan_join_with_filter_tail_wraps_filter_on_top() {
2197 let plan =
2198 plan("User as u join Order as o on u.id = o.user_id filter o.total > 100").unwrap();
2199 match plan {
2200 PlanNode::Filter { input, .. } => {
2201 assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
2202 }
2203 other => panic!("expected Filter(NestedLoopJoin), got {other:?}"),
2204 }
2205 }
2206
2207 #[test]
2208 fn test_plan_group_by_builds_groupby_node() {
2209 let plan = plan("User group .status { .status, n: count(.name) }").unwrap();
2210 match plan {
2212 PlanNode::Project { input, fields } => {
2213 assert_eq!(fields.len(), 2);
2214 match *input {
2215 PlanNode::GroupBy {
2216 input: inner,
2217 keys,
2218 aggregates,
2219 having,
2220 } => {
2221 assert!(matches!(*inner, PlanNode::SeqScan { .. }));
2222 assert_eq!(
2223 keys,
2224 vec![GroupKey {
2225 expr: Expr::Field("status".into()),
2226 output_name: "status".into(),
2227 }]
2228 );
2229 assert_eq!(aggregates.len(), 1);
2230 assert_eq!(aggregates[0].function, AggFunc::Count);
2231 assert_eq!(aggregates[0].argument, Expr::Field("name".into()));
2232 assert!(having.is_none());
2233 }
2234 other => panic!("expected GroupBy, got {other:?}"),
2235 }
2236 }
2237 other => panic!("expected Project, got {other:?}"),
2238 }
2239 }
2240
2241 #[test]
2242 fn test_plan_joined_group_applies_order_offset_limit_after_grouping() {
2243 let plan = plan(
2244 "User as u join Order as o on u.id = o.user_id \
2245 group u.status { u.status, n: count(*) } order n desc offset 1 limit 2",
2246 )
2247 .unwrap();
2248
2249 let PlanNode::Limit { input, .. } = plan else {
2250 panic!("expected Limit at the grouped-result boundary");
2251 };
2252 let PlanNode::Offset { input, .. } = *input else {
2253 panic!("expected Offset below Limit");
2254 };
2255 let PlanNode::Sort { input, .. } = *input else {
2256 panic!("expected Sort below Offset");
2257 };
2258 let PlanNode::Project { input, .. } = *input else {
2259 panic!("expected Project below Sort");
2260 };
2261 let PlanNode::GroupBy { input, .. } = *input else {
2262 panic!("expected GroupBy below Project");
2263 };
2264 assert!(
2265 matches!(*input, PlanNode::NestedLoopJoin { .. }),
2266 "joined rows must flow into GroupBy before result limiting"
2267 );
2268 }
2269
2270 #[test]
2271 fn test_plan_group_by_having_rewrites_agg_in_having() {
2272 let plan = plan("User group .status having count(.name) > 1 { .status }").unwrap();
2273 match plan {
2274 PlanNode::Project { input, .. } => {
2275 match *input {
2276 PlanNode::GroupBy {
2277 having, aggregates, ..
2278 } => {
2279 assert_eq!(aggregates.len(), 1);
2282 assert_eq!(aggregates[0].output_name, "__agg_0");
2283 let h = having.expect("having should be Some");
2284 match h {
2285 Expr::BinaryOp(l, BinOp::Gt, _) => {
2286 assert!(
2287 matches!(*l, Expr::Field(ref name) if name == "__agg_0"),
2288 "expected Field(__agg_0), got {l:?}"
2289 );
2290 }
2291 other => panic!("expected BinaryOp, got {other:?}"),
2292 }
2293 }
2294 other => panic!("expected GroupBy, got {other:?}"),
2295 }
2296 }
2297 other => panic!("expected Project, got {other:?}"),
2298 }
2299 }
2300
2301 #[test]
2302 fn test_plan_window_inserts_window_node_before_project() {
2303 let plan = plan("User { .name, rn: row_number() over (order .age) }").unwrap();
2304 match plan {
2306 PlanNode::Project { input, fields } => {
2307 assert_eq!(fields.len(), 2);
2308 assert!(
2310 matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"),
2311 "expected Field(__win_0), got {:?}",
2312 fields[1].expr
2313 );
2314 match *input {
2315 PlanNode::Window {
2316 input: inner,
2317 windows,
2318 } => {
2319 assert_eq!(windows.len(), 1);
2320 assert_eq!(windows[0].output_name, "__win_0");
2321 assert!(matches!(*inner, PlanNode::SeqScan { .. }));
2322 }
2323 other => panic!("expected Window, got {other:?}"),
2324 }
2325 }
2326 other => panic!("expected Project, got {other:?}"),
2327 }
2328 }
2329
2330 #[test]
2331 fn test_plan_multiple_windows() {
2332 let plan = plan(
2333 "User { .name, rn: row_number() over (order .age), s: sum(.salary) over (partition .dept order .salary) }"
2334 ).unwrap();
2335 match plan {
2336 PlanNode::Project { input, fields } => {
2337 assert_eq!(fields.len(), 3);
2338 assert!(matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"));
2339 assert!(matches!(&fields[2].expr, Expr::Field(name) if name == "__win_1"));
2340 match *input {
2341 PlanNode::Window { windows, .. } => {
2342 assert_eq!(windows.len(), 2);
2343 assert_eq!(windows[0].output_name, "__win_0");
2344 assert_eq!(windows[1].output_name, "__win_1");
2345 }
2346 other => panic!("expected Window, got {other:?}"),
2347 }
2348 }
2349 other => panic!("expected Project, got {other:?}"),
2350 }
2351 }
2352
2353 #[test]
2354 fn test_plan_no_window_without_over() {
2355 let plan = plan("User group .dept { .dept, total: sum(.salary) }").unwrap();
2357 match plan {
2358 PlanNode::Project { input, .. } => {
2359 assert!(
2361 matches!(*input, PlanNode::GroupBy { .. }),
2362 "expected GroupBy under Project, got {:?}",
2363 input
2364 );
2365 }
2366 other => panic!("expected Project, got {other:?}"),
2367 }
2368 }
2369
2370 #[test]
2371 fn test_plan_explain_wraps_inner() {
2372 let plan = plan("explain User filter .age > 30").unwrap();
2373 match plan {
2374 PlanNode::Explain { input } => {
2375 assert!(
2376 matches!(*input, PlanNode::RangeScan { .. }),
2377 "expected Explain(RangeScan), got {:?}",
2378 input
2379 );
2380 }
2381 other => panic!("expected Explain, got {other:?}"),
2382 }
2383 }
2384
2385 #[test]
2386 fn test_plan_explain_simple_scan() {
2387 let plan = plan("explain User").unwrap();
2388 match plan {
2389 PlanNode::Explain { input } => {
2390 assert!(matches!(*input, PlanNode::SeqScan { .. }));
2391 }
2392 other => panic!("expected Explain(SeqScan), got {other:?}"),
2393 }
2394 }
2395
2396 #[test]
2397 fn test_plan_explain_join() {
2398 let plan = plan("explain User as u join Order as o on u.id = o.user_id").unwrap();
2399 match plan {
2400 PlanNode::Explain { input } => {
2401 assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
2402 }
2403 other => panic!("expected Explain(NestedLoopJoin), got {other:?}"),
2404 }
2405 }
2406}