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 }
952 .into());
953 }
954 let right = PlanNode::AliasScan {
955 table: join.source,
956 alias: right_alias,
957 };
958 match join.kind {
959 JoinKind::Inner | JoinKind::LeftOuter | JoinKind::Cross => {
960 node = PlanNode::NestedLoopJoin {
961 left: Box::new(node),
962 right: Box::new(right),
963 on: join.on,
964 kind: join.kind,
965 };
966 }
967 JoinKind::RightOuter => {
968 node = PlanNode::NestedLoopJoin {
970 left: Box::new(right),
971 right: Box::new(node),
972 on: join.on,
973 kind: JoinKind::LeftOuter,
974 };
975 }
976 }
977 }
978
979 if let Some(pred) = q.filter {
980 node = PlanNode::Filter {
981 input: Box::new(node),
982 predicate: pred,
983 };
984 }
985
986 if q.group_by.is_none() {
987 if let Some(order) = q.order.take() {
988 node = PlanNode::Sort {
989 input: Box::new(node),
990 keys: order
991 .keys
992 .into_iter()
993 .map(|k| SortKey {
994 expr: k.expr,
995 descending: k.descending,
996 })
997 .collect(),
998 };
999 }
1000 }
1001
1002 if let Some(group) = q.group_by {
1004 let mut grouped_order = q.order;
1005 let mut proj_fields: Vec<ProjectField> = q
1006 .projection
1007 .map(|proj| {
1008 proj.into_iter()
1009 .map(|pf| ProjectField {
1010 alias: pf.alias,
1011 expr: pf.expr,
1012 })
1013 .collect()
1014 })
1015 .unwrap_or_default();
1016 let mut having = group.having;
1017 let aggregates = extract_aggregates(&mut proj_fields, &mut having, &aliases)?;
1018 rewrite_group_order_keys(grouped_order.as_mut(), &proj_fields, &group.keys);
1019 rewrite_group_key_references(&mut proj_fields, &mut having, &group.keys);
1020
1021 node = PlanNode::GroupBy {
1022 input: Box::new(node),
1023 keys: group.keys,
1024 aggregates,
1025 having,
1026 };
1027
1028 if !proj_fields.is_empty() {
1029 node = PlanNode::Project {
1030 input: Box::new(node),
1031 fields: proj_fields,
1032 };
1033 }
1034 if q.distinct {
1037 node = PlanNode::Distinct {
1038 input: Box::new(node),
1039 };
1040 }
1041 if let Some(order) = grouped_order {
1042 node = PlanNode::Sort {
1043 input: Box::new(node),
1044 keys: order
1045 .keys
1046 .into_iter()
1047 .map(|key| SortKey {
1048 expr: key.expr,
1049 descending: key.descending,
1050 })
1051 .collect(),
1052 };
1053 }
1054 return Ok(slice_layer(node, q.offset, q.limit));
1058 }
1059
1060 node = projected_tail(node, q.projection, q.distinct, q.offset, q.limit);
1061
1062 if let Some(agg) = q.aggregation {
1063 let provenance_alias =
1064 symmetric_provenance_alias(agg.function, agg.argument.as_ref(), agg.mode, &aliases)?;
1065 node = PlanNode::Aggregate {
1066 input: Box::new(node),
1067 function: agg.function,
1068 argument: agg.argument,
1069 mode: agg.mode,
1070 provenance_alias,
1071 };
1072 }
1073
1074 Ok(node)
1075}
1076
1077fn plan_insert(ins: InsertExpr) -> Result<PlanNode, PlanError> {
1078 Ok(PlanNode::Insert {
1079 table: ins.target,
1080 rows: ins.rows,
1081 returning: ins.returning,
1082 })
1083}
1084
1085fn plan_update(mut upd: UpdateExpr) -> Result<PlanNode, PlanError> {
1086 let visible = upd.alias.clone().unwrap_or_else(|| upd.source.clone());
1090 if let Some(filter) = upd.filter.as_mut() {
1091 resolve_scan_qualifiers(filter, &visible)?;
1092 }
1093 for assign in upd.assignments.iter_mut() {
1094 resolve_scan_qualifiers(&mut assign.value, &visible)?;
1095 }
1096 let source = match upd.filter {
1101 Some(pred) => match try_extract_eq_index_key(&upd.source, &pred) {
1102 Some(index_scan) => index_scan,
1103 None => match try_extract_range_index_keys(&upd.source, &pred) {
1104 Some(range_scan) => range_scan,
1105 None => PlanNode::Filter {
1106 input: Box::new(PlanNode::SeqScan {
1107 table: upd.source.clone(),
1108 }),
1109 predicate: pred,
1110 },
1111 },
1112 },
1113 None => PlanNode::SeqScan {
1114 table: upd.source.clone(),
1115 },
1116 };
1117 Ok(PlanNode::Update {
1118 input: Box::new(source),
1119 table: upd.source,
1120 assignments: upd.assignments,
1121 returning: upd.returning,
1122 })
1123}
1124
1125fn plan_delete(mut del: DeleteExpr) -> Result<PlanNode, PlanError> {
1126 let visible = del.alias.clone().unwrap_or_else(|| del.source.clone());
1130 if let Some(filter) = del.filter.as_mut() {
1131 resolve_scan_qualifiers(filter, &visible)?;
1132 }
1133 let source = match del.filter {
1134 Some(pred) => match try_extract_eq_index_key(&del.source, &pred) {
1135 Some(index_scan) => index_scan,
1136 None => match try_extract_range_index_keys(&del.source, &pred) {
1137 Some(range_scan) => range_scan,
1138 None => PlanNode::Filter {
1139 input: Box::new(PlanNode::SeqScan {
1140 table: del.source.clone(),
1141 }),
1142 predicate: pred,
1143 },
1144 },
1145 },
1146 None => PlanNode::SeqScan {
1147 table: del.source.clone(),
1148 },
1149 };
1150 Ok(PlanNode::Delete {
1151 input: Box::new(source),
1152 table: del.source,
1153 returning: del.returning,
1154 })
1155}
1156
1157fn plan_upsert(ups: UpsertExpr) -> Result<PlanNode, PlanError> {
1158 Ok(PlanNode::Upsert {
1159 table: ups.target,
1160 key_column: ups.key_column,
1161 assignments: ups.assignments,
1162 on_conflict: ups.on_conflict,
1163 })
1164}
1165
1166fn plan_create_type(ct: CreateTypeExpr) -> Result<PlanNode, PlanError> {
1167 let fields = ct
1168 .fields
1169 .into_iter()
1170 .map(|f| crate::plan::CreateField {
1171 name: f.name,
1172 type_name: f.type_name,
1173 required: f.required,
1174 unique: f.unique,
1175 default: f.default,
1176 auto: f.auto,
1177 })
1178 .collect();
1179 Ok(PlanNode::CreateTable {
1180 name: ct.name,
1181 fields,
1182 if_not_exists: ct.if_not_exists,
1183 })
1184}
1185
1186pub(crate) fn try_extract_eq_index_key(table: &str, pred: &Expr) -> Option<PlanNode> {
1196 let (lhs, op, rhs) = match pred {
1197 Expr::BinaryOp(lhs, op, rhs) => (lhs.as_ref(), *op, rhs.as_ref()),
1198 _ => return None,
1199 };
1200 if op != BinOp::Eq {
1201 return None;
1202 }
1203 match (lhs, rhs) {
1204 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some(PlanNode::ExprIndexScan {
1205 table: table.to_string(),
1206 path: stored_json_path(path)?,
1207 key: rhs.clone(),
1208 }),
1209 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some(PlanNode::ExprIndexScan {
1210 table: table.to_string(),
1211 path: stored_json_path(path)?,
1212 key: lhs.clone(),
1213 }),
1214 (Expr::Field(name), Expr::Literal(_)) => Some(PlanNode::IndexScan {
1215 table: table.to_string(),
1216 column: name.clone(),
1217 key: rhs.clone(),
1218 }),
1219 (Expr::Literal(_), Expr::Field(name)) => Some(PlanNode::IndexScan {
1220 table: table.to_string(),
1221 column: name.clone(),
1222 key: lhs.clone(),
1223 }),
1224 _ => None,
1225 }
1226}
1227
1228fn stored_json_path(expr: &Expr) -> Option<StoredJsonPathV1> {
1229 JsonPathIdentityV1::from_expr(expr)?.bind_table_local(None)
1230}
1231
1232pub(crate) fn extract_single_bound(pred: &Expr) -> Option<RangeBound> {
1235 let (lhs, op, rhs) = match pred {
1236 Expr::BinaryOp(lhs, op, rhs) => (lhs.as_ref(), *op, rhs.as_ref()),
1237 _ => return None,
1238 };
1239 match op {
1240 BinOp::Gt => match (lhs, rhs) {
1242 (Expr::Field(name), Expr::Literal(_)) => Some((
1243 RangeTarget::Column(name.clone()),
1244 Some((rhs.clone(), false)),
1245 None,
1246 )),
1247 (Expr::Literal(_), Expr::Field(name)) => {
1248 Some((
1250 RangeTarget::Column(name.clone()),
1251 None,
1252 Some((lhs.clone(), false)),
1253 ))
1254 }
1255 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1256 RangeTarget::JsonPath(stored_json_path(path)?),
1257 Some((rhs.clone(), false)),
1258 None,
1259 )),
1260 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1261 RangeTarget::JsonPath(stored_json_path(path)?),
1262 None,
1263 Some((lhs.clone(), false)),
1264 )),
1265 _ => None,
1266 },
1267 BinOp::Gte => match (lhs, rhs) {
1269 (Expr::Field(name), Expr::Literal(_)) => Some((
1270 RangeTarget::Column(name.clone()),
1271 Some((rhs.clone(), true)),
1272 None,
1273 )),
1274 (Expr::Literal(_), Expr::Field(name)) => Some((
1275 RangeTarget::Column(name.clone()),
1276 None,
1277 Some((lhs.clone(), true)),
1278 )),
1279 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1280 RangeTarget::JsonPath(stored_json_path(path)?),
1281 Some((rhs.clone(), true)),
1282 None,
1283 )),
1284 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1285 RangeTarget::JsonPath(stored_json_path(path)?),
1286 None,
1287 Some((lhs.clone(), true)),
1288 )),
1289 _ => None,
1290 },
1291 BinOp::Lt => match (lhs, rhs) {
1293 (Expr::Field(name), Expr::Literal(_)) => Some((
1294 RangeTarget::Column(name.clone()),
1295 None,
1296 Some((rhs.clone(), false)),
1297 )),
1298 (Expr::Literal(_), Expr::Field(name)) => Some((
1299 RangeTarget::Column(name.clone()),
1300 Some((lhs.clone(), false)),
1301 None,
1302 )),
1303 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1304 RangeTarget::JsonPath(stored_json_path(path)?),
1305 None,
1306 Some((rhs.clone(), false)),
1307 )),
1308 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1309 RangeTarget::JsonPath(stored_json_path(path)?),
1310 Some((lhs.clone(), false)),
1311 None,
1312 )),
1313 _ => None,
1314 },
1315 BinOp::Lte => match (lhs, rhs) {
1317 (Expr::Field(name), Expr::Literal(_)) => Some((
1318 RangeTarget::Column(name.clone()),
1319 None,
1320 Some((rhs.clone(), true)),
1321 )),
1322 (Expr::Literal(_), Expr::Field(name)) => Some((
1323 RangeTarget::Column(name.clone()),
1324 Some((lhs.clone(), true)),
1325 None,
1326 )),
1327 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1328 RangeTarget::JsonPath(stored_json_path(path)?),
1329 None,
1330 Some((rhs.clone(), true)),
1331 )),
1332 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1333 RangeTarget::JsonPath(stored_json_path(path)?),
1334 Some((lhs.clone(), true)),
1335 None,
1336 )),
1337 _ => None,
1338 },
1339 _ => None,
1340 }
1341}
1342
1343fn try_extract_range_index_keys(table: &str, pred: &Expr) -> Option<PlanNode> {
1366 if let Expr::BinaryOp(lhs, BinOp::And, rhs) = pred {
1369 if let (Some((col1, s1, e1)), Some((col2, s2, e2))) =
1370 (extract_single_bound(lhs), extract_single_bound(rhs))
1371 {
1372 if col1 == col2 {
1373 if let (Some(start), None, None, Some(end)) = (s1, e1, s2, e2) {
1374 return Some(range_scan_for_target(table, col1, Some(start), Some(end)));
1375 }
1376 }
1377 }
1378 }
1379
1380 if let Some((col, start, end)) = extract_single_bound(pred) {
1382 return Some(range_scan_for_target(table, col, start, end));
1383 }
1384
1385 None
1386}
1387
1388pub(crate) fn range_scan_for_target(
1389 table: &str,
1390 target: RangeTarget,
1391 start: Option<(Expr, bool)>,
1392 end: Option<(Expr, bool)>,
1393) -> PlanNode {
1394 match target {
1395 RangeTarget::Column(column) => PlanNode::RangeScan {
1396 table: table.to_string(),
1397 column,
1398 start,
1399 end,
1400 },
1401 RangeTarget::JsonPath(path) => PlanNode::ExprRangeScan {
1402 table: table.to_string(),
1403 path,
1404 start,
1405 end,
1406 },
1407 }
1408}
1409
1410fn try_extract_ordered_expr_index_scan(query: &QueryExpr) -> Option<PlanNode> {
1415 if query.alias.is_some()
1416 || !query.joins.is_empty()
1417 || query.filter.is_some()
1418 || query.group_by.is_some()
1419 || query.distinct
1420 || query.aggregation.is_some()
1421 || query.projection.as_ref().is_some_and(|fields| {
1422 fields
1423 .iter()
1424 .any(|field| matches!(field.expr, Expr::Window { .. }))
1425 })
1426 {
1427 return None;
1428 }
1429 let order = query.order.as_ref()?;
1430 let [key] = order.keys.as_slice() else {
1431 return None;
1432 };
1433 let path = stored_json_path(&key.expr)?;
1434 let limit = query.limit.as_ref()?;
1435 if !matches!(limit, Expr::Literal(Literal::Int(value)) if *value >= 0) {
1436 return None;
1437 }
1438 if !query
1439 .offset
1440 .as_ref()
1441 .is_none_or(|offset| matches!(offset, Expr::Literal(Literal::Int(value)) if *value >= 0))
1442 {
1443 return None;
1444 }
1445 Some(PlanNode::OrderedExprIndexScan {
1446 table: query.source.clone(),
1447 path,
1448 descending: key.descending,
1449 limit: limit.clone(),
1450 offset: query.offset.clone(),
1451 })
1452}
1453
1454fn extract_windows(proj_fields: &mut [ProjectField]) -> Vec<WindowDef> {
1459 let mut defs = Vec::new();
1460 let mut counter = 0usize;
1461 for f in proj_fields.iter_mut() {
1462 if let Expr::Window {
1463 function,
1464 args,
1465 mode,
1466 partition_by,
1467 order_by,
1468 } = &f.expr
1469 {
1470 let output_name = format!("__win_{counter}");
1471 defs.push(WindowDef {
1472 function: *function,
1473 args: args.clone(),
1474 mode: *mode,
1475 partition_by: partition_by.clone(),
1476 order_by: order_by
1477 .iter()
1478 .map(|k| SortKey {
1479 expr: k.expr.clone(),
1480 descending: k.descending,
1481 })
1482 .collect(),
1483 output_name: output_name.clone(),
1484 });
1485 f.expr = Expr::Field(output_name);
1486 counter += 1;
1487 }
1488 }
1489 defs
1490}
1491
1492fn extract_aggregates(
1498 proj_fields: &mut [ProjectField],
1499 having: &mut Option<Expr>,
1500 source_aliases: &std::collections::HashSet<String>,
1501) -> Result<Vec<GroupAgg>, PlanError> {
1502 let mut aggs: Vec<GroupAgg> = Vec::new();
1503 let mut counter = 0usize;
1504 for f in proj_fields.iter_mut() {
1505 rewrite_agg_expr(&mut f.expr, &mut aggs, &mut counter, source_aliases)?;
1506 }
1507 if let Some(h) = having {
1508 rewrite_agg_expr(h, &mut aggs, &mut counter, source_aliases)?;
1509 }
1510 Ok(aggs)
1511}
1512
1513fn rewrite_group_key_references(
1514 fields: &mut [ProjectField],
1515 having: &mut Option<Expr>,
1516 keys: &[GroupKey],
1517) {
1518 for field in fields {
1519 rewrite_group_key_expr(&mut field.expr, keys);
1520 }
1521 if let Some(having) = having {
1522 rewrite_group_key_expr(having, keys);
1523 }
1524}
1525
1526fn rewrite_group_order_keys(
1527 order: Option<&mut OrderClause>,
1528 projection: &[ProjectField],
1529 keys: &[GroupKey],
1530) {
1531 let Some(order) = order else {
1532 return;
1533 };
1534 for order_key in &mut order.keys {
1535 let Some(group_key) = keys.iter().find(|key| key.expr == order_key.expr) else {
1536 continue;
1537 };
1538 let projected_name = projection
1539 .iter()
1540 .find(|field| field.expr == group_key.expr)
1541 .and_then(|field| field.alias.clone())
1542 .unwrap_or_else(|| group_key.output_name());
1543 order_key.expr = Expr::Field(projected_name);
1544 }
1545}
1546
1547fn rewrite_group_key_expr(expr: &mut Expr, keys: &[GroupKey]) {
1548 if let Some(key) = keys.iter().find(|key| key.expr == *expr) {
1549 *expr = Expr::Field(key.output_name());
1550 return;
1551 }
1552 match expr {
1553 Expr::FunctionCall(..) => {}
1557 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1558 rewrite_group_key_expr(left, keys);
1559 rewrite_group_key_expr(right, keys);
1560 }
1561 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) => rewrite_group_key_expr(inner, keys),
1562 Expr::ScalarFunc(_, args) => {
1563 for arg in args {
1564 rewrite_group_key_expr(arg, keys);
1565 }
1566 }
1567 Expr::InList { expr, list, .. } => {
1568 rewrite_group_key_expr(expr, keys);
1569 for item in list {
1570 rewrite_group_key_expr(item, keys);
1571 }
1572 }
1573 Expr::Case { whens, else_expr } => {
1574 for (condition, result) in whens {
1575 rewrite_group_key_expr(condition, keys);
1576 rewrite_group_key_expr(result, keys);
1577 }
1578 if let Some(expr) = else_expr {
1579 rewrite_group_key_expr(expr, keys);
1580 }
1581 }
1582 _ => {}
1583 }
1584}
1585
1586fn rewrite_agg_expr(
1587 expr: &mut Expr,
1588 aggs: &mut Vec<GroupAgg>,
1589 counter: &mut usize,
1590 source_aliases: &std::collections::HashSet<String>,
1591) -> Result<(), PlanError> {
1592 match expr {
1593 Expr::FunctionCall(func, inner, mode) => {
1594 let output = find_or_insert_agg(aggs, *func, inner, *mode, counter, source_aliases)?;
1595 *expr = Expr::Field(output);
1596 }
1597 Expr::BinaryOp(l, _, r) => {
1598 rewrite_agg_expr(l, aggs, counter, source_aliases)?;
1599 rewrite_agg_expr(r, aggs, counter, source_aliases)?;
1600 }
1601 Expr::UnaryOp(_, inner) => rewrite_agg_expr(inner, aggs, counter, source_aliases)?,
1602 Expr::Coalesce(l, r) => {
1603 rewrite_agg_expr(l, aggs, counter, source_aliases)?;
1604 rewrite_agg_expr(r, aggs, counter, source_aliases)?;
1605 }
1606 Expr::InList { expr: e, list, .. } => {
1607 rewrite_agg_expr(e, aggs, counter, source_aliases)?;
1608 for item in list {
1609 rewrite_agg_expr(item, aggs, counter, source_aliases)?;
1610 }
1611 }
1612 Expr::InSubquery { expr: e, .. } => {
1613 rewrite_agg_expr(e, aggs, counter, source_aliases)?;
1614 }
1615 _ => {}
1616 }
1617 Ok(())
1618}
1619
1620fn find_or_insert_agg(
1621 aggs: &mut Vec<GroupAgg>,
1622 func: AggFunc,
1623 argument: &Expr,
1624 mode: AggregateMode,
1625 counter: &mut usize,
1626 source_aliases: &std::collections::HashSet<String>,
1627) -> Result<String, PlanError> {
1628 for existing in aggs.iter() {
1629 if existing.function == func && existing.argument == *argument && existing.mode == mode {
1630 return Ok(existing.output_name.clone());
1631 }
1632 }
1633 let provenance_alias = symmetric_provenance_alias(func, Some(argument), mode, source_aliases)?;
1634 let output_name = format!("__agg_{counter}");
1635 aggs.push(GroupAgg {
1636 function: func,
1637 argument: argument.clone(),
1638 mode,
1639 provenance_alias,
1640 output_name: output_name.clone(),
1641 });
1642 *counter += 1;
1643 Ok(output_name)
1644}
1645
1646fn symmetric_provenance_alias(
1647 function: AggFunc,
1648 argument: Option<&Expr>,
1649 mode: AggregateMode,
1650 source_aliases: &std::collections::HashSet<String>,
1651) -> Result<Option<String>, PlanError> {
1652 if mode == AggregateMode::Raw
1653 || source_aliases.len() < 2
1654 || !matches!(function, AggFunc::Sum | AggFunc::Avg | AggFunc::Count)
1655 || (function == AggFunc::Count
1656 && argument.is_none_or(|argument| matches!(argument, Expr::Field(name) if name == "*")))
1657 {
1658 return Ok(None);
1659 }
1660 let Some(argument) = argument else {
1661 return Err(symmetric_aggregate_error(
1662 function,
1663 "does not reference a source row",
1664 ));
1665 };
1666
1667 let mut qualified = std::collections::HashSet::new();
1668 let mut has_unqualified = false;
1669 collect_expression_sources(argument, &mut qualified, &mut has_unqualified);
1670
1671 for alias in &qualified {
1672 if !source_aliases.contains(alias) {
1673 return Err(symmetric_aggregate_error(
1674 function,
1675 &format!("references unknown source alias '{alias}'"),
1676 ));
1677 }
1678 }
1679 if has_unqualified {
1680 if source_aliases.len() != 1 {
1681 return Err(symmetric_aggregate_error(
1682 function,
1683 "contains an ambiguous unqualified field",
1684 ));
1685 }
1686 qualified.extend(source_aliases.iter().cloned());
1687 }
1688 match qualified.len() {
1689 1 => Ok(qualified.into_iter().next()),
1690 0 => Err(symmetric_aggregate_error(
1691 function,
1692 "does not reference a source row",
1693 )),
1694 _ => Err(symmetric_aggregate_error(
1695 function,
1696 "references multiple source aliases",
1697 )),
1698 }
1699}
1700
1701fn symmetric_aggregate_error(function: AggFunc, reason: &str) -> PlanError {
1702 let name = format!("{function:?}").to_lowercase();
1703 PlanError::Semantic(format!(
1704 "symmetric {name} expression {reason}; reference exactly one source alias or use {name}(raw ...)"
1705 ))
1706}
1707
1708fn collect_expression_sources(
1709 expr: &Expr,
1710 qualified: &mut std::collections::HashSet<String>,
1711 has_unqualified: &mut bool,
1712) {
1713 match expr {
1714 Expr::Field(name) if name != "*" => *has_unqualified = true,
1715 Expr::QualifiedField { qualifier, .. } => {
1716 qualified.insert(qualifier.clone());
1717 }
1718 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1719 collect_expression_sources(left, qualified, has_unqualified);
1720 collect_expression_sources(right, qualified, has_unqualified);
1721 }
1722 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
1723 collect_expression_sources(inner, qualified, has_unqualified);
1724 }
1725 Expr::ScalarFunc(_, args) => {
1726 for argument in args {
1727 collect_expression_sources(argument, qualified, has_unqualified);
1728 }
1729 }
1730 Expr::InList { expr, list, .. } => {
1731 collect_expression_sources(expr, qualified, has_unqualified);
1732 for item in list {
1733 collect_expression_sources(item, qualified, has_unqualified);
1734 }
1735 }
1736 Expr::InSubquery { expr, .. } => {
1737 collect_expression_sources(expr, qualified, has_unqualified);
1738 }
1739 Expr::Case { whens, else_expr } => {
1740 for (condition, result) in whens {
1741 collect_expression_sources(condition, qualified, has_unqualified);
1742 collect_expression_sources(result, qualified, has_unqualified);
1743 }
1744 if let Some(expr) = else_expr {
1745 collect_expression_sources(expr, qualified, has_unqualified);
1746 }
1747 }
1748 Expr::Window {
1749 args,
1750 partition_by,
1751 order_by,
1752 ..
1753 } => {
1754 for expr in args.iter().chain(partition_by) {
1755 collect_expression_sources(expr, qualified, has_unqualified);
1756 }
1757 for key in order_by {
1758 collect_expression_sources(&key.expr, qualified, has_unqualified);
1759 }
1760 }
1761 Expr::FunctionCall(_, inner, _) => {
1762 collect_expression_sources(inner, qualified, has_unqualified);
1763 }
1764 Expr::LinkPath { outer_alias, .. } => {
1766 qualified.insert(outer_alias.clone());
1767 }
1768 Expr::ExistsSubquery { .. }
1769 | Expr::Field(_)
1770 | Expr::Literal(_)
1771 | Expr::Param(_)
1772 | Expr::ValueLit(_)
1773 | Expr::Null
1774 | Expr::NestedQuery(_) => {}
1775 }
1776}
1777
1778#[cfg(test)]
1779mod tests {
1780 use super::*;
1781 use crate::plan::PlanNode;
1782
1783 #[test]
1784 fn test_plan_simple_scan() {
1785 let plan = plan("User").unwrap();
1786 assert!(matches!(plan, PlanNode::SeqScan { table } if table == "User"));
1787 }
1788
1789 #[test]
1790 fn test_plan_filter() {
1791 let plan = plan("User filter .age > 30").unwrap();
1792 assert!(matches!(plan, PlanNode::RangeScan { .. }));
1793 }
1794
1795 #[test]
1796 fn test_plan_filter_with_projection() {
1797 let plan = plan("User filter .age > 30 { name, email }").unwrap();
1798 assert!(matches!(plan, PlanNode::Project { .. }));
1799 }
1800
1801 #[test]
1802 fn test_plan_insert() {
1803 let plan = plan(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
1804 assert!(matches!(plan, PlanNode::Insert { .. }));
1805 }
1806
1807 #[test]
1808 fn test_plan_order_limit() {
1809 let plan = plan("User order .name limit 10").unwrap();
1810 match plan {
1811 PlanNode::Limit { input, .. } => {
1812 assert!(matches!(*input, PlanNode::Sort { .. }));
1813 }
1814 _ => panic!("expected Limit(Sort(SeqScan))"),
1815 }
1816 }
1817
1818 #[test]
1819 fn test_plan_count() {
1820 let plan = plan("count(User)").unwrap();
1821 assert!(matches!(plan, PlanNode::Aggregate { .. }));
1822 }
1823
1824 #[test]
1825 fn single_source_aggregates_do_not_request_provenance() {
1826 for query in [
1827 "sum(User { .amount })",
1828 "avg(User { .amount })",
1829 "count(User { .amount })",
1830 ] {
1831 match plan(query).unwrap() {
1832 PlanNode::Aggregate {
1833 provenance_alias, ..
1834 } => assert!(
1835 provenance_alias.is_none(),
1836 "unexpected provenance for {query}"
1837 ),
1838 other => panic!("expected Aggregate for {query}, got {other:?}"),
1839 }
1840 }
1841
1842 match plan("User group .dept { total: sum(.amount) }").unwrap() {
1843 PlanNode::Project { input, .. } => match *input {
1844 PlanNode::GroupBy { aggregates, .. } => {
1845 assert!(aggregates[0].provenance_alias.is_none());
1846 }
1847 other => panic!("expected GroupBy, got {other:?}"),
1848 },
1849 other => panic!("expected Project(GroupBy), got {other:?}"),
1850 }
1851 }
1852
1853 #[test]
1854 fn join_provenance_is_limited_to_fanout_sensitive_aggregates() {
1855 let base = "Account as a join Entry as e on a.id = e.account_id group a.dept";
1856 for (function, expects_provenance) in [
1857 ("sum(a.balance)", true),
1858 ("avg(a.balance)", true),
1859 ("count(a.balance)", true),
1860 ("min(a.balance)", false),
1861 ("max(a.balance)", false),
1862 ("count(distinct a.balance)", false),
1863 ("count(*)", false),
1864 ] {
1865 let query = format!("{base} {{ value: {function} }}");
1866 match plan(&query).unwrap() {
1867 PlanNode::Project { input, .. } => match *input {
1868 PlanNode::GroupBy { aggregates, .. } => assert_eq!(
1869 aggregates[0].provenance_alias.as_deref(),
1870 expects_provenance.then_some("a"),
1871 "unexpected provenance selection for {function}"
1872 ),
1873 other => panic!("expected GroupBy for {function}, got {other:?}"),
1874 },
1875 other => panic!("expected Project(GroupBy) for {function}, got {other:?}"),
1876 }
1877 }
1878 }
1879
1880 #[test]
1881 fn test_plan_eq_becomes_index_scan() {
1882 let plan = plan("User filter .id = 42").unwrap();
1885 match plan {
1886 PlanNode::IndexScan { table, column, key } => {
1887 assert_eq!(table, "User");
1888 assert_eq!(column, "id");
1889 assert!(matches!(key, Expr::Literal(Literal::Int(42))));
1890 }
1891 other => panic!("expected IndexScan, got {other:?}"),
1892 }
1893 }
1894
1895 #[test]
1896 fn test_plan_eq_reversed_becomes_index_scan() {
1897 let plan = plan(r#"User filter "NYC" = .city"#).unwrap();
1899 assert!(matches!(plan, PlanNode::IndexScan { .. }));
1900 }
1901
1902 #[test]
1903 fn json_path_equality_and_reversed_equality_are_speculative_expression_scans() {
1904 for query in ["Post filter .data->age = 21", "Post filter 21 = .data->age"] {
1905 match plan(query).unwrap() {
1906 PlanNode::ExprIndexScan { table, path, key } => {
1907 assert_eq!(table, "Post");
1908 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1909 assert!(matches!(key, Expr::Literal(Literal::Int(21))));
1910 }
1911 other => panic!("expected ExprIndexScan for `{query}`, got {other:?}"),
1912 }
1913 }
1914 }
1915
1916 #[test]
1917 fn json_path_range_and_same_path_compound_bounds_are_speculative_scans() {
1918 for query in ["Post filter .data->age > 18", "Post filter 18 < .data->age"] {
1919 match plan(query).unwrap() {
1920 PlanNode::ExprRangeScan {
1921 path, start, end, ..
1922 } => {
1923 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1924 assert!(start.is_some());
1925 assert!(end.is_none());
1926 }
1927 other => panic!("expected ExprRangeScan for `{query}`, got {other:?}"),
1928 }
1929 }
1930
1931 match plan("Post filter .data->age >= 18 and .data->age < 65").unwrap() {
1932 PlanNode::ExprRangeScan {
1933 path, start, end, ..
1934 } => {
1935 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1936 assert_eq!(start, Some((Expr::Literal(Literal::Int(18)), true)));
1937 assert_eq!(end, Some((Expr::Literal(Literal::Int(65)), false)));
1938 }
1939 other => panic!("expected bounded ExprRangeScan, got {other:?}"),
1940 }
1941
1942 assert!(matches!(
1943 plan("Post filter .data->age >= 18 and .data->score < 65").unwrap(),
1944 PlanNode::Filter { .. }
1945 ));
1946 }
1947
1948 #[test]
1949 fn exact_single_path_order_limit_uses_ordered_expression_scan() {
1950 match plan("Post order .data->age desc limit 10 offset 2 { .id }").unwrap() {
1951 PlanNode::Project { input, .. } => match *input {
1952 PlanNode::OrderedExprIndexScan {
1953 table,
1954 path,
1955 descending,
1956 limit,
1957 offset,
1958 } => {
1959 assert_eq!(table, "Post");
1960 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1961 assert!(descending);
1962 assert_eq!(limit, Expr::Literal(Literal::Int(10)));
1963 assert_eq!(offset, Some(Expr::Literal(Literal::Int(2))));
1964 }
1965 other => panic!("expected OrderedExprIndexScan, got {other:?}"),
1966 },
1967 other => panic!("expected Project(OrderedExprIndexScan), got {other:?}"),
1968 }
1969 }
1970
1971 #[test]
1972 fn incompatible_path_order_shapes_keep_generic_sort() {
1973 for query in [
1974 "Post order .data->age",
1975 "Post order .data->age, .id limit 10",
1976 "Post filter .data->active = true order .data->age limit 10",
1977 "Post order .data->age limit .id",
1978 ] {
1979 let planned = plan(query).unwrap();
1980 assert!(
1981 !plan_contains_ordered_expr_scan(&planned),
1982 "`{query}` must remain on the generic pipeline: {planned:?}"
1983 );
1984 }
1985 }
1986
1987 fn plan_contains_ordered_expr_scan(plan: &PlanNode) -> bool {
1988 match plan {
1989 PlanNode::OrderedExprIndexScan { .. } => true,
1990 PlanNode::Filter { input, .. }
1991 | PlanNode::Project { input, .. }
1992 | PlanNode::Sort { input, .. }
1993 | PlanNode::Limit { input, .. }
1994 | PlanNode::Offset { input, .. }
1995 | PlanNode::Aggregate { input, .. }
1996 | PlanNode::Distinct { input }
1997 | PlanNode::GroupBy { input, .. }
1998 | PlanNode::Update { input, .. }
1999 | PlanNode::Delete { input, .. }
2000 | PlanNode::Window { input, .. }
2001 | PlanNode::Explain { input } => plan_contains_ordered_expr_scan(input),
2002 PlanNode::NestedLoopJoin { left, right, .. } | PlanNode::Union { left, right, .. } => {
2003 plan_contains_ordered_expr_scan(left) || plan_contains_ordered_expr_scan(right)
2004 }
2005 _ => false,
2006 }
2007 }
2008
2009 #[test]
2010 fn test_plan_non_eq_stays_filter() {
2011 let plan = plan("User filter .age > 30").unwrap();
2013 match plan {
2014 PlanNode::RangeScan {
2015 column, start, end, ..
2016 } => {
2017 assert_eq!(column, "age");
2018 assert!(start.is_some(), "expected lower bound");
2019 assert!(end.is_none(), "expected no upper bound");
2020 let (_, inclusive) = start.unwrap();
2021 assert!(!inclusive, "expected exclusive lower bound for >");
2022 }
2023 other => panic!("expected RangeScan, got {other:?}"),
2024 }
2025 }
2026
2027 #[test]
2028 fn test_plan_index_scan_with_projection() {
2029 let plan = plan("User filter .id = 1 { .name }").unwrap();
2031 match plan {
2032 PlanNode::Project { input, .. } => {
2033 assert!(matches!(*input, PlanNode::IndexScan { .. }));
2034 }
2035 other => panic!("expected Project(IndexScan), got {other:?}"),
2036 }
2037 }
2038
2039 #[test]
2040 fn test_plan_update_by_pk_becomes_index_scan() {
2041 let plan = plan("User filter .id = 42 update { age := 31 }").unwrap();
2044 match plan {
2045 PlanNode::Update { input, .. } => {
2046 assert!(
2047 matches!(*input, PlanNode::IndexScan { .. }),
2048 "expected Update(IndexScan), got {input:?}"
2049 );
2050 }
2051 other => panic!("expected Update, got {other:?}"),
2052 }
2053 }
2054
2055 #[test]
2056 fn test_plan_update_range_stays_range_scan() {
2057 let plan = plan("User filter .age > 30 update { age := 31 }").unwrap();
2058 match plan {
2059 PlanNode::Update { input, .. } => {
2060 assert!(
2061 matches!(*input, PlanNode::RangeScan { .. }),
2062 "expected Update(RangeScan), got {input:?}"
2063 );
2064 }
2065 other => panic!("expected Update, got {other:?}"),
2066 }
2067 }
2068
2069 #[test]
2070 fn test_plan_delete_by_pk_becomes_index_scan() {
2071 let plan = plan("User filter .id = 7 delete").unwrap();
2072 match plan {
2073 PlanNode::Delete { input, .. } => {
2074 assert!(matches!(*input, PlanNode::IndexScan { .. }));
2075 }
2076 other => panic!("expected Delete, got {other:?}"),
2077 }
2078 }
2079
2080 #[test]
2081 fn test_plan_inner_join_builds_nested_loop() {
2082 let plan = plan("User as u join Order as o on u.id = o.user_id").unwrap();
2085 match plan {
2086 PlanNode::NestedLoopJoin {
2087 left,
2088 right,
2089 on,
2090 kind,
2091 } => {
2092 assert_eq!(kind, JoinKind::Inner);
2093 assert!(on.is_some());
2094 assert!(matches!(*left, PlanNode::AliasScan { .. }));
2095 assert!(matches!(*right, PlanNode::AliasScan { .. }));
2096 }
2097 other => panic!("expected NestedLoopJoin, got {other:?}"),
2098 }
2099 }
2100
2101 #[test]
2102 fn duplicate_join_aliases_are_rejected_before_execution() {
2103 let err = plan("A as x join A as x on x.id = x.id").unwrap_err();
2104 assert!(
2105 err.to_string().contains("duplicate source alias `x`"),
2106 "unexpected error: {err}"
2107 );
2108 }
2109
2110 #[test]
2111 fn test_plan_right_join_rewritten_as_left_with_swapped_inputs() {
2112 let plan = plan("User as u right join Order as o on u.id = o.user_id").unwrap();
2113 match plan {
2114 PlanNode::NestedLoopJoin {
2115 left, right, kind, ..
2116 } => {
2117 assert_eq!(kind, JoinKind::LeftOuter);
2118 match *left {
2120 PlanNode::AliasScan { table, .. } => assert_eq!(table, "Order"),
2121 other => panic!("expected AliasScan(Order), got {other:?}"),
2122 }
2123 match *right {
2124 PlanNode::AliasScan { table, .. } => assert_eq!(table, "User"),
2125 other => panic!("expected AliasScan(User), got {other:?}"),
2126 }
2127 }
2128 other => panic!("expected NestedLoopJoin, got {other:?}"),
2129 }
2130 }
2131
2132 #[test]
2133 fn test_plan_multi_join_is_left_deep() {
2134 let plan = plan(
2136 "User as u join Order as o on u.id = o.user_id \
2137 join Product as p on o.product_id = p.id",
2138 )
2139 .unwrap();
2140 match plan {
2141 PlanNode::NestedLoopJoin { left, right, .. } => {
2142 match *right {
2144 PlanNode::AliasScan { table, .. } => assert_eq!(table, "Product"),
2145 other => panic!("expected AliasScan(Product), got {other:?}"),
2146 }
2147 assert!(matches!(*left, PlanNode::NestedLoopJoin { .. }));
2149 }
2150 other => panic!("expected NestedLoopJoin, got {other:?}"),
2151 }
2152 }
2153
2154 #[test]
2155 fn test_plan_join_with_filter_tail_wraps_filter_on_top() {
2156 let plan =
2157 plan("User as u join Order as o on u.id = o.user_id filter o.total > 100").unwrap();
2158 match plan {
2159 PlanNode::Filter { input, .. } => {
2160 assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
2161 }
2162 other => panic!("expected Filter(NestedLoopJoin), got {other:?}"),
2163 }
2164 }
2165
2166 #[test]
2167 fn test_plan_group_by_builds_groupby_node() {
2168 let plan = plan("User group .status { .status, n: count(.name) }").unwrap();
2169 match plan {
2171 PlanNode::Project { input, fields } => {
2172 assert_eq!(fields.len(), 2);
2173 match *input {
2174 PlanNode::GroupBy {
2175 input: inner,
2176 keys,
2177 aggregates,
2178 having,
2179 } => {
2180 assert!(matches!(*inner, PlanNode::SeqScan { .. }));
2181 assert_eq!(
2182 keys,
2183 vec![GroupKey {
2184 expr: Expr::Field("status".into()),
2185 output_name: "status".into(),
2186 }]
2187 );
2188 assert_eq!(aggregates.len(), 1);
2189 assert_eq!(aggregates[0].function, AggFunc::Count);
2190 assert_eq!(aggregates[0].argument, Expr::Field("name".into()));
2191 assert!(having.is_none());
2192 }
2193 other => panic!("expected GroupBy, got {other:?}"),
2194 }
2195 }
2196 other => panic!("expected Project, got {other:?}"),
2197 }
2198 }
2199
2200 #[test]
2201 fn test_plan_joined_group_applies_order_offset_limit_after_grouping() {
2202 let plan = plan(
2203 "User as u join Order as o on u.id = o.user_id \
2204 group u.status { u.status, n: count(*) } order n desc offset 1 limit 2",
2205 )
2206 .unwrap();
2207
2208 let PlanNode::Limit { input, .. } = plan else {
2209 panic!("expected Limit at the grouped-result boundary");
2210 };
2211 let PlanNode::Offset { input, .. } = *input else {
2212 panic!("expected Offset below Limit");
2213 };
2214 let PlanNode::Sort { input, .. } = *input else {
2215 panic!("expected Sort below Offset");
2216 };
2217 let PlanNode::Project { input, .. } = *input else {
2218 panic!("expected Project below Sort");
2219 };
2220 let PlanNode::GroupBy { input, .. } = *input else {
2221 panic!("expected GroupBy below Project");
2222 };
2223 assert!(
2224 matches!(*input, PlanNode::NestedLoopJoin { .. }),
2225 "joined rows must flow into GroupBy before result limiting"
2226 );
2227 }
2228
2229 #[test]
2230 fn test_plan_group_by_having_rewrites_agg_in_having() {
2231 let plan = plan("User group .status having count(.name) > 1 { .status }").unwrap();
2232 match plan {
2233 PlanNode::Project { input, .. } => {
2234 match *input {
2235 PlanNode::GroupBy {
2236 having, aggregates, ..
2237 } => {
2238 assert_eq!(aggregates.len(), 1);
2241 assert_eq!(aggregates[0].output_name, "__agg_0");
2242 let h = having.expect("having should be Some");
2243 match h {
2244 Expr::BinaryOp(l, BinOp::Gt, _) => {
2245 assert!(
2246 matches!(*l, Expr::Field(ref name) if name == "__agg_0"),
2247 "expected Field(__agg_0), got {l:?}"
2248 );
2249 }
2250 other => panic!("expected BinaryOp, got {other:?}"),
2251 }
2252 }
2253 other => panic!("expected GroupBy, got {other:?}"),
2254 }
2255 }
2256 other => panic!("expected Project, got {other:?}"),
2257 }
2258 }
2259
2260 #[test]
2261 fn test_plan_window_inserts_window_node_before_project() {
2262 let plan = plan("User { .name, rn: row_number() over (order .age) }").unwrap();
2263 match plan {
2265 PlanNode::Project { input, fields } => {
2266 assert_eq!(fields.len(), 2);
2267 assert!(
2269 matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"),
2270 "expected Field(__win_0), got {:?}",
2271 fields[1].expr
2272 );
2273 match *input {
2274 PlanNode::Window {
2275 input: inner,
2276 windows,
2277 } => {
2278 assert_eq!(windows.len(), 1);
2279 assert_eq!(windows[0].output_name, "__win_0");
2280 assert!(matches!(*inner, PlanNode::SeqScan { .. }));
2281 }
2282 other => panic!("expected Window, got {other:?}"),
2283 }
2284 }
2285 other => panic!("expected Project, got {other:?}"),
2286 }
2287 }
2288
2289 #[test]
2290 fn test_plan_multiple_windows() {
2291 let plan = plan(
2292 "User { .name, rn: row_number() over (order .age), s: sum(.salary) over (partition .dept order .salary) }"
2293 ).unwrap();
2294 match plan {
2295 PlanNode::Project { input, fields } => {
2296 assert_eq!(fields.len(), 3);
2297 assert!(matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"));
2298 assert!(matches!(&fields[2].expr, Expr::Field(name) if name == "__win_1"));
2299 match *input {
2300 PlanNode::Window { windows, .. } => {
2301 assert_eq!(windows.len(), 2);
2302 assert_eq!(windows[0].output_name, "__win_0");
2303 assert_eq!(windows[1].output_name, "__win_1");
2304 }
2305 other => panic!("expected Window, got {other:?}"),
2306 }
2307 }
2308 other => panic!("expected Project, got {other:?}"),
2309 }
2310 }
2311
2312 #[test]
2313 fn test_plan_no_window_without_over() {
2314 let plan = plan("User group .dept { .dept, total: sum(.salary) }").unwrap();
2316 match plan {
2317 PlanNode::Project { input, .. } => {
2318 assert!(
2320 matches!(*input, PlanNode::GroupBy { .. }),
2321 "expected GroupBy under Project, got {:?}",
2322 input
2323 );
2324 }
2325 other => panic!("expected Project, got {other:?}"),
2326 }
2327 }
2328
2329 #[test]
2330 fn test_plan_explain_wraps_inner() {
2331 let plan = plan("explain User filter .age > 30").unwrap();
2332 match plan {
2333 PlanNode::Explain { input } => {
2334 assert!(
2335 matches!(*input, PlanNode::RangeScan { .. }),
2336 "expected Explain(RangeScan), got {:?}",
2337 input
2338 );
2339 }
2340 other => panic!("expected Explain, got {other:?}"),
2341 }
2342 }
2343
2344 #[test]
2345 fn test_plan_explain_simple_scan() {
2346 let plan = plan("explain User").unwrap();
2347 match plan {
2348 PlanNode::Explain { input } => {
2349 assert!(matches!(*input, PlanNode::SeqScan { .. }));
2350 }
2351 other => panic!("expected Explain(SeqScan), got {other:?}"),
2352 }
2353 }
2354
2355 #[test]
2356 fn test_plan_explain_join() {
2357 let plan = plan("explain User as u join Order as o on u.id = o.user_id").unwrap();
2358 match plan {
2359 PlanNode::Explain { input } => {
2360 assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
2361 }
2362 other => panic!("expected Explain(NestedLoopJoin), got {other:?}"),
2363 }
2364 }
2365}