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