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