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