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> {
1125 if let Expr::BinaryOp(lhs, BinOp::And, rhs) = pred {
1127 if let (Some((col1, s1, e1)), Some((col2, s2, e2))) =
1128 (extract_single_bound(lhs), extract_single_bound(rhs))
1129 {
1130 if col1 == col2 {
1131 let start = s1.or(s2);
1132 let end = e1.or(e2);
1133 if start.is_some() || end.is_some() {
1134 return Some(range_scan_for_target(table, col1, start, end));
1135 }
1136 }
1137 }
1138 }
1139
1140 if let Some((col, start, end)) = extract_single_bound(pred) {
1142 return Some(range_scan_for_target(table, col, start, end));
1143 }
1144
1145 None
1146}
1147
1148pub(crate) fn range_scan_for_target(
1149 table: &str,
1150 target: RangeTarget,
1151 start: Option<(Expr, bool)>,
1152 end: Option<(Expr, bool)>,
1153) -> PlanNode {
1154 match target {
1155 RangeTarget::Column(column) => PlanNode::RangeScan {
1156 table: table.to_string(),
1157 column,
1158 start,
1159 end,
1160 },
1161 RangeTarget::JsonPath(path) => PlanNode::ExprRangeScan {
1162 table: table.to_string(),
1163 path,
1164 start,
1165 end,
1166 },
1167 }
1168}
1169
1170fn try_extract_ordered_expr_index_scan(query: &QueryExpr) -> Option<PlanNode> {
1175 if query.alias.is_some()
1176 || !query.joins.is_empty()
1177 || query.filter.is_some()
1178 || query.group_by.is_some()
1179 || query.distinct
1180 || query.aggregation.is_some()
1181 || query.projection.as_ref().is_some_and(|fields| {
1182 fields
1183 .iter()
1184 .any(|field| matches!(field.expr, Expr::Window { .. }))
1185 })
1186 {
1187 return None;
1188 }
1189 let order = query.order.as_ref()?;
1190 let [key] = order.keys.as_slice() else {
1191 return None;
1192 };
1193 let path = stored_json_path(&key.expr)?;
1194 let limit = query.limit.as_ref()?;
1195 if !matches!(limit, Expr::Literal(Literal::Int(value)) if *value >= 0) {
1196 return None;
1197 }
1198 if !query
1199 .offset
1200 .as_ref()
1201 .is_none_or(|offset| matches!(offset, Expr::Literal(Literal::Int(value)) if *value >= 0))
1202 {
1203 return None;
1204 }
1205 Some(PlanNode::OrderedExprIndexScan {
1206 table: query.source.clone(),
1207 path,
1208 descending: key.descending,
1209 limit: limit.clone(),
1210 offset: query.offset.clone(),
1211 })
1212}
1213
1214fn extract_windows(proj_fields: &mut [ProjectField]) -> Vec<WindowDef> {
1219 let mut defs = Vec::new();
1220 let mut counter = 0usize;
1221 for f in proj_fields.iter_mut() {
1222 if let Expr::Window {
1223 function,
1224 args,
1225 mode,
1226 partition_by,
1227 order_by,
1228 } = &f.expr
1229 {
1230 let output_name = format!("__win_{counter}");
1231 defs.push(WindowDef {
1232 function: *function,
1233 args: args.clone(),
1234 mode: *mode,
1235 partition_by: partition_by.clone(),
1236 order_by: order_by
1237 .iter()
1238 .map(|k| SortKey {
1239 expr: k.expr.clone(),
1240 descending: k.descending,
1241 })
1242 .collect(),
1243 output_name: output_name.clone(),
1244 });
1245 f.expr = Expr::Field(output_name);
1246 counter += 1;
1247 }
1248 }
1249 defs
1250}
1251
1252fn extract_aggregates(
1258 proj_fields: &mut [ProjectField],
1259 having: &mut Option<Expr>,
1260 source_aliases: &std::collections::HashSet<String>,
1261) -> Result<Vec<GroupAgg>, PlanError> {
1262 let mut aggs: Vec<GroupAgg> = Vec::new();
1263 let mut counter = 0usize;
1264 for f in proj_fields.iter_mut() {
1265 rewrite_agg_expr(&mut f.expr, &mut aggs, &mut counter, source_aliases)?;
1266 }
1267 if let Some(h) = having {
1268 rewrite_agg_expr(h, &mut aggs, &mut counter, source_aliases)?;
1269 }
1270 Ok(aggs)
1271}
1272
1273fn rewrite_group_key_references(
1274 fields: &mut [ProjectField],
1275 having: &mut Option<Expr>,
1276 keys: &[GroupKey],
1277) {
1278 for field in fields {
1279 rewrite_group_key_expr(&mut field.expr, keys);
1280 }
1281 if let Some(having) = having {
1282 rewrite_group_key_expr(having, keys);
1283 }
1284}
1285
1286fn rewrite_group_order_keys(
1287 order: Option<&mut OrderClause>,
1288 projection: &[ProjectField],
1289 keys: &[GroupKey],
1290) {
1291 let Some(order) = order else {
1292 return;
1293 };
1294 for order_key in &mut order.keys {
1295 let Some(group_key) = keys.iter().find(|key| key.expr == order_key.expr) else {
1296 continue;
1297 };
1298 let projected_name = projection
1299 .iter()
1300 .find(|field| field.expr == group_key.expr)
1301 .and_then(|field| field.alias.clone())
1302 .unwrap_or_else(|| group_key.output_name());
1303 order_key.expr = Expr::Field(projected_name);
1304 }
1305}
1306
1307fn rewrite_group_key_expr(expr: &mut Expr, keys: &[GroupKey]) {
1308 if let Some(key) = keys.iter().find(|key| key.expr == *expr) {
1309 *expr = Expr::Field(key.output_name());
1310 return;
1311 }
1312 match expr {
1313 Expr::FunctionCall(..) => {}
1317 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1318 rewrite_group_key_expr(left, keys);
1319 rewrite_group_key_expr(right, keys);
1320 }
1321 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) => rewrite_group_key_expr(inner, keys),
1322 Expr::ScalarFunc(_, args) => {
1323 for arg in args {
1324 rewrite_group_key_expr(arg, keys);
1325 }
1326 }
1327 Expr::InList { expr, list, .. } => {
1328 rewrite_group_key_expr(expr, keys);
1329 for item in list {
1330 rewrite_group_key_expr(item, keys);
1331 }
1332 }
1333 Expr::Case { whens, else_expr } => {
1334 for (condition, result) in whens {
1335 rewrite_group_key_expr(condition, keys);
1336 rewrite_group_key_expr(result, keys);
1337 }
1338 if let Some(expr) = else_expr {
1339 rewrite_group_key_expr(expr, keys);
1340 }
1341 }
1342 _ => {}
1343 }
1344}
1345
1346fn rewrite_agg_expr(
1347 expr: &mut Expr,
1348 aggs: &mut Vec<GroupAgg>,
1349 counter: &mut usize,
1350 source_aliases: &std::collections::HashSet<String>,
1351) -> Result<(), PlanError> {
1352 match expr {
1353 Expr::FunctionCall(func, inner, mode) => {
1354 let output = find_or_insert_agg(aggs, *func, inner, *mode, counter, source_aliases)?;
1355 *expr = Expr::Field(output);
1356 }
1357 Expr::BinaryOp(l, _, r) => {
1358 rewrite_agg_expr(l, aggs, counter, source_aliases)?;
1359 rewrite_agg_expr(r, aggs, counter, source_aliases)?;
1360 }
1361 Expr::UnaryOp(_, inner) => rewrite_agg_expr(inner, aggs, counter, source_aliases)?,
1362 Expr::Coalesce(l, r) => {
1363 rewrite_agg_expr(l, aggs, counter, source_aliases)?;
1364 rewrite_agg_expr(r, aggs, counter, source_aliases)?;
1365 }
1366 Expr::InList { expr: e, list, .. } => {
1367 rewrite_agg_expr(e, aggs, counter, source_aliases)?;
1368 for item in list {
1369 rewrite_agg_expr(item, aggs, counter, source_aliases)?;
1370 }
1371 }
1372 Expr::InSubquery { expr: e, .. } => {
1373 rewrite_agg_expr(e, aggs, counter, source_aliases)?;
1374 }
1375 _ => {}
1376 }
1377 Ok(())
1378}
1379
1380fn find_or_insert_agg(
1381 aggs: &mut Vec<GroupAgg>,
1382 func: AggFunc,
1383 argument: &Expr,
1384 mode: AggregateMode,
1385 counter: &mut usize,
1386 source_aliases: &std::collections::HashSet<String>,
1387) -> Result<String, PlanError> {
1388 for existing in aggs.iter() {
1389 if existing.function == func && existing.argument == *argument && existing.mode == mode {
1390 return Ok(existing.output_name.clone());
1391 }
1392 }
1393 let provenance_alias = symmetric_provenance_alias(func, Some(argument), mode, source_aliases)?;
1394 let output_name = format!("__agg_{counter}");
1395 aggs.push(GroupAgg {
1396 function: func,
1397 argument: argument.clone(),
1398 mode,
1399 provenance_alias,
1400 output_name: output_name.clone(),
1401 });
1402 *counter += 1;
1403 Ok(output_name)
1404}
1405
1406fn symmetric_provenance_alias(
1407 function: AggFunc,
1408 argument: Option<&Expr>,
1409 mode: AggregateMode,
1410 source_aliases: &std::collections::HashSet<String>,
1411) -> Result<Option<String>, PlanError> {
1412 if mode == AggregateMode::Raw
1413 || source_aliases.len() < 2
1414 || !matches!(function, AggFunc::Sum | AggFunc::Avg | AggFunc::Count)
1415 || (function == AggFunc::Count
1416 && argument.is_none_or(|argument| matches!(argument, Expr::Field(name) if name == "*")))
1417 {
1418 return Ok(None);
1419 }
1420 let Some(argument) = argument else {
1421 return Err(symmetric_aggregate_error(
1422 function,
1423 "does not reference a source row",
1424 ));
1425 };
1426
1427 let mut qualified = std::collections::HashSet::new();
1428 let mut has_unqualified = false;
1429 collect_expression_sources(argument, &mut qualified, &mut has_unqualified);
1430
1431 for alias in &qualified {
1432 if !source_aliases.contains(alias) {
1433 return Err(symmetric_aggregate_error(
1434 function,
1435 &format!("references unknown source alias '{alias}'"),
1436 ));
1437 }
1438 }
1439 if has_unqualified {
1440 if source_aliases.len() != 1 {
1441 return Err(symmetric_aggregate_error(
1442 function,
1443 "contains an ambiguous unqualified field",
1444 ));
1445 }
1446 qualified.extend(source_aliases.iter().cloned());
1447 }
1448 match qualified.len() {
1449 1 => Ok(qualified.into_iter().next()),
1450 0 => Err(symmetric_aggregate_error(
1451 function,
1452 "does not reference a source row",
1453 )),
1454 _ => Err(symmetric_aggregate_error(
1455 function,
1456 "references multiple source aliases",
1457 )),
1458 }
1459}
1460
1461fn symmetric_aggregate_error(function: AggFunc, reason: &str) -> PlanError {
1462 let name = format!("{function:?}").to_lowercase();
1463 PlanError::Semantic(format!(
1464 "symmetric {name} expression {reason}; reference exactly one source alias or use {name}(raw ...)"
1465 ))
1466}
1467
1468fn collect_expression_sources(
1469 expr: &Expr,
1470 qualified: &mut std::collections::HashSet<String>,
1471 has_unqualified: &mut bool,
1472) {
1473 match expr {
1474 Expr::Field(name) if name != "*" => *has_unqualified = true,
1475 Expr::QualifiedField { qualifier, .. } => {
1476 qualified.insert(qualifier.clone());
1477 }
1478 Expr::BinaryOp(left, _, right) | Expr::Coalesce(left, right) => {
1479 collect_expression_sources(left, qualified, has_unqualified);
1480 collect_expression_sources(right, qualified, has_unqualified);
1481 }
1482 Expr::UnaryOp(_, inner) | Expr::Cast(inner, _) | Expr::JsonPath { base: inner, .. } => {
1483 collect_expression_sources(inner, qualified, has_unqualified);
1484 }
1485 Expr::ScalarFunc(_, args) => {
1486 for argument in args {
1487 collect_expression_sources(argument, qualified, has_unqualified);
1488 }
1489 }
1490 Expr::InList { expr, list, .. } => {
1491 collect_expression_sources(expr, qualified, has_unqualified);
1492 for item in list {
1493 collect_expression_sources(item, qualified, has_unqualified);
1494 }
1495 }
1496 Expr::InSubquery { expr, .. } => {
1497 collect_expression_sources(expr, qualified, has_unqualified);
1498 }
1499 Expr::Case { whens, else_expr } => {
1500 for (condition, result) in whens {
1501 collect_expression_sources(condition, qualified, has_unqualified);
1502 collect_expression_sources(result, qualified, has_unqualified);
1503 }
1504 if let Some(expr) = else_expr {
1505 collect_expression_sources(expr, qualified, has_unqualified);
1506 }
1507 }
1508 Expr::Window {
1509 args,
1510 partition_by,
1511 order_by,
1512 ..
1513 } => {
1514 for expr in args.iter().chain(partition_by) {
1515 collect_expression_sources(expr, qualified, has_unqualified);
1516 }
1517 for key in order_by {
1518 collect_expression_sources(&key.expr, qualified, has_unqualified);
1519 }
1520 }
1521 Expr::FunctionCall(_, inner, _) => {
1522 collect_expression_sources(inner, qualified, has_unqualified);
1523 }
1524 Expr::ExistsSubquery { .. }
1525 | Expr::Field(_)
1526 | Expr::Literal(_)
1527 | Expr::Param(_)
1528 | Expr::ValueLit(_)
1529 | Expr::Null
1530 | Expr::NestedQuery(_) => {}
1531 }
1532}
1533
1534#[cfg(test)]
1535mod tests {
1536 use super::*;
1537 use crate::plan::PlanNode;
1538
1539 #[test]
1540 fn test_plan_simple_scan() {
1541 let plan = plan("User").unwrap();
1542 assert!(matches!(plan, PlanNode::SeqScan { table } if table == "User"));
1543 }
1544
1545 #[test]
1546 fn test_plan_filter() {
1547 let plan = plan("User filter .age > 30").unwrap();
1548 assert!(matches!(plan, PlanNode::RangeScan { .. }));
1549 }
1550
1551 #[test]
1552 fn test_plan_filter_with_projection() {
1553 let plan = plan("User filter .age > 30 { name, email }").unwrap();
1554 assert!(matches!(plan, PlanNode::Project { .. }));
1555 }
1556
1557 #[test]
1558 fn test_plan_insert() {
1559 let plan = plan(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
1560 assert!(matches!(plan, PlanNode::Insert { .. }));
1561 }
1562
1563 #[test]
1564 fn test_plan_order_limit() {
1565 let plan = plan("User order .name limit 10").unwrap();
1566 match plan {
1567 PlanNode::Limit { input, .. } => {
1568 assert!(matches!(*input, PlanNode::Sort { .. }));
1569 }
1570 _ => panic!("expected Limit(Sort(SeqScan))"),
1571 }
1572 }
1573
1574 #[test]
1575 fn test_plan_count() {
1576 let plan = plan("count(User)").unwrap();
1577 assert!(matches!(plan, PlanNode::Aggregate { .. }));
1578 }
1579
1580 #[test]
1581 fn single_source_aggregates_do_not_request_provenance() {
1582 for query in [
1583 "sum(User { .amount })",
1584 "avg(User { .amount })",
1585 "count(User { .amount })",
1586 ] {
1587 match plan(query).unwrap() {
1588 PlanNode::Aggregate {
1589 provenance_alias, ..
1590 } => assert!(
1591 provenance_alias.is_none(),
1592 "unexpected provenance for {query}"
1593 ),
1594 other => panic!("expected Aggregate for {query}, got {other:?}"),
1595 }
1596 }
1597
1598 match plan("User group .dept { total: sum(.amount) }").unwrap() {
1599 PlanNode::Project { input, .. } => match *input {
1600 PlanNode::GroupBy { aggregates, .. } => {
1601 assert!(aggregates[0].provenance_alias.is_none());
1602 }
1603 other => panic!("expected GroupBy, got {other:?}"),
1604 },
1605 other => panic!("expected Project(GroupBy), got {other:?}"),
1606 }
1607 }
1608
1609 #[test]
1610 fn join_provenance_is_limited_to_fanout_sensitive_aggregates() {
1611 let base = "Account as a join Entry as e on a.id = e.account_id group a.dept";
1612 for (function, expects_provenance) in [
1613 ("sum(a.balance)", true),
1614 ("avg(a.balance)", true),
1615 ("count(a.balance)", true),
1616 ("min(a.balance)", false),
1617 ("max(a.balance)", false),
1618 ("count(distinct a.balance)", false),
1619 ("count(*)", false),
1620 ] {
1621 let query = format!("{base} {{ value: {function} }}");
1622 match plan(&query).unwrap() {
1623 PlanNode::Project { input, .. } => match *input {
1624 PlanNode::GroupBy { aggregates, .. } => assert_eq!(
1625 aggregates[0].provenance_alias.as_deref(),
1626 expects_provenance.then_some("a"),
1627 "unexpected provenance selection for {function}"
1628 ),
1629 other => panic!("expected GroupBy for {function}, got {other:?}"),
1630 },
1631 other => panic!("expected Project(GroupBy) for {function}, got {other:?}"),
1632 }
1633 }
1634 }
1635
1636 #[test]
1637 fn test_plan_eq_becomes_index_scan() {
1638 let plan = plan("User filter .id = 42").unwrap();
1641 match plan {
1642 PlanNode::IndexScan { table, column, key } => {
1643 assert_eq!(table, "User");
1644 assert_eq!(column, "id");
1645 assert!(matches!(key, Expr::Literal(Literal::Int(42))));
1646 }
1647 other => panic!("expected IndexScan, got {other:?}"),
1648 }
1649 }
1650
1651 #[test]
1652 fn test_plan_eq_reversed_becomes_index_scan() {
1653 let plan = plan(r#"User filter "NYC" = .city"#).unwrap();
1655 assert!(matches!(plan, PlanNode::IndexScan { .. }));
1656 }
1657
1658 #[test]
1659 fn json_path_equality_and_reversed_equality_are_speculative_expression_scans() {
1660 for query in ["Post filter .data->age = 21", "Post filter 21 = .data->age"] {
1661 match plan(query).unwrap() {
1662 PlanNode::ExprIndexScan { table, path, key } => {
1663 assert_eq!(table, "Post");
1664 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1665 assert!(matches!(key, Expr::Literal(Literal::Int(21))));
1666 }
1667 other => panic!("expected ExprIndexScan for `{query}`, got {other:?}"),
1668 }
1669 }
1670 }
1671
1672 #[test]
1673 fn json_path_range_and_same_path_compound_bounds_are_speculative_scans() {
1674 for query in ["Post filter .data->age > 18", "Post filter 18 < .data->age"] {
1675 match plan(query).unwrap() {
1676 PlanNode::ExprRangeScan {
1677 path, start, end, ..
1678 } => {
1679 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1680 assert!(start.is_some());
1681 assert!(end.is_none());
1682 }
1683 other => panic!("expected ExprRangeScan for `{query}`, got {other:?}"),
1684 }
1685 }
1686
1687 match plan("Post filter .data->age >= 18 and .data->age < 65").unwrap() {
1688 PlanNode::ExprRangeScan {
1689 path, start, end, ..
1690 } => {
1691 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1692 assert_eq!(start, Some((Expr::Literal(Literal::Int(18)), true)));
1693 assert_eq!(end, Some((Expr::Literal(Literal::Int(65)), false)));
1694 }
1695 other => panic!("expected bounded ExprRangeScan, got {other:?}"),
1696 }
1697
1698 assert!(matches!(
1699 plan("Post filter .data->age >= 18 and .data->score < 65").unwrap(),
1700 PlanNode::Filter { .. }
1701 ));
1702 }
1703
1704 #[test]
1705 fn exact_single_path_order_limit_uses_ordered_expression_scan() {
1706 match plan("Post order .data->age desc limit 10 offset 2 { .id }").unwrap() {
1707 PlanNode::Project { input, .. } => match *input {
1708 PlanNode::OrderedExprIndexScan {
1709 table,
1710 path,
1711 descending,
1712 limit,
1713 offset,
1714 } => {
1715 assert_eq!(table, "Post");
1716 assert_eq!(path.canonical_text(), "v1:.data->\"age\"");
1717 assert!(descending);
1718 assert_eq!(limit, Expr::Literal(Literal::Int(10)));
1719 assert_eq!(offset, Some(Expr::Literal(Literal::Int(2))));
1720 }
1721 other => panic!("expected OrderedExprIndexScan, got {other:?}"),
1722 },
1723 other => panic!("expected Project(OrderedExprIndexScan), got {other:?}"),
1724 }
1725 }
1726
1727 #[test]
1728 fn incompatible_path_order_shapes_keep_generic_sort() {
1729 for query in [
1730 "Post order .data->age",
1731 "Post order .data->age, .id limit 10",
1732 "Post filter .data->active = true order .data->age limit 10",
1733 "Post order .data->age limit .id",
1734 ] {
1735 let planned = plan(query).unwrap();
1736 assert!(
1737 !plan_contains_ordered_expr_scan(&planned),
1738 "`{query}` must remain on the generic pipeline: {planned:?}"
1739 );
1740 }
1741 }
1742
1743 fn plan_contains_ordered_expr_scan(plan: &PlanNode) -> bool {
1744 match plan {
1745 PlanNode::OrderedExprIndexScan { .. } => true,
1746 PlanNode::Filter { input, .. }
1747 | PlanNode::Project { input, .. }
1748 | PlanNode::Sort { input, .. }
1749 | PlanNode::Limit { input, .. }
1750 | PlanNode::Offset { input, .. }
1751 | PlanNode::Aggregate { input, .. }
1752 | PlanNode::Distinct { input }
1753 | PlanNode::GroupBy { input, .. }
1754 | PlanNode::Update { input, .. }
1755 | PlanNode::Delete { input, .. }
1756 | PlanNode::Window { input, .. }
1757 | PlanNode::Explain { input } => plan_contains_ordered_expr_scan(input),
1758 PlanNode::NestedLoopJoin { left, right, .. } | PlanNode::Union { left, right, .. } => {
1759 plan_contains_ordered_expr_scan(left) || plan_contains_ordered_expr_scan(right)
1760 }
1761 _ => false,
1762 }
1763 }
1764
1765 #[test]
1766 fn test_plan_non_eq_stays_filter() {
1767 let plan = plan("User filter .age > 30").unwrap();
1769 match plan {
1770 PlanNode::RangeScan {
1771 column, start, end, ..
1772 } => {
1773 assert_eq!(column, "age");
1774 assert!(start.is_some(), "expected lower bound");
1775 assert!(end.is_none(), "expected no upper bound");
1776 let (_, inclusive) = start.unwrap();
1777 assert!(!inclusive, "expected exclusive lower bound for >");
1778 }
1779 other => panic!("expected RangeScan, got {other:?}"),
1780 }
1781 }
1782
1783 #[test]
1784 fn test_plan_index_scan_with_projection() {
1785 let plan = plan("User filter .id = 1 { .name }").unwrap();
1787 match plan {
1788 PlanNode::Project { input, .. } => {
1789 assert!(matches!(*input, PlanNode::IndexScan { .. }));
1790 }
1791 other => panic!("expected Project(IndexScan), got {other:?}"),
1792 }
1793 }
1794
1795 #[test]
1796 fn test_plan_update_by_pk_becomes_index_scan() {
1797 let plan = plan("User filter .id = 42 update { age := 31 }").unwrap();
1800 match plan {
1801 PlanNode::Update { input, .. } => {
1802 assert!(
1803 matches!(*input, PlanNode::IndexScan { .. }),
1804 "expected Update(IndexScan), got {input:?}"
1805 );
1806 }
1807 other => panic!("expected Update, got {other:?}"),
1808 }
1809 }
1810
1811 #[test]
1812 fn test_plan_update_range_stays_range_scan() {
1813 let plan = plan("User filter .age > 30 update { age := 31 }").unwrap();
1814 match plan {
1815 PlanNode::Update { input, .. } => {
1816 assert!(
1817 matches!(*input, PlanNode::RangeScan { .. }),
1818 "expected Update(RangeScan), got {input:?}"
1819 );
1820 }
1821 other => panic!("expected Update, got {other:?}"),
1822 }
1823 }
1824
1825 #[test]
1826 fn test_plan_delete_by_pk_becomes_index_scan() {
1827 let plan = plan("User filter .id = 7 delete").unwrap();
1828 match plan {
1829 PlanNode::Delete { input, .. } => {
1830 assert!(matches!(*input, PlanNode::IndexScan { .. }));
1831 }
1832 other => panic!("expected Delete, got {other:?}"),
1833 }
1834 }
1835
1836 #[test]
1837 fn test_plan_inner_join_builds_nested_loop() {
1838 let plan = plan("User as u join Order as o on u.id = o.user_id").unwrap();
1841 match plan {
1842 PlanNode::NestedLoopJoin {
1843 left,
1844 right,
1845 on,
1846 kind,
1847 } => {
1848 assert_eq!(kind, JoinKind::Inner);
1849 assert!(on.is_some());
1850 assert!(matches!(*left, PlanNode::AliasScan { .. }));
1851 assert!(matches!(*right, PlanNode::AliasScan { .. }));
1852 }
1853 other => panic!("expected NestedLoopJoin, got {other:?}"),
1854 }
1855 }
1856
1857 #[test]
1858 fn duplicate_join_aliases_are_rejected_before_execution() {
1859 let err = plan("A as x join A as x on x.id = x.id").unwrap_err();
1860 assert!(
1861 err.to_string().contains("duplicate source alias `x`"),
1862 "unexpected error: {err}"
1863 );
1864 }
1865
1866 #[test]
1867 fn test_plan_right_join_rewritten_as_left_with_swapped_inputs() {
1868 let plan = plan("User as u right join Order as o on u.id = o.user_id").unwrap();
1869 match plan {
1870 PlanNode::NestedLoopJoin {
1871 left, right, kind, ..
1872 } => {
1873 assert_eq!(kind, JoinKind::LeftOuter);
1874 match *left {
1876 PlanNode::AliasScan { table, .. } => assert_eq!(table, "Order"),
1877 other => panic!("expected AliasScan(Order), got {other:?}"),
1878 }
1879 match *right {
1880 PlanNode::AliasScan { table, .. } => assert_eq!(table, "User"),
1881 other => panic!("expected AliasScan(User), got {other:?}"),
1882 }
1883 }
1884 other => panic!("expected NestedLoopJoin, got {other:?}"),
1885 }
1886 }
1887
1888 #[test]
1889 fn test_plan_multi_join_is_left_deep() {
1890 let plan = plan(
1892 "User as u join Order as o on u.id = o.user_id \
1893 join Product as p on o.product_id = p.id",
1894 )
1895 .unwrap();
1896 match plan {
1897 PlanNode::NestedLoopJoin { left, right, .. } => {
1898 match *right {
1900 PlanNode::AliasScan { table, .. } => assert_eq!(table, "Product"),
1901 other => panic!("expected AliasScan(Product), got {other:?}"),
1902 }
1903 assert!(matches!(*left, PlanNode::NestedLoopJoin { .. }));
1905 }
1906 other => panic!("expected NestedLoopJoin, got {other:?}"),
1907 }
1908 }
1909
1910 #[test]
1911 fn test_plan_join_with_filter_tail_wraps_filter_on_top() {
1912 let plan =
1913 plan("User as u join Order as o on u.id = o.user_id filter o.total > 100").unwrap();
1914 match plan {
1915 PlanNode::Filter { input, .. } => {
1916 assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
1917 }
1918 other => panic!("expected Filter(NestedLoopJoin), got {other:?}"),
1919 }
1920 }
1921
1922 #[test]
1923 fn test_plan_group_by_builds_groupby_node() {
1924 let plan = plan("User group .status { .status, n: count(.name) }").unwrap();
1925 match plan {
1927 PlanNode::Project { input, fields } => {
1928 assert_eq!(fields.len(), 2);
1929 match *input {
1930 PlanNode::GroupBy {
1931 input: inner,
1932 keys,
1933 aggregates,
1934 having,
1935 } => {
1936 assert!(matches!(*inner, PlanNode::SeqScan { .. }));
1937 assert_eq!(
1938 keys,
1939 vec![GroupKey {
1940 expr: Expr::Field("status".into()),
1941 output_name: "status".into(),
1942 }]
1943 );
1944 assert_eq!(aggregates.len(), 1);
1945 assert_eq!(aggregates[0].function, AggFunc::Count);
1946 assert_eq!(aggregates[0].argument, Expr::Field("name".into()));
1947 assert!(having.is_none());
1948 }
1949 other => panic!("expected GroupBy, got {other:?}"),
1950 }
1951 }
1952 other => panic!("expected Project, got {other:?}"),
1953 }
1954 }
1955
1956 #[test]
1957 fn test_plan_joined_group_applies_order_offset_limit_after_grouping() {
1958 let plan = plan(
1959 "User as u join Order as o on u.id = o.user_id \
1960 group u.status { u.status, n: count(*) } order n desc offset 1 limit 2",
1961 )
1962 .unwrap();
1963
1964 let PlanNode::Limit { input, .. } = plan else {
1965 panic!("expected Limit at the grouped-result boundary");
1966 };
1967 let PlanNode::Offset { input, .. } = *input else {
1968 panic!("expected Offset below Limit");
1969 };
1970 let PlanNode::Sort { input, .. } = *input else {
1971 panic!("expected Sort below Offset");
1972 };
1973 let PlanNode::Project { input, .. } = *input else {
1974 panic!("expected Project below Sort");
1975 };
1976 let PlanNode::GroupBy { input, .. } = *input else {
1977 panic!("expected GroupBy below Project");
1978 };
1979 assert!(
1980 matches!(*input, PlanNode::NestedLoopJoin { .. }),
1981 "joined rows must flow into GroupBy before result limiting"
1982 );
1983 }
1984
1985 #[test]
1986 fn test_plan_group_by_having_rewrites_agg_in_having() {
1987 let plan = plan("User group .status having count(.name) > 1 { .status }").unwrap();
1988 match plan {
1989 PlanNode::Project { input, .. } => {
1990 match *input {
1991 PlanNode::GroupBy {
1992 having, aggregates, ..
1993 } => {
1994 assert_eq!(aggregates.len(), 1);
1997 assert_eq!(aggregates[0].output_name, "__agg_0");
1998 let h = having.expect("having should be Some");
1999 match h {
2000 Expr::BinaryOp(l, BinOp::Gt, _) => {
2001 assert!(
2002 matches!(*l, Expr::Field(ref name) if name == "__agg_0"),
2003 "expected Field(__agg_0), got {l:?}"
2004 );
2005 }
2006 other => panic!("expected BinaryOp, got {other:?}"),
2007 }
2008 }
2009 other => panic!("expected GroupBy, got {other:?}"),
2010 }
2011 }
2012 other => panic!("expected Project, got {other:?}"),
2013 }
2014 }
2015
2016 #[test]
2017 fn test_plan_window_inserts_window_node_before_project() {
2018 let plan = plan("User { .name, rn: row_number() over (order .age) }").unwrap();
2019 match plan {
2021 PlanNode::Project { input, fields } => {
2022 assert_eq!(fields.len(), 2);
2023 assert!(
2025 matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"),
2026 "expected Field(__win_0), got {:?}",
2027 fields[1].expr
2028 );
2029 match *input {
2030 PlanNode::Window {
2031 input: inner,
2032 windows,
2033 } => {
2034 assert_eq!(windows.len(), 1);
2035 assert_eq!(windows[0].output_name, "__win_0");
2036 assert!(matches!(*inner, PlanNode::SeqScan { .. }));
2037 }
2038 other => panic!("expected Window, got {other:?}"),
2039 }
2040 }
2041 other => panic!("expected Project, got {other:?}"),
2042 }
2043 }
2044
2045 #[test]
2046 fn test_plan_multiple_windows() {
2047 let plan = plan(
2048 "User { .name, rn: row_number() over (order .age), s: sum(.salary) over (partition .dept order .salary) }"
2049 ).unwrap();
2050 match plan {
2051 PlanNode::Project { input, fields } => {
2052 assert_eq!(fields.len(), 3);
2053 assert!(matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"));
2054 assert!(matches!(&fields[2].expr, Expr::Field(name) if name == "__win_1"));
2055 match *input {
2056 PlanNode::Window { windows, .. } => {
2057 assert_eq!(windows.len(), 2);
2058 assert_eq!(windows[0].output_name, "__win_0");
2059 assert_eq!(windows[1].output_name, "__win_1");
2060 }
2061 other => panic!("expected Window, got {other:?}"),
2062 }
2063 }
2064 other => panic!("expected Project, got {other:?}"),
2065 }
2066 }
2067
2068 #[test]
2069 fn test_plan_no_window_without_over() {
2070 let plan = plan("User group .dept { .dept, total: sum(.salary) }").unwrap();
2072 match plan {
2073 PlanNode::Project { input, .. } => {
2074 assert!(
2076 matches!(*input, PlanNode::GroupBy { .. }),
2077 "expected GroupBy under Project, got {:?}",
2078 input
2079 );
2080 }
2081 other => panic!("expected Project, got {other:?}"),
2082 }
2083 }
2084
2085 #[test]
2086 fn test_plan_explain_wraps_inner() {
2087 let plan = plan("explain User filter .age > 30").unwrap();
2088 match plan {
2089 PlanNode::Explain { input } => {
2090 assert!(
2091 matches!(*input, PlanNode::RangeScan { .. }),
2092 "expected Explain(RangeScan), got {:?}",
2093 input
2094 );
2095 }
2096 other => panic!("expected Explain, got {other:?}"),
2097 }
2098 }
2099
2100 #[test]
2101 fn test_plan_explain_simple_scan() {
2102 let plan = plan("explain User").unwrap();
2103 match plan {
2104 PlanNode::Explain { input } => {
2105 assert!(matches!(*input, PlanNode::SeqScan { .. }));
2106 }
2107 other => panic!("expected Explain(SeqScan), got {other:?}"),
2108 }
2109 }
2110
2111 #[test]
2112 fn test_plan_explain_join() {
2113 let plan = plan("explain User as u join Order as o on u.id = o.user_id").unwrap();
2114 match plan {
2115 PlanNode::Explain { input } => {
2116 assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
2117 }
2118 other => panic!("expected Explain(NestedLoopJoin), got {other:?}"),
2119 }
2120 }
2121}