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::AlterTable(at) => Ok(PlanNode::AlterTable {
68 table: at.table,
69 action: at.action,
70 }),
71 Statement::DropTable(dt) => Ok(PlanNode::DropTable {
72 name: dt.table,
73 if_exists: dt.if_exists,
74 }),
75 Statement::CreateView(cv) => Ok(PlanNode::CreateView {
76 name: cv.name,
77 query_text: cv.query_text,
78 }),
79 Statement::RefreshView(rv) => Ok(PlanNode::RefreshView { name: rv.name }),
80 Statement::DropView(dv) => Ok(PlanNode::DropView {
81 name: dv.name,
82 if_exists: dv.if_exists,
83 }),
84 Statement::ListTypes => Ok(PlanNode::ListTypes),
85 Statement::Describe(table) => Ok(PlanNode::Describe { table }),
86 Statement::Union(u) => {
87 let left = plan_statement(*u.left)?;
88 let right = plan_statement(*u.right)?;
89 Ok(PlanNode::Union {
90 left: Box::new(left),
91 right: Box::new(right),
92 all: u.all,
93 })
94 }
95 Statement::Upsert(ups) => plan_upsert(ups),
96 Statement::Begin => Ok(PlanNode::Begin),
97 Statement::Commit => Ok(PlanNode::Commit),
98 Statement::Rollback => Ok(PlanNode::Rollback),
99 Statement::Explain(inner) => {
100 let inner_plan = plan_statement(*inner)?;
101 Ok(PlanNode::Explain {
102 input: Box::new(inner_plan),
103 })
104 }
105 }
106}
107
108fn plan_query(mut q: QueryExpr) -> Result<PlanNode, PlanError> {
109 if q.projection.as_ref().is_some_and(|proj| {
112 proj.iter()
113 .any(|pf| matches!(pf.expr, Expr::NestedQuery(_)))
114 }) {
115 return plan_nested_query(q);
116 }
117 if !q.joins.is_empty() {
123 return plan_joined_query(q);
124 }
125 let source_aliases = std::collections::HashSet::from([q.source.clone()]);
126 let ordered_expr_scan = try_extract_ordered_expr_index_scan(&q);
138 let (source, filter) = if let Some(scan) = ordered_expr_scan {
139 q.order = None;
143 q.limit = None;
144 q.offset = None;
145 (scan, None)
146 } else {
147 match q.filter {
148 Some(pred) => match try_extract_eq_index_key(&q.source, &pred) {
149 Some(index_scan) => (index_scan, None),
150 None => match try_extract_range_index_keys(&q.source, &pred) {
151 Some(range_scan) => (range_scan, None),
152 None => (
153 PlanNode::SeqScan {
154 table: q.source.clone(),
155 },
156 Some(pred),
157 ),
158 },
159 },
160 None => (
161 PlanNode::SeqScan {
162 table: q.source.clone(),
163 },
164 None,
165 ),
166 }
167 };
168 let mut node = source;
169
170 if let Some(pred) = filter {
171 node = PlanNode::Filter {
172 input: Box::new(node),
173 predicate: pred,
174 };
175 }
176
177 if let Some(group) = q.group_by {
180 let mut grouped_order = q.order;
181 let mut proj_fields: Vec<ProjectField> = q
182 .projection
183 .map(|proj| {
184 proj.into_iter()
185 .map(|pf| ProjectField {
186 alias: pf.alias,
187 expr: pf.expr,
188 })
189 .collect()
190 })
191 .unwrap_or_default();
192 let mut having = group.having;
193 let aggregates = extract_aggregates(&mut proj_fields, &mut having, &source_aliases)?;
194 rewrite_group_order_keys(grouped_order.as_mut(), &proj_fields, &group.keys);
195 rewrite_group_key_references(&mut proj_fields, &mut having, &group.keys);
196
197 node = PlanNode::GroupBy {
198 input: Box::new(node),
199 keys: group.keys,
200 aggregates,
201 having,
202 };
203
204 if !proj_fields.is_empty() {
205 node = PlanNode::Project {
206 input: Box::new(node),
207 fields: proj_fields,
208 };
209 }
210
211 if let Some(order) = grouped_order {
212 node = PlanNode::Sort {
213 input: Box::new(node),
214 keys: order
215 .keys
216 .into_iter()
217 .map(|k| SortKey {
218 expr: k.expr,
219 descending: k.descending,
220 })
221 .collect(),
222 };
223 }
224 if let Some(off) = q.offset {
228 node = PlanNode::Offset {
229 input: Box::new(node),
230 count: off,
231 };
232 }
233 if let Some(lim) = q.limit {
234 node = PlanNode::Limit {
235 input: Box::new(node),
236 count: lim,
237 };
238 }
239 if q.distinct {
240 node = PlanNode::Distinct {
241 input: Box::new(node),
242 };
243 }
244 return Ok(node);
245 }
246
247 if let Some(order) = q.order {
248 node = PlanNode::Sort {
249 input: Box::new(node),
250 keys: order
251 .keys
252 .into_iter()
253 .map(|k| SortKey {
254 expr: k.expr,
255 descending: k.descending,
256 })
257 .collect(),
258 };
259 }
260
261 if let Some(off) = q.offset {
265 node = PlanNode::Offset {
266 input: Box::new(node),
267 count: off,
268 };
269 }
270
271 if let Some(lim) = q.limit {
272 node = PlanNode::Limit {
273 input: Box::new(node),
274 count: lim,
275 };
276 }
277
278 if let Some(proj) = q.projection {
279 let mut fields: Vec<ProjectField> = proj
280 .into_iter()
281 .map(|pf| ProjectField {
282 alias: pf.alias,
283 expr: pf.expr,
284 })
285 .collect();
286 let windows = extract_windows(&mut fields);
287 if !windows.is_empty() {
288 node = PlanNode::Window {
289 input: Box::new(node),
290 windows,
291 };
292 }
293 node = PlanNode::Project {
294 input: Box::new(node),
295 fields,
296 };
297 }
298
299 if q.distinct {
300 node = PlanNode::Distinct {
301 input: Box::new(node),
302 };
303 }
304
305 if let Some(agg) = q.aggregation {
306 let provenance_alias = symmetric_provenance_alias(
307 agg.function,
308 agg.argument.as_ref(),
309 agg.mode,
310 &source_aliases,
311 )?;
312 node = PlanNode::Aggregate {
313 input: Box::new(node),
314 function: agg.function,
315 argument: agg.argument,
316 mode: agg.mode,
317 provenance_alias,
318 };
319 }
320
321 Ok(node)
322}
323
324fn plan_nested_query(q: QueryExpr) -> Result<PlanNode, PlanError> {
331 if !q.joins.is_empty() || q.group_by.is_some() || q.aggregation.is_some() || q.distinct {
332 return Err(PlanError::Semantic(
333 "nested projections require a plain aliased table scan (no joins, \
334 group, distinct, or aggregation)"
335 .into(),
336 ));
337 }
338 let parent_alias = q.alias.unwrap_or_else(|| q.source.clone());
339 let mut node = PlanNode::AliasScan {
340 table: q.source,
341 alias: parent_alias.clone(),
342 };
343 if let Some(pred) = q.filter {
344 node = PlanNode::Filter {
345 input: Box::new(node),
346 predicate: pred,
347 };
348 }
349 if let Some(order) = q.order {
350 node = PlanNode::Sort {
351 input: Box::new(node),
352 keys: order
353 .keys
354 .into_iter()
355 .map(|k| SortKey {
356 expr: k.expr,
357 descending: k.descending,
358 })
359 .collect(),
360 };
361 }
362 if let Some(off) = q.offset {
363 node = PlanNode::Offset {
364 input: Box::new(node),
365 count: off,
366 };
367 }
368 if let Some(lim) = q.limit {
369 node = PlanNode::Limit {
370 input: Box::new(node),
371 count: lim,
372 };
373 }
374 let fields = q
375 .projection
376 .expect("plan_nested_query is only called with a projection")
377 .into_iter()
378 .map(|pf| match pf.expr {
379 Expr::NestedQuery(nested) => {
380 let name = pf.alias.ok_or_else(|| {
381 PlanError::Semantic("nested projection field requires a name".into())
382 })?;
383 resolve_nested_projection(name, *nested, &parent_alias)
384 .map(|nested| NestedProjectField::Nested(Box::new(nested)))
385 }
386 expr => Ok(NestedProjectField::Plain(ProjectField {
387 alias: pf.alias,
388 expr,
389 })),
390 })
391 .collect::<Result<Vec<_>, PlanError>>()?;
392 Ok(PlanNode::NestedProject {
393 input: Box::new(node),
394 fields,
395 })
396}
397
398fn resolve_nested_projection(
404 name: String,
405 nested: NestedQuery,
406 parent_alias: &str,
407) -> Result<NestedProjection, PlanError> {
408 resolve_nested_projection_inner(name, nested, parent_alias, true)
409}
410
411fn resolve_nested_projection_inner(
415 name: String,
416 nested: NestedQuery,
417 parent_alias: &str,
418 qualify_parent_key: bool,
419) -> Result<NestedProjection, PlanError> {
420 let mut conjuncts = Vec::new();
421 split_and_chain(nested.filter, &mut conjuncts);
422
423 let correlation_of = |expr: &Expr| -> Option<(String, String)> {
425 let Expr::BinaryOp(left, BinOp::Eq, right) = expr else {
426 return None;
427 };
428 let side = |expr: &Expr| match expr {
429 Expr::QualifiedField { qualifier, field } => Some((qualifier.clone(), field.clone())),
430 _ => None,
431 };
432 let ((lq, lf), (rq, rf)) = (side(left)?, side(right)?);
433 if lq == nested.alias && rq == parent_alias {
434 Some((lf, rf))
435 } else if rq == nested.alias && lq == parent_alias {
436 Some((rf, lf))
437 } else {
438 None
439 }
440 };
441 let mut correlation: Option<(String, String)> = None;
442 let mut residual: Option<Expr> = None;
443 for conjunct in conjuncts {
444 match correlation_of(&conjunct) {
445 Some(keys) if correlation.is_none() => correlation = Some(keys),
446 Some(_) => {
447 return Err(PlanError::Semantic(format!(
448 "nested projection `{name}` links `{child}` to `{parent}` more than \
449 once; exactly one correlation predicate \
450 ({child}.<col> = {parent}.<col>) is supported",
451 child = nested.alias,
452 parent = parent_alias,
453 )))
454 }
455 None => {
456 let rewritten =
457 rewrite_residual_condition(conjunct, &name, &nested.alias, parent_alias)?;
458 residual = Some(match residual {
459 Some(existing) => {
460 Expr::BinaryOp(Box::new(existing), BinOp::And, Box::new(rewritten))
461 }
462 None => rewritten,
463 });
464 }
465 }
466 }
467 let Some((child_key, parent_key)) = correlation else {
468 return Err(PlanError::Semantic(format!(
469 "nested projection `{name}` requires an equi-correlation predicate linking \
470 `{child}` to the outer query ({child}.<col> = {parent}.<col>) somewhere in \
471 its filter",
472 child = nested.alias,
473 parent = parent_alias,
474 )));
475 };
476 let order = nested
477 .order
478 .map(|clause| {
479 clause
480 .keys
481 .into_iter()
482 .map(|key| {
483 let column = match &key.expr {
484 Expr::Field(field) => field.clone(),
485 Expr::QualifiedField { qualifier, field } if *qualifier == nested.alias => {
486 field.clone()
487 }
488 _ => {
489 return Err(PlanError::Semantic(format!(
490 "nested projection `{name}` order keys must be plain \
491 columns of `{child}` (`{child}.<col>` or `.<col>`)",
492 child = nested.alias,
493 )))
494 }
495 };
496 Ok((column, key.descending))
497 })
498 .collect::<Result<Vec<_>, PlanError>>()
499 })
500 .transpose()?
501 .unwrap_or_default();
502 let fields = nested
503 .fields
504 .into_iter()
505 .map(|pf| {
506 if let Expr::NestedQuery(inner) = pf.expr {
507 let inner_name = pf.alias.ok_or_else(|| {
508 PlanError::Semantic(
509 "nested projection field requires a name \
510 (`<name>: <Table> as <alias> ...`)"
511 .into(),
512 )
513 })?;
514 return resolve_nested_projection_inner(inner_name, *inner, &nested.alias, false)
517 .map(|inner| NestedField::Nested(Box::new(inner)));
518 }
519 let column = match &pf.expr {
520 Expr::Field(field) => field.clone(),
521 Expr::QualifiedField { qualifier, field } if *qualifier == nested.alias => {
522 field.clone()
523 }
524 _ => {
525 return Err(PlanError::Semantic(format!(
526 "nested projection `{name}` fields must be plain columns of `{}` \
527 (`{}.<col>` or `.<col>`) or a deeper nested projection",
528 nested.alias, nested.alias
529 )))
530 }
531 };
532 let key = pf.alias.unwrap_or_else(|| column.clone());
533 Ok(NestedField::Scalar { key, column })
534 })
535 .collect::<Result<Vec<_>, PlanError>>()?;
536 Ok(NestedProjection {
537 name,
538 table: nested.source,
539 alias: nested.alias,
540 parent_alias: parent_alias.to_string(),
541 child_key,
542 parent_key: if qualify_parent_key {
543 format!("{parent_alias}.{parent_key}")
544 } else {
545 parent_key
546 },
547 residual,
548 order,
549 limit: nested.limit,
550 offset: nested.offset,
551 offset_before_limit: nested.offset_before_limit,
552 fields,
553 })
554}
555
556fn split_and_chain(expr: Expr, out: &mut Vec<Expr>) {
558 match expr {
559 Expr::BinaryOp(left, BinOp::And, right) => {
560 split_and_chain(*left, out);
561 split_and_chain(*right, out);
562 }
563 other => out.push(other),
564 }
565}
566
567fn rewrite_residual_condition(
573 expr: Expr,
574 name: &str,
575 child_alias: &str,
576 parent_alias: &str,
577) -> Result<Expr, PlanError> {
578 let rewrite = |inner: Box<Expr>| -> Result<Box<Expr>, PlanError> {
579 Ok(Box::new(rewrite_residual_condition(
580 *inner,
581 name,
582 child_alias,
583 parent_alias,
584 )?))
585 };
586 match expr {
587 Expr::QualifiedField { qualifier, field } => {
588 if qualifier == child_alias {
589 Ok(Expr::Field(field))
590 } else if qualifier == parent_alias {
591 Err(PlanError::Semantic(format!(
592 "nested projection `{name}` filter references outer alias \
593 `{parent_alias}` (`{parent_alias}.{field}`) outside the correlation \
594 predicate; move that condition to the outer query's filter"
595 )))
596 } else {
597 Err(PlanError::Semantic(format!(
598 "nested projection `{name}` filter references unknown alias \
599 `{qualifier}`; only columns of `{child_alias}` may be used"
600 )))
601 }
602 }
603 Expr::Field(_) | Expr::Literal(_) | Expr::Param(_) | Expr::ValueLit(_) | Expr::Null => {
604 Ok(expr)
605 }
606 Expr::BinaryOp(left, op, right) => Ok(Expr::BinaryOp(rewrite(left)?, op, rewrite(right)?)),
607 Expr::UnaryOp(op, inner) => Ok(Expr::UnaryOp(op, rewrite(inner)?)),
608 Expr::Coalesce(left, right) => Ok(Expr::Coalesce(rewrite(left)?, rewrite(right)?)),
609 Expr::Cast(inner, ty) => Ok(Expr::Cast(rewrite(inner)?, ty)),
610 Expr::ScalarFunc(func, args) => Ok(Expr::ScalarFunc(
611 func,
612 args.into_iter()
613 .map(|arg| rewrite_residual_condition(arg, name, child_alias, parent_alias))
614 .collect::<Result<Vec<_>, _>>()?,
615 )),
616 Expr::InList {
617 expr,
618 list,
619 negated,
620 } => Ok(Expr::InList {
621 expr: rewrite(expr)?,
622 list: list
623 .into_iter()
624 .map(|item| rewrite_residual_condition(item, name, child_alias, parent_alias))
625 .collect::<Result<Vec<_>, _>>()?,
626 negated,
627 }),
628 Expr::Case { whens, else_expr } => Ok(Expr::Case {
629 whens: whens
630 .into_iter()
631 .map(|(cond, result)| Ok((rewrite(cond)?, rewrite(result)?)))
632 .collect::<Result<Vec<_>, PlanError>>()?,
633 else_expr: else_expr.map(rewrite).transpose()?,
634 }),
635 Expr::JsonPath { base, segments } => Ok(Expr::JsonPath {
636 base: rewrite(base)?,
637 segments,
638 }),
639 Expr::InSubquery { .. } | Expr::ExistsSubquery { .. } => Err(PlanError::Semantic(format!(
640 "nested projection `{name}` filter cannot contain a subquery; \
641 filter the outer query or the child columns directly"
642 ))),
643 Expr::FunctionCall(..) => Err(PlanError::Semantic(format!(
644 "nested projection `{name}` filter cannot contain an aggregate function"
645 ))),
646 Expr::Window { .. } => Err(PlanError::Semantic(format!(
647 "nested projection `{name}` filter cannot contain a window function"
648 ))),
649 Expr::NestedQuery(_) => Err(PlanError::Semantic(format!(
650 "nested projection `{name}` filter cannot contain another nested projection"
651 ))),
652 }
653}
654
655fn plan_joined_query(mut q: QueryExpr) -> Result<PlanNode, PlanError> {
677 let primary_alias = q.alias.clone().unwrap_or_else(|| q.source.clone());
678 let mut aliases = std::collections::HashSet::new();
679 aliases.insert(primary_alias.clone());
680 let mut node = PlanNode::AliasScan {
681 table: q.source.clone(),
682 alias: primary_alias,
683 };
684
685 for join in q.joins {
686 let right_alias = join.alias.unwrap_or_else(|| join.source.clone());
687 if !aliases.insert(right_alias.clone()) {
688 return Err(ParseError::Syntax {
689 message: format!(
690 "duplicate source alias `{right_alias}` in join; every joined source needs a unique alias"
691 ),
692 }
693 .into());
694 }
695 let right = PlanNode::AliasScan {
696 table: join.source,
697 alias: right_alias,
698 };
699 match join.kind {
700 JoinKind::Inner | JoinKind::LeftOuter | JoinKind::Cross => {
701 node = PlanNode::NestedLoopJoin {
702 left: Box::new(node),
703 right: Box::new(right),
704 on: join.on,
705 kind: join.kind,
706 };
707 }
708 JoinKind::RightOuter => {
709 node = PlanNode::NestedLoopJoin {
711 left: Box::new(right),
712 right: Box::new(node),
713 on: join.on,
714 kind: JoinKind::LeftOuter,
715 };
716 }
717 }
718 }
719
720 if let Some(pred) = q.filter {
721 node = PlanNode::Filter {
722 input: Box::new(node),
723 predicate: pred,
724 };
725 }
726
727 if q.group_by.is_none() {
728 if let Some(order) = q.order.take() {
729 node = PlanNode::Sort {
730 input: Box::new(node),
731 keys: order
732 .keys
733 .into_iter()
734 .map(|k| SortKey {
735 expr: k.expr,
736 descending: k.descending,
737 })
738 .collect(),
739 };
740 }
741 }
742
743 if let Some(group) = q.group_by {
745 let mut grouped_order = q.order;
746 let mut proj_fields: Vec<ProjectField> = q
747 .projection
748 .map(|proj| {
749 proj.into_iter()
750 .map(|pf| ProjectField {
751 alias: pf.alias,
752 expr: pf.expr,
753 })
754 .collect()
755 })
756 .unwrap_or_default();
757 let mut having = group.having;
758 let aggregates = extract_aggregates(&mut proj_fields, &mut having, &aliases)?;
759 rewrite_group_order_keys(grouped_order.as_mut(), &proj_fields, &group.keys);
760 rewrite_group_key_references(&mut proj_fields, &mut having, &group.keys);
761
762 node = PlanNode::GroupBy {
763 input: Box::new(node),
764 keys: group.keys,
765 aggregates,
766 having,
767 };
768
769 if !proj_fields.is_empty() {
770 node = PlanNode::Project {
771 input: Box::new(node),
772 fields: proj_fields,
773 };
774 }
775 if let Some(order) = grouped_order {
776 node = PlanNode::Sort {
777 input: Box::new(node),
778 keys: order
779 .keys
780 .into_iter()
781 .map(|key| SortKey {
782 expr: key.expr,
783 descending: key.descending,
784 })
785 .collect(),
786 };
787 }
788 if let Some(off) = q.offset {
793 node = PlanNode::Offset {
794 input: Box::new(node),
795 count: off,
796 };
797 }
798 if let Some(lim) = q.limit {
799 node = PlanNode::Limit {
800 input: Box::new(node),
801 count: lim,
802 };
803 }
804 if q.distinct {
805 node = PlanNode::Distinct {
806 input: Box::new(node),
807 };
808 }
809 return Ok(node);
810 }
811
812 if let Some(off) = q.offset {
816 node = PlanNode::Offset {
817 input: Box::new(node),
818 count: off,
819 };
820 }
821
822 if let Some(lim) = q.limit {
823 node = PlanNode::Limit {
824 input: Box::new(node),
825 count: lim,
826 };
827 }
828
829 if let Some(proj) = q.projection {
830 let mut fields: Vec<ProjectField> = proj
831 .into_iter()
832 .map(|pf| ProjectField {
833 alias: pf.alias,
834 expr: pf.expr,
835 })
836 .collect();
837 let windows = extract_windows(&mut fields);
838 if !windows.is_empty() {
839 node = PlanNode::Window {
840 input: Box::new(node),
841 windows,
842 };
843 }
844 node = PlanNode::Project {
845 input: Box::new(node),
846 fields,
847 };
848 }
849
850 if q.distinct {
851 node = PlanNode::Distinct {
852 input: Box::new(node),
853 };
854 }
855
856 if let Some(agg) = q.aggregation {
857 let provenance_alias =
858 symmetric_provenance_alias(agg.function, agg.argument.as_ref(), agg.mode, &aliases)?;
859 node = PlanNode::Aggregate {
860 input: Box::new(node),
861 function: agg.function,
862 argument: agg.argument,
863 mode: agg.mode,
864 provenance_alias,
865 };
866 }
867
868 Ok(node)
869}
870
871fn plan_insert(ins: InsertExpr) -> Result<PlanNode, PlanError> {
872 Ok(PlanNode::Insert {
873 table: ins.target,
874 rows: ins.rows,
875 returning: ins.returning,
876 })
877}
878
879fn plan_update(upd: UpdateExpr) -> Result<PlanNode, PlanError> {
880 let source = match upd.filter {
885 Some(pred) => match try_extract_eq_index_key(&upd.source, &pred) {
886 Some(index_scan) => index_scan,
887 None => match try_extract_range_index_keys(&upd.source, &pred) {
888 Some(range_scan) => range_scan,
889 None => PlanNode::Filter {
890 input: Box::new(PlanNode::SeqScan {
891 table: upd.source.clone(),
892 }),
893 predicate: pred,
894 },
895 },
896 },
897 None => PlanNode::SeqScan {
898 table: upd.source.clone(),
899 },
900 };
901 Ok(PlanNode::Update {
902 input: Box::new(source),
903 table: upd.source,
904 assignments: upd.assignments,
905 returning: upd.returning,
906 })
907}
908
909fn plan_delete(del: DeleteExpr) -> Result<PlanNode, PlanError> {
910 let source = match del.filter {
911 Some(pred) => match try_extract_eq_index_key(&del.source, &pred) {
912 Some(index_scan) => index_scan,
913 None => match try_extract_range_index_keys(&del.source, &pred) {
914 Some(range_scan) => range_scan,
915 None => PlanNode::Filter {
916 input: Box::new(PlanNode::SeqScan {
917 table: del.source.clone(),
918 }),
919 predicate: pred,
920 },
921 },
922 },
923 None => PlanNode::SeqScan {
924 table: del.source.clone(),
925 },
926 };
927 Ok(PlanNode::Delete {
928 input: Box::new(source),
929 table: del.source,
930 returning: del.returning,
931 })
932}
933
934fn plan_upsert(ups: UpsertExpr) -> Result<PlanNode, PlanError> {
935 Ok(PlanNode::Upsert {
936 table: ups.target,
937 key_column: ups.key_column,
938 assignments: ups.assignments,
939 on_conflict: ups.on_conflict,
940 })
941}
942
943fn plan_create_type(ct: CreateTypeExpr) -> Result<PlanNode, PlanError> {
944 let fields = ct
945 .fields
946 .into_iter()
947 .map(|f| crate::plan::CreateField {
948 name: f.name,
949 type_name: f.type_name,
950 required: f.required,
951 unique: f.unique,
952 default: f.default,
953 auto: f.auto,
954 })
955 .collect();
956 Ok(PlanNode::CreateTable {
957 name: ct.name,
958 fields,
959 if_not_exists: ct.if_not_exists,
960 })
961}
962
963pub(crate) fn try_extract_eq_index_key(table: &str, pred: &Expr) -> Option<PlanNode> {
973 let (lhs, op, rhs) = match pred {
974 Expr::BinaryOp(lhs, op, rhs) => (lhs.as_ref(), *op, rhs.as_ref()),
975 _ => return None,
976 };
977 if op != BinOp::Eq {
978 return None;
979 }
980 match (lhs, rhs) {
981 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some(PlanNode::ExprIndexScan {
982 table: table.to_string(),
983 path: stored_json_path(path)?,
984 key: rhs.clone(),
985 }),
986 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some(PlanNode::ExprIndexScan {
987 table: table.to_string(),
988 path: stored_json_path(path)?,
989 key: lhs.clone(),
990 }),
991 (Expr::Field(name), Expr::Literal(_)) => Some(PlanNode::IndexScan {
992 table: table.to_string(),
993 column: name.clone(),
994 key: rhs.clone(),
995 }),
996 (Expr::Literal(_), Expr::Field(name)) => Some(PlanNode::IndexScan {
997 table: table.to_string(),
998 column: name.clone(),
999 key: lhs.clone(),
1000 }),
1001 _ => None,
1002 }
1003}
1004
1005fn stored_json_path(expr: &Expr) -> Option<StoredJsonPathV1> {
1006 JsonPathIdentityV1::from_expr(expr)?.bind_table_local(None)
1007}
1008
1009pub(crate) fn extract_single_bound(pred: &Expr) -> Option<RangeBound> {
1012 let (lhs, op, rhs) = match pred {
1013 Expr::BinaryOp(lhs, op, rhs) => (lhs.as_ref(), *op, rhs.as_ref()),
1014 _ => return None,
1015 };
1016 match op {
1017 BinOp::Gt => match (lhs, rhs) {
1019 (Expr::Field(name), Expr::Literal(_)) => Some((
1020 RangeTarget::Column(name.clone()),
1021 Some((rhs.clone(), false)),
1022 None,
1023 )),
1024 (Expr::Literal(_), Expr::Field(name)) => {
1025 Some((
1027 RangeTarget::Column(name.clone()),
1028 None,
1029 Some((lhs.clone(), false)),
1030 ))
1031 }
1032 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1033 RangeTarget::JsonPath(stored_json_path(path)?),
1034 Some((rhs.clone(), false)),
1035 None,
1036 )),
1037 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1038 RangeTarget::JsonPath(stored_json_path(path)?),
1039 None,
1040 Some((lhs.clone(), false)),
1041 )),
1042 _ => None,
1043 },
1044 BinOp::Gte => match (lhs, rhs) {
1046 (Expr::Field(name), Expr::Literal(_)) => Some((
1047 RangeTarget::Column(name.clone()),
1048 Some((rhs.clone(), true)),
1049 None,
1050 )),
1051 (Expr::Literal(_), Expr::Field(name)) => Some((
1052 RangeTarget::Column(name.clone()),
1053 None,
1054 Some((lhs.clone(), true)),
1055 )),
1056 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1057 RangeTarget::JsonPath(stored_json_path(path)?),
1058 Some((rhs.clone(), true)),
1059 None,
1060 )),
1061 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1062 RangeTarget::JsonPath(stored_json_path(path)?),
1063 None,
1064 Some((lhs.clone(), true)),
1065 )),
1066 _ => None,
1067 },
1068 BinOp::Lt => match (lhs, rhs) {
1070 (Expr::Field(name), Expr::Literal(_)) => Some((
1071 RangeTarget::Column(name.clone()),
1072 None,
1073 Some((rhs.clone(), false)),
1074 )),
1075 (Expr::Literal(_), Expr::Field(name)) => Some((
1076 RangeTarget::Column(name.clone()),
1077 Some((lhs.clone(), false)),
1078 None,
1079 )),
1080 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1081 RangeTarget::JsonPath(stored_json_path(path)?),
1082 None,
1083 Some((rhs.clone(), false)),
1084 )),
1085 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1086 RangeTarget::JsonPath(stored_json_path(path)?),
1087 Some((lhs.clone(), false)),
1088 None,
1089 )),
1090 _ => None,
1091 },
1092 BinOp::Lte => match (lhs, rhs) {
1094 (Expr::Field(name), Expr::Literal(_)) => Some((
1095 RangeTarget::Column(name.clone()),
1096 None,
1097 Some((rhs.clone(), true)),
1098 )),
1099 (Expr::Literal(_), Expr::Field(name)) => Some((
1100 RangeTarget::Column(name.clone()),
1101 Some((lhs.clone(), true)),
1102 None,
1103 )),
1104 (path @ Expr::JsonPath { .. }, Expr::Literal(_)) => Some((
1105 RangeTarget::JsonPath(stored_json_path(path)?),
1106 None,
1107 Some((rhs.clone(), true)),
1108 )),
1109 (Expr::Literal(_), path @ Expr::JsonPath { .. }) => Some((
1110 RangeTarget::JsonPath(stored_json_path(path)?),
1111 Some((lhs.clone(), true)),
1112 None,
1113 )),
1114 _ => None,
1115 },
1116 _ => None,
1117 }
1118}
1119
1120fn try_extract_range_index_keys(table: &str, pred: &Expr) -> Option<PlanNode> {
1143 if let Expr::BinaryOp(lhs, BinOp::And, rhs) = pred {
1146 if let (Some((col1, s1, e1)), Some((col2, s2, e2))) =
1147 (extract_single_bound(lhs), extract_single_bound(rhs))
1148 {
1149 if col1 == col2 {
1150 if let (Some(start), None, None, Some(end)) = (s1, e1, s2, e2) {
1151 return Some(range_scan_for_target(table, col1, Some(start), Some(end)));
1152 }
1153 }
1154 }
1155 }
1156
1157 if let Some((col, start, end)) = extract_single_bound(pred) {
1159 return Some(range_scan_for_target(table, col, start, end));
1160 }
1161
1162 None
1163}
1164
1165pub(crate) fn range_scan_for_target(
1166 table: &str,
1167 target: RangeTarget,
1168 start: Option<(Expr, bool)>,
1169 end: Option<(Expr, bool)>,
1170) -> PlanNode {
1171 match target {
1172 RangeTarget::Column(column) => PlanNode::RangeScan {
1173 table: table.to_string(),
1174 column,
1175 start,
1176 end,
1177 },
1178 RangeTarget::JsonPath(path) => PlanNode::ExprRangeScan {
1179 table: table.to_string(),
1180 path,
1181 start,
1182 end,
1183 },
1184 }
1185}
1186
1187fn try_extract_ordered_expr_index_scan(query: &QueryExpr) -> Option<PlanNode> {
1192 if query.alias.is_some()
1193 || !query.joins.is_empty()
1194 || query.filter.is_some()
1195 || query.group_by.is_some()
1196 || query.distinct
1197 || query.aggregation.is_some()
1198 || query.projection.as_ref().is_some_and(|fields| {
1199 fields
1200 .iter()
1201 .any(|field| matches!(field.expr, Expr::Window { .. }))
1202 })
1203 {
1204 return None;
1205 }
1206 let order = query.order.as_ref()?;
1207 let [key] = order.keys.as_slice() else {
1208 return None;
1209 };
1210 let path = stored_json_path(&key.expr)?;
1211 let limit = query.limit.as_ref()?;
1212 if !matches!(limit, Expr::Literal(Literal::Int(value)) if *value >= 0) {
1213 return None;
1214 }
1215 if !query
1216 .offset
1217 .as_ref()
1218 .is_none_or(|offset| matches!(offset, Expr::Literal(Literal::Int(value)) if *value >= 0))
1219 {
1220 return None;
1221 }
1222 Some(PlanNode::OrderedExprIndexScan {
1223 table: query.source.clone(),
1224 path,
1225 descending: key.descending,
1226 limit: limit.clone(),
1227 offset: query.offset.clone(),
1228 })
1229}
1230
1231fn extract_windows(proj_fields: &mut [ProjectField]) -> Vec<WindowDef> {
1236 let mut defs = Vec::new();
1237 let mut counter = 0usize;
1238 for f in proj_fields.iter_mut() {
1239 if let Expr::Window {
1240 function,
1241 args,
1242 mode,
1243 partition_by,
1244 order_by,
1245 } = &f.expr
1246 {
1247 let output_name = format!("__win_{counter}");
1248 defs.push(WindowDef {
1249 function: *function,
1250 args: args.clone(),
1251 mode: *mode,
1252 partition_by: partition_by.clone(),
1253 order_by: order_by
1254 .iter()
1255 .map(|k| SortKey {
1256 expr: k.expr.clone(),
1257 descending: k.descending,
1258 })
1259 .collect(),
1260 output_name: output_name.clone(),
1261 });
1262 f.expr = Expr::Field(output_name);
1263 counter += 1;
1264 }
1265 }
1266 defs
1267}
1268
1269fn extract_aggregates(
1275 proj_fields: &mut [ProjectField],
1276 having: &mut Option<Expr>,
1277 source_aliases: &std::collections::HashSet<String>,
1278) -> Result<Vec<GroupAgg>, PlanError> {
1279 let mut aggs: Vec<GroupAgg> = Vec::new();
1280 let mut counter = 0usize;
1281 for f in proj_fields.iter_mut() {
1282 rewrite_agg_expr(&mut f.expr, &mut aggs, &mut counter, source_aliases)?;
1283 }
1284 if let Some(h) = having {
1285 rewrite_agg_expr(h, &mut aggs, &mut counter, source_aliases)?;
1286 }
1287 Ok(aggs)
1288}
1289
1290fn rewrite_group_key_references(
1291 fields: &mut [ProjectField],
1292 having: &mut Option<Expr>,
1293 keys: &[GroupKey],
1294) {
1295 for field in fields {
1296 rewrite_group_key_expr(&mut field.expr, keys);
1297 }
1298 if let Some(having) = having {
1299 rewrite_group_key_expr(having, keys);
1300 }
1301}
1302
1303fn rewrite_group_order_keys(
1304 order: Option<&mut OrderClause>,
1305 projection: &[ProjectField],
1306 keys: &[GroupKey],
1307) {
1308 let Some(order) = order else {
1309 return;
1310 };
1311 for order_key in &mut order.keys {
1312 let Some(group_key) = keys.iter().find(|key| key.expr == order_key.expr) else {
1313 continue;
1314 };
1315 let projected_name = projection
1316 .iter()
1317 .find(|field| field.expr == group_key.expr)
1318 .and_then(|field| field.alias.clone())
1319 .unwrap_or_else(|| group_key.output_name());
1320 order_key.expr = Expr::Field(projected_name);
1321 }
1322}
1323
1324fn rewrite_group_key_expr(expr: &mut Expr, keys: &[GroupKey]) {
1325 if let Some(key) = keys.iter().find(|key| key.expr == *expr) {
1326 *expr = Expr::Field(key.output_name());
1327 return;
1328 }
1329 match expr {
1330 Expr::FunctionCall(..) => {}
1334 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1335 rewrite_group_key_expr(left, keys);
1336 rewrite_group_key_expr(right, keys);
1337 }
1338 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) => rewrite_group_key_expr(inner, keys),
1339 Expr::ScalarFunc(_, args) => {
1340 for arg in args {
1341 rewrite_group_key_expr(arg, keys);
1342 }
1343 }
1344 Expr::InList { expr, list, .. } => {
1345 rewrite_group_key_expr(expr, keys);
1346 for item in list {
1347 rewrite_group_key_expr(item, keys);
1348 }
1349 }
1350 Expr::Case { whens, else_expr } => {
1351 for (condition, result) in whens {
1352 rewrite_group_key_expr(condition, keys);
1353 rewrite_group_key_expr(result, keys);
1354 }
1355 if let Some(expr) = else_expr {
1356 rewrite_group_key_expr(expr, keys);
1357 }
1358 }
1359 _ => {}
1360 }
1361}
1362
1363fn rewrite_agg_expr(
1364 expr: &mut Expr,
1365 aggs: &mut Vec<GroupAgg>,
1366 counter: &mut usize,
1367 source_aliases: &std::collections::HashSet<String>,
1368) -> Result<(), PlanError> {
1369 match expr {
1370 Expr::FunctionCall(func, inner, mode) => {
1371 let output = find_or_insert_agg(aggs, *func, inner, *mode, counter, source_aliases)?;
1372 *expr = Expr::Field(output);
1373 }
1374 Expr::BinaryOp(l, _, r) => {
1375 rewrite_agg_expr(l, aggs, counter, source_aliases)?;
1376 rewrite_agg_expr(r, aggs, counter, source_aliases)?;
1377 }
1378 Expr::UnaryOp(_, inner) => rewrite_agg_expr(inner, aggs, counter, source_aliases)?,
1379 Expr::Coalesce(l, r) => {
1380 rewrite_agg_expr(l, aggs, counter, source_aliases)?;
1381 rewrite_agg_expr(r, aggs, counter, source_aliases)?;
1382 }
1383 Expr::InList { expr: e, list, .. } => {
1384 rewrite_agg_expr(e, aggs, counter, source_aliases)?;
1385 for item in list {
1386 rewrite_agg_expr(item, aggs, counter, source_aliases)?;
1387 }
1388 }
1389 Expr::InSubquery { expr: e, .. } => {
1390 rewrite_agg_expr(e, aggs, counter, source_aliases)?;
1391 }
1392 _ => {}
1393 }
1394 Ok(())
1395}
1396
1397fn find_or_insert_agg(
1398 aggs: &mut Vec<GroupAgg>,
1399 func: AggFunc,
1400 argument: &Expr,
1401 mode: AggregateMode,
1402 counter: &mut usize,
1403 source_aliases: &std::collections::HashSet<String>,
1404) -> Result<String, PlanError> {
1405 for existing in aggs.iter() {
1406 if existing.function == func && existing.argument == *argument && existing.mode == mode {
1407 return Ok(existing.output_name.clone());
1408 }
1409 }
1410 let provenance_alias = symmetric_provenance_alias(func, Some(argument), mode, source_aliases)?;
1411 let output_name = format!("__agg_{counter}");
1412 aggs.push(GroupAgg {
1413 function: func,
1414 argument: argument.clone(),
1415 mode,
1416 provenance_alias,
1417 output_name: output_name.clone(),
1418 });
1419 *counter += 1;
1420 Ok(output_name)
1421}
1422
1423fn symmetric_provenance_alias(
1424 function: AggFunc,
1425 argument: Option<&Expr>,
1426 mode: AggregateMode,
1427 source_aliases: &std::collections::HashSet<String>,
1428) -> Result<Option<String>, PlanError> {
1429 if mode == AggregateMode::Raw
1430 || source_aliases.len() < 2
1431 || !matches!(function, AggFunc::Sum | AggFunc::Avg | AggFunc::Count)
1432 || (function == AggFunc::Count
1433 && argument.is_none_or(|argument| matches!(argument, Expr::Field(name) if name == "*")))
1434 {
1435 return Ok(None);
1436 }
1437 let Some(argument) = argument else {
1438 return Err(symmetric_aggregate_error(
1439 function,
1440 "does not reference a source row",
1441 ));
1442 };
1443
1444 let mut qualified = std::collections::HashSet::new();
1445 let mut has_unqualified = false;
1446 collect_expression_sources(argument, &mut qualified, &mut has_unqualified);
1447
1448 for alias in &qualified {
1449 if !source_aliases.contains(alias) {
1450 return Err(symmetric_aggregate_error(
1451 function,
1452 &format!("references unknown source alias '{alias}'"),
1453 ));
1454 }
1455 }
1456 if has_unqualified {
1457 if source_aliases.len() != 1 {
1458 return Err(symmetric_aggregate_error(
1459 function,
1460 "contains an ambiguous unqualified field",
1461 ));
1462 }
1463 qualified.extend(source_aliases.iter().cloned());
1464 }
1465 match qualified.len() {
1466 1 => Ok(qualified.into_iter().next()),
1467 0 => Err(symmetric_aggregate_error(
1468 function,
1469 "does not reference a source row",
1470 )),
1471 _ => Err(symmetric_aggregate_error(
1472 function,
1473 "references multiple source aliases",
1474 )),
1475 }
1476}
1477
1478fn symmetric_aggregate_error(function: AggFunc, reason: &str) -> PlanError {
1479 let name = format!("{function:?}").to_lowercase();
1480 PlanError::Semantic(format!(
1481 "symmetric {name} expression {reason}; reference exactly one source alias or use {name}(raw ...)"
1482 ))
1483}
1484
1485fn collect_expression_sources(
1486 expr: &Expr,
1487 qualified: &mut std::collections::HashSet<String>,
1488 has_unqualified: &mut bool,
1489) {
1490 match expr {
1491 Expr::Field(name) if name != "*" => *has_unqualified = true,
1492 Expr::QualifiedField { qualifier, .. } => {
1493 qualified.insert(qualifier.clone());
1494 }
1495 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1496 collect_expression_sources(left, qualified, has_unqualified);
1497 collect_expression_sources(right, qualified, has_unqualified);
1498 }
1499 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
1500 collect_expression_sources(inner, qualified, has_unqualified);
1501 }
1502 Expr::ScalarFunc(_, args) => {
1503 for argument in args {
1504 collect_expression_sources(argument, qualified, has_unqualified);
1505 }
1506 }
1507 Expr::InList { expr, list, .. } => {
1508 collect_expression_sources(expr, qualified, has_unqualified);
1509 for item in list {
1510 collect_expression_sources(item, qualified, has_unqualified);
1511 }
1512 }
1513 Expr::InSubquery { expr, .. } => {
1514 collect_expression_sources(expr, qualified, has_unqualified);
1515 }
1516 Expr::Case { whens, else_expr } => {
1517 for (condition, result) in whens {
1518 collect_expression_sources(condition, qualified, has_unqualified);
1519 collect_expression_sources(result, qualified, has_unqualified);
1520 }
1521 if let Some(expr) = else_expr {
1522 collect_expression_sources(expr, qualified, has_unqualified);
1523 }
1524 }
1525 Expr::Window {
1526 args,
1527 partition_by,
1528 order_by,
1529 ..
1530 } => {
1531 for expr in args.iter().chain(partition_by) {
1532 collect_expression_sources(expr, qualified, has_unqualified);
1533 }
1534 for key in order_by {
1535 collect_expression_sources(&key.expr, qualified, has_unqualified);
1536 }
1537 }
1538 Expr::FunctionCall(_, inner, _) => {
1539 collect_expression_sources(inner, qualified, has_unqualified);
1540 }
1541 Expr::ExistsSubquery { .. }
1542 | Expr::Field(_)
1543 | Expr::Literal(_)
1544 | Expr::Param(_)
1545 | Expr::ValueLit(_)
1546 | Expr::Null
1547 | Expr::NestedQuery(_) => {}
1548 }
1549}
1550
1551#[cfg(test)]
1552mod tests {
1553 use super::*;
1554 use crate::plan::PlanNode;
1555
1556 #[test]
1557 fn test_plan_simple_scan() {
1558 let plan = plan("User").unwrap();
1559 assert!(matches!(plan, PlanNode::SeqScan { table } if table == "User"));
1560 }
1561
1562 #[test]
1563 fn test_plan_filter() {
1564 let plan = plan("User filter .age > 30").unwrap();
1565 assert!(matches!(plan, PlanNode::RangeScan { .. }));
1566 }
1567
1568 #[test]
1569 fn test_plan_filter_with_projection() {
1570 let plan = plan("User filter .age > 30 { name, email }").unwrap();
1571 assert!(matches!(plan, PlanNode::Project { .. }));
1572 }
1573
1574 #[test]
1575 fn test_plan_insert() {
1576 let plan = plan(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
1577 assert!(matches!(plan, PlanNode::Insert { .. }));
1578 }
1579
1580 #[test]
1581 fn test_plan_order_limit() {
1582 let plan = plan("User order .name limit 10").unwrap();
1583 match plan {
1584 PlanNode::Limit { input, .. } => {
1585 assert!(matches!(*input, PlanNode::Sort { .. }));
1586 }
1587 _ => panic!("expected Limit(Sort(SeqScan))"),
1588 }
1589 }
1590
1591 #[test]
1592 fn test_plan_count() {
1593 let plan = plan("count(User)").unwrap();
1594 assert!(matches!(plan, PlanNode::Aggregate { .. }));
1595 }
1596
1597 #[test]
1598 fn single_source_aggregates_do_not_request_provenance() {
1599 for query in [
1600 "sum(User { .amount })",
1601 "avg(User { .amount })",
1602 "count(User { .amount })",
1603 ] {
1604 match plan(query).unwrap() {
1605 PlanNode::Aggregate {
1606 provenance_alias, ..
1607 } => assert!(
1608 provenance_alias.is_none(),
1609 "unexpected provenance for {query}"
1610 ),
1611 other => panic!("expected Aggregate for {query}, got {other:?}"),
1612 }
1613 }
1614
1615 match plan("User group .dept { total: sum(.amount) }").unwrap() {
1616 PlanNode::Project { input, .. } => match *input {
1617 PlanNode::GroupBy { aggregates, .. } => {
1618 assert!(aggregates[0].provenance_alias.is_none());
1619 }
1620 other => panic!("expected GroupBy, got {other:?}"),
1621 },
1622 other => panic!("expected Project(GroupBy), got {other:?}"),
1623 }
1624 }
1625
1626 #[test]
1627 fn join_provenance_is_limited_to_fanout_sensitive_aggregates() {
1628 let base = "Account as a join Entry as e on a.id = e.account_id group a.dept";
1629 for (function, expects_provenance) in [
1630 ("sum(a.balance)", true),
1631 ("avg(a.balance)", true),
1632 ("count(a.balance)", true),
1633 ("min(a.balance)", false),
1634 ("max(a.balance)", false),
1635 ("count(distinct a.balance)", false),
1636 ("count(*)", false),
1637 ] {
1638 let query = format!("{base} {{ value: {function} }}");
1639 match plan(&query).unwrap() {
1640 PlanNode::Project { input, .. } => match *input {
1641 PlanNode::GroupBy { aggregates, .. } => assert_eq!(
1642 aggregates[0].provenance_alias.as_deref(),
1643 expects_provenance.then_some("a"),
1644 "unexpected provenance selection for {function}"
1645 ),
1646 other => panic!("expected GroupBy for {function}, got {other:?}"),
1647 },
1648 other => panic!("expected Project(GroupBy) for {function}, got {other:?}"),
1649 }
1650 }
1651 }
1652
1653 #[test]
1654 fn test_plan_eq_becomes_index_scan() {
1655 let plan = plan("User filter .id = 42").unwrap();
1658 match plan {
1659 PlanNode::IndexScan { table, column, key } => {
1660 assert_eq!(table, "User");
1661 assert_eq!(column, "id");
1662 assert!(matches!(key, Expr::Literal(Literal::Int(42))));
1663 }
1664 other => panic!("expected IndexScan, got {other:?}"),
1665 }
1666 }
1667
1668 #[test]
1669 fn test_plan_eq_reversed_becomes_index_scan() {
1670 let plan = plan(r#"User filter "NYC" = .city"#).unwrap();
1672 assert!(matches!(plan, PlanNode::IndexScan { .. }));
1673 }
1674
1675 #[test]
1676 fn json_path_equality_and_reversed_equality_are_speculative_expression_scans() {
1677 for query in ["Post filter .data->age = 21", "Post filter 21 = .data->age"] {
1678 match plan(query).unwrap() {
1679 PlanNode::ExprIndexScan { table, path, key } => {
1680 assert_eq!(table, "Post");
1681 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1682 assert!(matches!(key, Expr::Literal(Literal::Int(21))));
1683 }
1684 other => panic!("expected ExprIndexScan for `{query}`, got {other:?}"),
1685 }
1686 }
1687 }
1688
1689 #[test]
1690 fn json_path_range_and_same_path_compound_bounds_are_speculative_scans() {
1691 for query in ["Post filter .data->age > 18", "Post filter 18 < .data->age"] {
1692 match plan(query).unwrap() {
1693 PlanNode::ExprRangeScan {
1694 path, start, end, ..
1695 } => {
1696 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1697 assert!(start.is_some());
1698 assert!(end.is_none());
1699 }
1700 other => panic!("expected ExprRangeScan for `{query}`, got {other:?}"),
1701 }
1702 }
1703
1704 match plan("Post filter .data->age >= 18 and .data->age < 65").unwrap() {
1705 PlanNode::ExprRangeScan {
1706 path, start, end, ..
1707 } => {
1708 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1709 assert_eq!(start, Some((Expr::Literal(Literal::Int(18)), true)));
1710 assert_eq!(end, Some((Expr::Literal(Literal::Int(65)), false)));
1711 }
1712 other => panic!("expected bounded ExprRangeScan, got {other:?}"),
1713 }
1714
1715 assert!(matches!(
1716 plan("Post filter .data->age >= 18 and .data->score < 65").unwrap(),
1717 PlanNode::Filter { .. }
1718 ));
1719 }
1720
1721 #[test]
1722 fn exact_single_path_order_limit_uses_ordered_expression_scan() {
1723 match plan("Post order .data->age desc limit 10 offset 2 { .id }").unwrap() {
1724 PlanNode::Project { input, .. } => match *input {
1725 PlanNode::OrderedExprIndexScan {
1726 table,
1727 path,
1728 descending,
1729 limit,
1730 offset,
1731 } => {
1732 assert_eq!(table, "Post");
1733 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1734 assert!(descending);
1735 assert_eq!(limit, Expr::Literal(Literal::Int(10)));
1736 assert_eq!(offset, Some(Expr::Literal(Literal::Int(2))));
1737 }
1738 other => panic!("expected OrderedExprIndexScan, got {other:?}"),
1739 },
1740 other => panic!("expected Project(OrderedExprIndexScan), got {other:?}"),
1741 }
1742 }
1743
1744 #[test]
1745 fn incompatible_path_order_shapes_keep_generic_sort() {
1746 for query in [
1747 "Post order .data->age",
1748 "Post order .data->age, .id limit 10",
1749 "Post filter .data->active = true order .data->age limit 10",
1750 "Post order .data->age limit .id",
1751 ] {
1752 let planned = plan(query).unwrap();
1753 assert!(
1754 !plan_contains_ordered_expr_scan(&planned),
1755 "`{query}` must remain on the generic pipeline: {planned:?}"
1756 );
1757 }
1758 }
1759
1760 fn plan_contains_ordered_expr_scan(plan: &PlanNode) -> bool {
1761 match plan {
1762 PlanNode::OrderedExprIndexScan { .. } => true,
1763 PlanNode::Filter { input, .. }
1764 | PlanNode::Project { input, .. }
1765 | PlanNode::Sort { input, .. }
1766 | PlanNode::Limit { input, .. }
1767 | PlanNode::Offset { input, .. }
1768 | PlanNode::Aggregate { input, .. }
1769 | PlanNode::Distinct { input }
1770 | PlanNode::GroupBy { input, .. }
1771 | PlanNode::Update { input, .. }
1772 | PlanNode::Delete { input, .. }
1773 | PlanNode::Window { input, .. }
1774 | PlanNode::Explain { input } => plan_contains_ordered_expr_scan(input),
1775 PlanNode::NestedLoopJoin { left, right, .. } | PlanNode::Union { left, right, .. } => {
1776 plan_contains_ordered_expr_scan(left) || plan_contains_ordered_expr_scan(right)
1777 }
1778 _ => false,
1779 }
1780 }
1781
1782 #[test]
1783 fn test_plan_non_eq_stays_filter() {
1784 let plan = plan("User filter .age > 30").unwrap();
1786 match plan {
1787 PlanNode::RangeScan {
1788 column, start, end, ..
1789 } => {
1790 assert_eq!(column, "age");
1791 assert!(start.is_some(), "expected lower bound");
1792 assert!(end.is_none(), "expected no upper bound");
1793 let (_, inclusive) = start.unwrap();
1794 assert!(!inclusive, "expected exclusive lower bound for >");
1795 }
1796 other => panic!("expected RangeScan, got {other:?}"),
1797 }
1798 }
1799
1800 #[test]
1801 fn test_plan_index_scan_with_projection() {
1802 let plan = plan("User filter .id = 1 { .name }").unwrap();
1804 match plan {
1805 PlanNode::Project { input, .. } => {
1806 assert!(matches!(*input, PlanNode::IndexScan { .. }));
1807 }
1808 other => panic!("expected Project(IndexScan), got {other:?}"),
1809 }
1810 }
1811
1812 #[test]
1813 fn test_plan_update_by_pk_becomes_index_scan() {
1814 let plan = plan("User filter .id = 42 update { age := 31 }").unwrap();
1817 match plan {
1818 PlanNode::Update { input, .. } => {
1819 assert!(
1820 matches!(*input, PlanNode::IndexScan { .. }),
1821 "expected Update(IndexScan), got {input:?}"
1822 );
1823 }
1824 other => panic!("expected Update, got {other:?}"),
1825 }
1826 }
1827
1828 #[test]
1829 fn test_plan_update_range_stays_range_scan() {
1830 let plan = plan("User filter .age > 30 update { age := 31 }").unwrap();
1831 match plan {
1832 PlanNode::Update { input, .. } => {
1833 assert!(
1834 matches!(*input, PlanNode::RangeScan { .. }),
1835 "expected Update(RangeScan), got {input:?}"
1836 );
1837 }
1838 other => panic!("expected Update, got {other:?}"),
1839 }
1840 }
1841
1842 #[test]
1843 fn test_plan_delete_by_pk_becomes_index_scan() {
1844 let plan = plan("User filter .id = 7 delete").unwrap();
1845 match plan {
1846 PlanNode::Delete { input, .. } => {
1847 assert!(matches!(*input, PlanNode::IndexScan { .. }));
1848 }
1849 other => panic!("expected Delete, got {other:?}"),
1850 }
1851 }
1852
1853 #[test]
1854 fn test_plan_inner_join_builds_nested_loop() {
1855 let plan = plan("User as u join Order as o on u.id = o.user_id").unwrap();
1858 match plan {
1859 PlanNode::NestedLoopJoin {
1860 left,
1861 right,
1862 on,
1863 kind,
1864 } => {
1865 assert_eq!(kind, JoinKind::Inner);
1866 assert!(on.is_some());
1867 assert!(matches!(*left, PlanNode::AliasScan { .. }));
1868 assert!(matches!(*right, PlanNode::AliasScan { .. }));
1869 }
1870 other => panic!("expected NestedLoopJoin, got {other:?}"),
1871 }
1872 }
1873
1874 #[test]
1875 fn duplicate_join_aliases_are_rejected_before_execution() {
1876 let err = plan("A as x join A as x on x.id = x.id").unwrap_err();
1877 assert!(
1878 err.to_string().contains("duplicate source alias `x`"),
1879 "unexpected error: {err}"
1880 );
1881 }
1882
1883 #[test]
1884 fn test_plan_right_join_rewritten_as_left_with_swapped_inputs() {
1885 let plan = plan("User as u right join Order as o on u.id = o.user_id").unwrap();
1886 match plan {
1887 PlanNode::NestedLoopJoin {
1888 left, right, kind, ..
1889 } => {
1890 assert_eq!(kind, JoinKind::LeftOuter);
1891 match *left {
1893 PlanNode::AliasScan { table, .. } => assert_eq!(table, "Order"),
1894 other => panic!("expected AliasScan(Order), got {other:?}"),
1895 }
1896 match *right {
1897 PlanNode::AliasScan { table, .. } => assert_eq!(table, "User"),
1898 other => panic!("expected AliasScan(User), got {other:?}"),
1899 }
1900 }
1901 other => panic!("expected NestedLoopJoin, got {other:?}"),
1902 }
1903 }
1904
1905 #[test]
1906 fn test_plan_multi_join_is_left_deep() {
1907 let plan = plan(
1909 "User as u join Order as o on u.id = o.user_id \
1910 join Product as p on o.product_id = p.id",
1911 )
1912 .unwrap();
1913 match plan {
1914 PlanNode::NestedLoopJoin { left, right, .. } => {
1915 match *right {
1917 PlanNode::AliasScan { table, .. } => assert_eq!(table, "Product"),
1918 other => panic!("expected AliasScan(Product), got {other:?}"),
1919 }
1920 assert!(matches!(*left, PlanNode::NestedLoopJoin { .. }));
1922 }
1923 other => panic!("expected NestedLoopJoin, got {other:?}"),
1924 }
1925 }
1926
1927 #[test]
1928 fn test_plan_join_with_filter_tail_wraps_filter_on_top() {
1929 let plan =
1930 plan("User as u join Order as o on u.id = o.user_id filter o.total > 100").unwrap();
1931 match plan {
1932 PlanNode::Filter { input, .. } => {
1933 assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
1934 }
1935 other => panic!("expected Filter(NestedLoopJoin), got {other:?}"),
1936 }
1937 }
1938
1939 #[test]
1940 fn test_plan_group_by_builds_groupby_node() {
1941 let plan = plan("User group .status { .status, n: count(.name) }").unwrap();
1942 match plan {
1944 PlanNode::Project { input, fields } => {
1945 assert_eq!(fields.len(), 2);
1946 match *input {
1947 PlanNode::GroupBy {
1948 input: inner,
1949 keys,
1950 aggregates,
1951 having,
1952 } => {
1953 assert!(matches!(*inner, PlanNode::SeqScan { .. }));
1954 assert_eq!(
1955 keys,
1956 vec![GroupKey {
1957 expr: Expr::Field("status".into()),
1958 output_name: "status".into(),
1959 }]
1960 );
1961 assert_eq!(aggregates.len(), 1);
1962 assert_eq!(aggregates[0].function, AggFunc::Count);
1963 assert_eq!(aggregates[0].argument, Expr::Field("name".into()));
1964 assert!(having.is_none());
1965 }
1966 other => panic!("expected GroupBy, got {other:?}"),
1967 }
1968 }
1969 other => panic!("expected Project, got {other:?}"),
1970 }
1971 }
1972
1973 #[test]
1974 fn test_plan_joined_group_applies_order_offset_limit_after_grouping() {
1975 let plan = plan(
1976 "User as u join Order as o on u.id = o.user_id \
1977 group u.status { u.status, n: count(*) } order n desc offset 1 limit 2",
1978 )
1979 .unwrap();
1980
1981 let PlanNode::Limit { input, .. } = plan else {
1982 panic!("expected Limit at the grouped-result boundary");
1983 };
1984 let PlanNode::Offset { input, .. } = *input else {
1985 panic!("expected Offset below Limit");
1986 };
1987 let PlanNode::Sort { input, .. } = *input else {
1988 panic!("expected Sort below Offset");
1989 };
1990 let PlanNode::Project { input, .. } = *input else {
1991 panic!("expected Project below Sort");
1992 };
1993 let PlanNode::GroupBy { input, .. } = *input else {
1994 panic!("expected GroupBy below Project");
1995 };
1996 assert!(
1997 matches!(*input, PlanNode::NestedLoopJoin { .. }),
1998 "joined rows must flow into GroupBy before result limiting"
1999 );
2000 }
2001
2002 #[test]
2003 fn test_plan_group_by_having_rewrites_agg_in_having() {
2004 let plan = plan("User group .status having count(.name) > 1 { .status }").unwrap();
2005 match plan {
2006 PlanNode::Project { input, .. } => {
2007 match *input {
2008 PlanNode::GroupBy {
2009 having, aggregates, ..
2010 } => {
2011 assert_eq!(aggregates.len(), 1);
2014 assert_eq!(aggregates[0].output_name, "__agg_0");
2015 let h = having.expect("having should be Some");
2016 match h {
2017 Expr::BinaryOp(l, BinOp::Gt, _) => {
2018 assert!(
2019 matches!(*l, Expr::Field(ref name) if name == "__agg_0"),
2020 "expected Field(__agg_0), got {l:?}"
2021 );
2022 }
2023 other => panic!("expected BinaryOp, got {other:?}"),
2024 }
2025 }
2026 other => panic!("expected GroupBy, got {other:?}"),
2027 }
2028 }
2029 other => panic!("expected Project, got {other:?}"),
2030 }
2031 }
2032
2033 #[test]
2034 fn test_plan_window_inserts_window_node_before_project() {
2035 let plan = plan("User { .name, rn: row_number() over (order .age) }").unwrap();
2036 match plan {
2038 PlanNode::Project { input, fields } => {
2039 assert_eq!(fields.len(), 2);
2040 assert!(
2042 matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"),
2043 "expected Field(__win_0), got {:?}",
2044 fields[1].expr
2045 );
2046 match *input {
2047 PlanNode::Window {
2048 input: inner,
2049 windows,
2050 } => {
2051 assert_eq!(windows.len(), 1);
2052 assert_eq!(windows[0].output_name, "__win_0");
2053 assert!(matches!(*inner, PlanNode::SeqScan { .. }));
2054 }
2055 other => panic!("expected Window, got {other:?}"),
2056 }
2057 }
2058 other => panic!("expected Project, got {other:?}"),
2059 }
2060 }
2061
2062 #[test]
2063 fn test_plan_multiple_windows() {
2064 let plan = plan(
2065 "User { .name, rn: row_number() over (order .age), s: sum(.salary) over (partition .dept order .salary) }"
2066 ).unwrap();
2067 match plan {
2068 PlanNode::Project { input, fields } => {
2069 assert_eq!(fields.len(), 3);
2070 assert!(matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"));
2071 assert!(matches!(&fields[2].expr, Expr::Field(name) if name == "__win_1"));
2072 match *input {
2073 PlanNode::Window { windows, .. } => {
2074 assert_eq!(windows.len(), 2);
2075 assert_eq!(windows[0].output_name, "__win_0");
2076 assert_eq!(windows[1].output_name, "__win_1");
2077 }
2078 other => panic!("expected Window, got {other:?}"),
2079 }
2080 }
2081 other => panic!("expected Project, got {other:?}"),
2082 }
2083 }
2084
2085 #[test]
2086 fn test_plan_no_window_without_over() {
2087 let plan = plan("User group .dept { .dept, total: sum(.salary) }").unwrap();
2089 match plan {
2090 PlanNode::Project { input, .. } => {
2091 assert!(
2093 matches!(*input, PlanNode::GroupBy { .. }),
2094 "expected GroupBy under Project, got {:?}",
2095 input
2096 );
2097 }
2098 other => panic!("expected Project, got {other:?}"),
2099 }
2100 }
2101
2102 #[test]
2103 fn test_plan_explain_wraps_inner() {
2104 let plan = plan("explain User filter .age > 30").unwrap();
2105 match plan {
2106 PlanNode::Explain { input } => {
2107 assert!(
2108 matches!(*input, PlanNode::RangeScan { .. }),
2109 "expected Explain(RangeScan), got {:?}",
2110 input
2111 );
2112 }
2113 other => panic!("expected Explain, got {other:?}"),
2114 }
2115 }
2116
2117 #[test]
2118 fn test_plan_explain_simple_scan() {
2119 let plan = plan("explain User").unwrap();
2120 match plan {
2121 PlanNode::Explain { input } => {
2122 assert!(matches!(*input, PlanNode::SeqScan { .. }));
2123 }
2124 other => panic!("expected Explain(SeqScan), got {other:?}"),
2125 }
2126 }
2127
2128 #[test]
2129 fn test_plan_explain_join() {
2130 let plan = plan("explain User as u join Order as o on u.id = o.user_id").unwrap();
2131 match plan {
2132 PlanNode::Explain { input } => {
2133 assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
2134 }
2135 other => panic!("expected Explain(NestedLoopJoin), got {other:?}"),
2136 }
2137 }
2138}