Skip to main content

powdb_query/
planner.rs

1use crate::ast::*;
2use crate::parser::{parse, ParseError};
3use crate::plan::*;
4
5/// (column_name, lower_bound, upper_bound) — used by range-index extraction.
6type RangeBound = (String, Option<(Expr, bool)>, Option<(Expr, bool)>);
7
8/// Plan-phase error — wraps ParseError for the full lex→parse→plan chain.
9#[derive(Debug)]
10pub enum PlanError {
11    /// Error originated in the parser (or lexer, via ParseError::Lex).
12    Parse(ParseError),
13}
14
15impl PlanError {
16    /// Convenience: human-readable message for any variant.
17    pub fn message(&self) -> String {
18        self.to_string()
19    }
20}
21
22impl std::fmt::Display for PlanError {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            Self::Parse(e) => write!(f, "{e}"),
26        }
27    }
28}
29
30impl std::error::Error for PlanError {
31    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
32        match self {
33            Self::Parse(e) => Some(e),
34        }
35    }
36}
37
38impl From<ParseError> for PlanError {
39    fn from(e: ParseError) -> Self {
40        PlanError::Parse(e)
41    }
42}
43
44pub fn plan(input: &str) -> Result<PlanNode, PlanError> {
45    let stmt = parse(input)?;
46    plan_statement(stmt)
47}
48
49pub fn plan_statement(stmt: Statement) -> Result<PlanNode, PlanError> {
50    match stmt {
51        Statement::Query(q) => plan_query(q),
52        Statement::Insert(ins) => plan_insert(ins),
53        Statement::UpdateQuery(upd) => plan_update(upd),
54        Statement::DeleteQuery(del) => plan_delete(del),
55        Statement::CreateType(ct) => plan_create_type(ct),
56        Statement::AlterTable(at) => Ok(PlanNode::AlterTable {
57            table: at.table,
58            action: at.action,
59        }),
60        Statement::DropTable(dt) => Ok(PlanNode::DropTable {
61            name: dt.table,
62            if_exists: dt.if_exists,
63        }),
64        Statement::CreateView(cv) => Ok(PlanNode::CreateView {
65            name: cv.name,
66            query_text: cv.query_text,
67        }),
68        Statement::RefreshView(rv) => Ok(PlanNode::RefreshView { name: rv.name }),
69        Statement::DropView(dv) => Ok(PlanNode::DropView {
70            name: dv.name,
71            if_exists: dv.if_exists,
72        }),
73        Statement::ListTypes => Ok(PlanNode::ListTypes),
74        Statement::Describe(table) => Ok(PlanNode::Describe { table }),
75        Statement::Union(u) => {
76            let left = plan_statement(*u.left)?;
77            let right = plan_statement(*u.right)?;
78            Ok(PlanNode::Union {
79                left: Box::new(left),
80                right: Box::new(right),
81                all: u.all,
82            })
83        }
84        Statement::Upsert(ups) => plan_upsert(ups),
85        Statement::Begin => Ok(PlanNode::Begin),
86        Statement::Commit => Ok(PlanNode::Commit),
87        Statement::Rollback => Ok(PlanNode::Rollback),
88        Statement::Explain(inner) => {
89            let inner_plan = plan_statement(*inner)?;
90            Ok(PlanNode::Explain {
91                input: Box::new(inner_plan),
92            })
93        }
94    }
95}
96
97fn plan_query(q: QueryExpr) -> Result<PlanNode, PlanError> {
98    // Mission E1.2: if the query has joins, build a left-deep nested-loop
99    // plan. Correctness first — hash-join optimization is E1.3. We also
100    // don't try to fold an IndexScan under a joined query yet (the
101    // leaf-level fast paths all match on `PlanNode::SeqScan { .. }`
102    // literally, so mixing them into a join plan would silently break).
103    if !q.joins.is_empty() {
104        return plan_joined_query(q);
105    }
106    // Try to fold `filter .col = literal` into an IndexScan. The executor
107    // decides at run time whether the column actually has an index — if not,
108    // it transparently falls back to a sequential scan with the same predicate,
109    // so this rewrite is always safe.
110    //
111    // We only rewrite the *simple* eq case: `filter .col = literal`. Conjunctions
112    // like `filter .col = 1 and .other > 5` fall through to SeqScan + Filter.
113    // Extending this to split conjunctions is a future optimization.
114    let (source, filter) = match q.filter {
115        Some(pred) => match try_extract_eq_index_key(&q.source, &pred) {
116            Some(index_scan) => (index_scan, None),
117            None => match try_extract_range_index_keys(&q.source, &pred) {
118                Some(range_scan) => (range_scan, None),
119                None => (
120                    PlanNode::SeqScan {
121                        table: q.source.clone(),
122                    },
123                    Some(pred),
124                ),
125            },
126        },
127        None => (
128            PlanNode::SeqScan {
129                table: q.source.clone(),
130            },
131            None,
132        ),
133    };
134    let mut node = source;
135
136    if let Some(pred) = filter {
137        node = PlanNode::Filter {
138            input: Box::new(node),
139            predicate: pred,
140        };
141    }
142
143    // Mission E2b: GROUP BY path — insert GroupBy + Project before
144    // order/limit/offset/distinct.
145    if let Some(group) = q.group_by {
146        let mut proj_fields: Vec<ProjectField> = q
147            .projection
148            .map(|proj| {
149                proj.into_iter()
150                    .map(|pf| ProjectField {
151                        alias: pf.alias,
152                        expr: pf.expr,
153                    })
154                    .collect()
155            })
156            .unwrap_or_default();
157        let mut having = group.having;
158        let aggregates = extract_aggregates(&mut proj_fields, &mut having);
159
160        node = PlanNode::GroupBy {
161            input: Box::new(node),
162            keys: group.keys,
163            aggregates,
164            having,
165        };
166
167        if !proj_fields.is_empty() {
168            node = PlanNode::Project {
169                input: Box::new(node),
170                fields: proj_fields,
171            };
172        }
173
174        if let Some(order) = q.order {
175            node = PlanNode::Sort {
176                input: Box::new(node),
177                keys: order
178                    .keys
179                    .into_iter()
180                    .map(|k| SortKey {
181                        field: k.field,
182                        descending: k.descending,
183                    })
184                    .collect(),
185            };
186        }
187        // Offset must be applied *before* Limit: skip M rows, then take N.
188        // Plan shape is Limit(Offset(...)), so Offset is built first (inner)
189        // and Limit wraps it (outer).
190        if let Some(off) = q.offset {
191            node = PlanNode::Offset {
192                input: Box::new(node),
193                count: off,
194            };
195        }
196        if let Some(lim) = q.limit {
197            node = PlanNode::Limit {
198                input: Box::new(node),
199                count: lim,
200            };
201        }
202        if q.distinct {
203            node = PlanNode::Distinct {
204                input: Box::new(node),
205            };
206        }
207        return Ok(node);
208    }
209
210    if let Some(order) = q.order {
211        node = PlanNode::Sort {
212            input: Box::new(node),
213            keys: order
214                .keys
215                .into_iter()
216                .map(|k| SortKey {
217                    field: k.field,
218                    descending: k.descending,
219                })
220                .collect(),
221        };
222    }
223
224    // Offset must be applied *before* Limit: skip M rows, then take N.
225    // Plan shape is Limit(Offset(...)), so Offset is built first (inner)
226    // and Limit wraps it (outer).
227    if let Some(off) = q.offset {
228        node = PlanNode::Offset {
229            input: Box::new(node),
230            count: off,
231        };
232    }
233
234    if let Some(lim) = q.limit {
235        node = PlanNode::Limit {
236            input: Box::new(node),
237            count: lim,
238        };
239    }
240
241    if let Some(proj) = q.projection {
242        let mut fields: Vec<ProjectField> = proj
243            .into_iter()
244            .map(|pf| ProjectField {
245                alias: pf.alias,
246                expr: pf.expr,
247            })
248            .collect();
249        let windows = extract_windows(&mut fields);
250        if !windows.is_empty() {
251            node = PlanNode::Window {
252                input: Box::new(node),
253                windows,
254            };
255        }
256        node = PlanNode::Project {
257            input: Box::new(node),
258            fields,
259        };
260    }
261
262    if q.distinct {
263        node = PlanNode::Distinct {
264            input: Box::new(node),
265        };
266    }
267
268    if let Some(agg) = q.aggregation {
269        node = PlanNode::Aggregate {
270            input: Box::new(node),
271            function: agg.function,
272            field: agg.field,
273        };
274    }
275
276    Ok(node)
277}
278
279/// Build a left-deep nested-loop join plan for a query with 1+ join clauses.
280///
281/// The plan shape for `T1 as a [inner|left|cross] join T2 as b on <pred> ...` is:
282///
283///   Project? (optional, from q.projection)
284///   └─ Offset? / Limit? / Sort?
285///      └─ Filter? (the top-level q.filter, using qualified columns)
286///         └─ NestedLoopJoin { kind, on }
287///            ├─ AliasScan { T1, a }
288///            └─ AliasScan { T2, b }
289///
290/// Multi-join chains extend left-deep: a third join adds a second
291/// `NestedLoopJoin` on top, with the first join's output as its `left`.
292///
293/// Aliases default to the source table name when the query didn't write
294/// `as <name>` explicitly — that way users can always write `T.field`
295/// without being forced to alias every source.
296///
297/// RightOuter is rewritten into LeftOuter with inputs swapped — the two
298/// differ only in which side survives non-matching rows, and swapping
299/// inputs lets the executor ship a single LeftOuter path.
300fn plan_joined_query(q: QueryExpr) -> Result<PlanNode, PlanError> {
301    let primary_alias = q.alias.clone().unwrap_or_else(|| q.source.clone());
302    let mut node = PlanNode::AliasScan {
303        table: q.source.clone(),
304        alias: primary_alias,
305    };
306
307    for join in q.joins {
308        let right_alias = join.alias.unwrap_or_else(|| join.source.clone());
309        let right = PlanNode::AliasScan {
310            table: join.source,
311            alias: right_alias,
312        };
313        match join.kind {
314            JoinKind::Inner | JoinKind::LeftOuter | JoinKind::Cross => {
315                node = PlanNode::NestedLoopJoin {
316                    left: Box::new(node),
317                    right: Box::new(right),
318                    on: join.on,
319                    kind: join.kind,
320                };
321            }
322            JoinKind::RightOuter => {
323                // `a RIGHT OUTER JOIN b ON <p>` ≡ `b LEFT OUTER JOIN a ON <p>`.
324                node = PlanNode::NestedLoopJoin {
325                    left: Box::new(right),
326                    right: Box::new(node),
327                    on: join.on,
328                    kind: JoinKind::LeftOuter,
329                };
330            }
331        }
332    }
333
334    if let Some(pred) = q.filter {
335        node = PlanNode::Filter {
336            input: Box::new(node),
337            predicate: pred,
338        };
339    }
340
341    if let Some(order) = q.order {
342        node = PlanNode::Sort {
343            input: Box::new(node),
344            keys: order
345                .keys
346                .into_iter()
347                .map(|k| SortKey {
348                    field: k.field,
349                    descending: k.descending,
350                })
351                .collect(),
352        };
353    }
354
355    // Offset must be applied *before* Limit: skip M rows, then take N.
356    // Plan shape is Limit(Offset(...)), so Offset is built first (inner)
357    // and Limit wraps it (outer).
358    if let Some(off) = q.offset {
359        node = PlanNode::Offset {
360            input: Box::new(node),
361            count: off,
362        };
363    }
364
365    if let Some(lim) = q.limit {
366        node = PlanNode::Limit {
367            input: Box::new(node),
368            count: lim,
369        };
370    }
371
372    // Mission E2b: GROUP BY path for joined queries.
373    if let Some(group) = q.group_by {
374        let mut proj_fields: Vec<ProjectField> = q
375            .projection
376            .map(|proj| {
377                proj.into_iter()
378                    .map(|pf| ProjectField {
379                        alias: pf.alias,
380                        expr: pf.expr,
381                    })
382                    .collect()
383            })
384            .unwrap_or_default();
385        let mut having = group.having;
386        let aggregates = extract_aggregates(&mut proj_fields, &mut having);
387
388        node = PlanNode::GroupBy {
389            input: Box::new(node),
390            keys: group.keys,
391            aggregates,
392            having,
393        };
394
395        if !proj_fields.is_empty() {
396            node = PlanNode::Project {
397                input: Box::new(node),
398                fields: proj_fields,
399            };
400        }
401        if q.distinct {
402            node = PlanNode::Distinct {
403                input: Box::new(node),
404            };
405        }
406        return Ok(node);
407    }
408
409    if let Some(proj) = q.projection {
410        let mut fields: Vec<ProjectField> = proj
411            .into_iter()
412            .map(|pf| ProjectField {
413                alias: pf.alias,
414                expr: pf.expr,
415            })
416            .collect();
417        let windows = extract_windows(&mut fields);
418        if !windows.is_empty() {
419            node = PlanNode::Window {
420                input: Box::new(node),
421                windows,
422            };
423        }
424        node = PlanNode::Project {
425            input: Box::new(node),
426            fields,
427        };
428    }
429
430    if q.distinct {
431        node = PlanNode::Distinct {
432            input: Box::new(node),
433        };
434    }
435
436    if let Some(agg) = q.aggregation {
437        node = PlanNode::Aggregate {
438            input: Box::new(node),
439            function: agg.function,
440            field: agg.field,
441        };
442    }
443
444    Ok(node)
445}
446
447fn plan_insert(ins: InsertExpr) -> Result<PlanNode, PlanError> {
448    Ok(PlanNode::Insert {
449        table: ins.target,
450        rows: ins.rows,
451        returning: ins.returning,
452    })
453}
454
455fn plan_update(upd: UpdateExpr) -> Result<PlanNode, PlanError> {
456    // Mirror the read-side IndexScan fold: when the update filter is a simple
457    // `.col = literal`, emit `Update(IndexScan)` so the executor's index-lookup
458    // mutation fast path fires. The executor falls back to a scan if the
459    // column happens to lack an index, so this is always safe.
460    let source = match upd.filter {
461        Some(pred) => match try_extract_eq_index_key(&upd.source, &pred) {
462            Some(index_scan) => index_scan,
463            None => match try_extract_range_index_keys(&upd.source, &pred) {
464                Some(range_scan) => range_scan,
465                None => PlanNode::Filter {
466                    input: Box::new(PlanNode::SeqScan {
467                        table: upd.source.clone(),
468                    }),
469                    predicate: pred,
470                },
471            },
472        },
473        None => PlanNode::SeqScan {
474            table: upd.source.clone(),
475        },
476    };
477    Ok(PlanNode::Update {
478        input: Box::new(source),
479        table: upd.source,
480        assignments: upd.assignments,
481        returning: upd.returning,
482    })
483}
484
485fn plan_delete(del: DeleteExpr) -> Result<PlanNode, PlanError> {
486    let source = match del.filter {
487        Some(pred) => match try_extract_eq_index_key(&del.source, &pred) {
488            Some(index_scan) => index_scan,
489            None => match try_extract_range_index_keys(&del.source, &pred) {
490                Some(range_scan) => range_scan,
491                None => PlanNode::Filter {
492                    input: Box::new(PlanNode::SeqScan {
493                        table: del.source.clone(),
494                    }),
495                    predicate: pred,
496                },
497            },
498        },
499        None => PlanNode::SeqScan {
500            table: del.source.clone(),
501        },
502    };
503    Ok(PlanNode::Delete {
504        input: Box::new(source),
505        table: del.source,
506        returning: del.returning,
507    })
508}
509
510fn plan_upsert(ups: UpsertExpr) -> Result<PlanNode, PlanError> {
511    Ok(PlanNode::Upsert {
512        table: ups.target,
513        key_column: ups.key_column,
514        assignments: ups.assignments,
515        on_conflict: ups.on_conflict,
516    })
517}
518
519fn plan_create_type(ct: CreateTypeExpr) -> Result<PlanNode, PlanError> {
520    let fields = ct
521        .fields
522        .into_iter()
523        .map(|f| crate::plan::CreateField {
524            name: f.name,
525            type_name: f.type_name,
526            required: f.required,
527            unique: f.unique,
528            default: f.default,
529            auto: f.auto,
530        })
531        .collect();
532    Ok(PlanNode::CreateTable {
533        name: ct.name,
534        fields,
535        if_not_exists: ct.if_not_exists,
536    })
537}
538
539/// If the predicate is a simple `.field = literal` (or `literal = .field`),
540/// return a corresponding IndexScan plan node. Otherwise return None so the
541/// caller can fall through to SeqScan + Filter.
542///
543/// The executor decides at run time whether the named column actually has a
544/// B-tree index — if not, IndexScan transparently falls back to a scan +
545/// equality filter on that column. That means this rewrite is always safe
546/// regardless of schema/index state; it just unlocks the fast path when an
547/// index happens to exist.
548fn try_extract_eq_index_key(table: &str, pred: &Expr) -> Option<PlanNode> {
549    let (lhs, op, rhs) = match pred {
550        Expr::BinaryOp(lhs, op, rhs) => (lhs.as_ref(), *op, rhs.as_ref()),
551        _ => return None,
552    };
553    if op != BinOp::Eq {
554        return None;
555    }
556    let (column, key) = match (lhs, rhs) {
557        (Expr::Field(name), Expr::Literal(_)) => (name.clone(), rhs.clone()),
558        (Expr::Literal(_), Expr::Field(name)) => (name.clone(), lhs.clone()),
559        _ => return None,
560    };
561    Some(PlanNode::IndexScan {
562        table: table.to_string(),
563        column,
564        key,
565    })
566}
567
568/// Extract a single range bound from a simple inequality predicate.
569/// Returns `(column, lower_bound, upper_bound)` where at most one bound is set.
570fn extract_single_bound(pred: &Expr) -> Option<RangeBound> {
571    let (lhs, op, rhs) = match pred {
572        Expr::BinaryOp(lhs, op, rhs) => (lhs.as_ref(), *op, rhs.as_ref()),
573        _ => return None,
574    };
575    match op {
576        // .col > literal  →  lower=(literal, exclusive)
577        BinOp::Gt => match (lhs, rhs) {
578            (Expr::Field(name), Expr::Literal(_)) => {
579                Some((name.clone(), Some((rhs.clone(), false)), None))
580            }
581            (Expr::Literal(_), Expr::Field(name)) => {
582                // literal > .col  →  col < literal  →  upper=(literal, exclusive)
583                Some((name.clone(), None, Some((lhs.clone(), false))))
584            }
585            _ => None,
586        },
587        // .col >= literal  →  lower=(literal, inclusive)
588        BinOp::Gte => match (lhs, rhs) {
589            (Expr::Field(name), Expr::Literal(_)) => {
590                Some((name.clone(), Some((rhs.clone(), true)), None))
591            }
592            (Expr::Literal(_), Expr::Field(name)) => {
593                Some((name.clone(), None, Some((lhs.clone(), true))))
594            }
595            _ => None,
596        },
597        // .col < literal  →  upper=(literal, exclusive)
598        BinOp::Lt => match (lhs, rhs) {
599            (Expr::Field(name), Expr::Literal(_)) => {
600                Some((name.clone(), None, Some((rhs.clone(), false))))
601            }
602            (Expr::Literal(_), Expr::Field(name)) => {
603                Some((name.clone(), Some((lhs.clone(), false)), None))
604            }
605            _ => None,
606        },
607        // .col <= literal  →  upper=(literal, inclusive)
608        BinOp::Lte => match (lhs, rhs) {
609            (Expr::Field(name), Expr::Literal(_)) => {
610                Some((name.clone(), None, Some((rhs.clone(), true))))
611            }
612            (Expr::Literal(_), Expr::Field(name)) => {
613                Some((name.clone(), Some((lhs.clone(), true)), None))
614            }
615            _ => None,
616        },
617        _ => None,
618    }
619}
620
621/// If the predicate is an inequality or a conjunction of two inequalities
622/// on the same indexed column, return a RangeScan plan node.
623/// Handles: `.col > lit`, `.col >= lit`, `.col < lit`, `.col <= lit`,
624/// and AND-conjunctions like `.col >= low AND .col <= high` (BETWEEN pattern).
625fn try_extract_range_index_keys(table: &str, pred: &Expr) -> Option<PlanNode> {
626    // Case 1: AND conjunction — try to merge two bounds on the same column.
627    if let Expr::BinaryOp(lhs, BinOp::And, rhs) = pred {
628        if let (Some((col1, s1, e1)), Some((col2, s2, e2))) =
629            (extract_single_bound(lhs), extract_single_bound(rhs))
630        {
631            if col1 == col2 {
632                let start = s1.or(s2);
633                let end = e1.or(e2);
634                if start.is_some() || end.is_some() {
635                    return Some(PlanNode::RangeScan {
636                        table: table.to_string(),
637                        column: col1,
638                        start,
639                        end,
640                    });
641                }
642            }
643        }
644    }
645
646    // Case 2: single inequality.
647    if let Some((col, start, end)) = extract_single_bound(pred) {
648        return Some(PlanNode::RangeScan {
649            table: table.to_string(),
650            column: col,
651            start,
652            end,
653        });
654    }
655
656    None
657}
658
659/// Walk projection fields, replacing every `Expr::Window { .. }` with
660/// `Expr::Field("__win_N")` and collecting the corresponding `WindowDef`
661/// descriptors. Returns the list of window definitions to insert as a
662/// `PlanNode::Window` before the `Project` node.
663fn extract_windows(proj_fields: &mut [ProjectField]) -> Vec<WindowDef> {
664    let mut defs = Vec::new();
665    let mut counter = 0usize;
666    for f in proj_fields.iter_mut() {
667        if let Expr::Window {
668            function,
669            args,
670            partition_by,
671            order_by,
672        } = &f.expr
673        {
674            let output_name = format!("__win_{counter}");
675            defs.push(WindowDef {
676                function: *function,
677                args: args.clone(),
678                partition_by: partition_by.clone(),
679                order_by: order_by
680                    .iter()
681                    .map(|k| SortKey {
682                        field: k.field.clone(),
683                        descending: k.descending,
684                    })
685                    .collect(),
686                output_name: output_name.clone(),
687            });
688            f.expr = Expr::Field(output_name);
689            counter += 1;
690        }
691    }
692    defs
693}
694
695/// Walk projection fields and HAVING expression, replacing every
696/// `Expr::FunctionCall(func, Field(col))` with `Expr::Field("__agg_N")`
697/// and collecting the corresponding `GroupAgg` descriptors. Deduplicates:
698/// if the same (func, field) pair appears in both projection and HAVING,
699/// they share a single `GroupAgg` entry.
700fn extract_aggregates(
701    proj_fields: &mut [ProjectField],
702    having: &mut Option<Expr>,
703) -> Vec<GroupAgg> {
704    let mut aggs: Vec<GroupAgg> = Vec::new();
705    let mut counter = 0usize;
706    for f in proj_fields.iter_mut() {
707        rewrite_agg_expr(&mut f.expr, &mut aggs, &mut counter);
708    }
709    if let Some(h) = having {
710        rewrite_agg_expr(h, &mut aggs, &mut counter);
711    }
712    aggs
713}
714
715fn rewrite_agg_expr(expr: &mut Expr, aggs: &mut Vec<GroupAgg>, counter: &mut usize) {
716    match expr {
717        Expr::FunctionCall(func, inner) => {
718            // Extract the aggregate's source column. Both the single-table
719            // form `count(.total)` (a `Field`) and the join form
720            // `count(o.total)` (a `QualifiedField`) must be lowered here; a
721            // qualified inner is folded to `Field("alias.field")` so it lines
722            // up with the join output columns named `alias.field`. Anything
723            // else (a nested expression, another aggregate) is left as a
724            // `FunctionCall`, which the executor's validation guard rejects as
725            // an unsupported position rather than silently evaluating it to
726            // Empty (a wrong answer). JsonPath inners (sum(.data->price)) are
727            // NOT extracted here: GroupAgg is keyed by column name end to end,
728            // so aggregating over a path needs Expr-valued aggregate inners.
729            // That lands in v0.13 with symmetric aggregates; until then such
730            // queries fail loudly via the unsupported-position guard.
731            let field_name = match inner.as_ref() {
732                Expr::Field(name) => Some(name.clone()),
733                Expr::QualifiedField { qualifier, field } => Some(format!("{qualifier}.{field}")),
734                _ => None,
735            };
736            if let Some(name) = field_name {
737                let output = find_or_insert_agg(aggs, *func, &name, counter);
738                *expr = Expr::Field(output);
739            }
740        }
741        Expr::BinaryOp(l, _, r) => {
742            rewrite_agg_expr(l, aggs, counter);
743            rewrite_agg_expr(r, aggs, counter);
744        }
745        Expr::UnaryOp(_, inner) => rewrite_agg_expr(inner, aggs, counter),
746        Expr::Coalesce(l, r) => {
747            rewrite_agg_expr(l, aggs, counter);
748            rewrite_agg_expr(r, aggs, counter);
749        }
750        Expr::InList { expr: e, list, .. } => {
751            rewrite_agg_expr(e, aggs, counter);
752            for item in list {
753                rewrite_agg_expr(item, aggs, counter);
754            }
755        }
756        Expr::InSubquery { expr: e, .. } => {
757            rewrite_agg_expr(e, aggs, counter);
758        }
759        _ => {}
760    }
761}
762
763fn find_or_insert_agg(
764    aggs: &mut Vec<GroupAgg>,
765    func: AggFunc,
766    field: &str,
767    counter: &mut usize,
768) -> String {
769    for existing in aggs.iter() {
770        if existing.function == func && existing.field == field {
771            return existing.output_name.clone();
772        }
773    }
774    let output_name = format!("__agg_{counter}");
775    aggs.push(GroupAgg {
776        function: func,
777        field: field.to_string(),
778        output_name: output_name.clone(),
779    });
780    *counter += 1;
781    output_name
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787    use crate::plan::PlanNode;
788
789    #[test]
790    fn test_plan_simple_scan() {
791        let plan = plan("User").unwrap();
792        assert!(matches!(plan, PlanNode::SeqScan { table } if table == "User"));
793    }
794
795    #[test]
796    fn test_plan_filter() {
797        let plan = plan("User filter .age > 30").unwrap();
798        assert!(matches!(plan, PlanNode::RangeScan { .. }));
799    }
800
801    #[test]
802    fn test_plan_filter_with_projection() {
803        let plan = plan("User filter .age > 30 { name, email }").unwrap();
804        assert!(matches!(plan, PlanNode::Project { .. }));
805    }
806
807    #[test]
808    fn test_plan_insert() {
809        let plan = plan(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
810        assert!(matches!(plan, PlanNode::Insert { .. }));
811    }
812
813    #[test]
814    fn test_plan_order_limit() {
815        let plan = plan("User order .name limit 10").unwrap();
816        match plan {
817            PlanNode::Limit { input, .. } => {
818                assert!(matches!(*input, PlanNode::Sort { .. }));
819            }
820            _ => panic!("expected Limit(Sort(SeqScan))"),
821        }
822    }
823
824    #[test]
825    fn test_plan_count() {
826        let plan = plan("count(User)").unwrap();
827        assert!(matches!(plan, PlanNode::Aggregate { .. }));
828    }
829
830    #[test]
831    fn test_plan_eq_becomes_index_scan() {
832        // `filter .col = literal` should fold into an IndexScan — the executor
833        // falls back to a scan if the column happens to lack an index.
834        let plan = plan("User filter .id = 42").unwrap();
835        match plan {
836            PlanNode::IndexScan { table, column, key } => {
837                assert_eq!(table, "User");
838                assert_eq!(column, "id");
839                assert!(matches!(key, Expr::Literal(Literal::Int(42))));
840            }
841            other => panic!("expected IndexScan, got {other:?}"),
842        }
843    }
844
845    #[test]
846    fn test_plan_eq_reversed_becomes_index_scan() {
847        // Literal-on-the-left form should fold the same way.
848        let plan = plan(r#"User filter "NYC" = .city"#).unwrap();
849        assert!(matches!(plan, PlanNode::IndexScan { .. }));
850    }
851
852    #[test]
853    fn test_plan_non_eq_stays_filter() {
854        // `>` now emits a RangeScan instead of SeqScan+Filter.
855        let plan = plan("User filter .age > 30").unwrap();
856        match plan {
857            PlanNode::RangeScan {
858                column, start, end, ..
859            } => {
860                assert_eq!(column, "age");
861                assert!(start.is_some(), "expected lower bound");
862                assert!(end.is_none(), "expected no upper bound");
863                let (_, inclusive) = start.unwrap();
864                assert!(!inclusive, "expected exclusive lower bound for >");
865            }
866            other => panic!("expected RangeScan, got {other:?}"),
867        }
868    }
869
870    #[test]
871    fn test_plan_index_scan_with_projection() {
872        // Projection on top of an IndexScan should layer correctly.
873        let plan = plan("User filter .id = 1 { .name }").unwrap();
874        match plan {
875            PlanNode::Project { input, .. } => {
876                assert!(matches!(*input, PlanNode::IndexScan { .. }));
877            }
878            other => panic!("expected Project(IndexScan), got {other:?}"),
879        }
880    }
881
882    #[test]
883    fn test_plan_update_by_pk_becomes_index_scan() {
884        // `.id = literal` update should fold to Update(IndexScan), not
885        // Update(Filter(SeqScan)).
886        let plan = plan("User filter .id = 42 update { age := 31 }").unwrap();
887        match plan {
888            PlanNode::Update { input, .. } => {
889                assert!(
890                    matches!(*input, PlanNode::IndexScan { .. }),
891                    "expected Update(IndexScan), got {input:?}"
892                );
893            }
894            other => panic!("expected Update, got {other:?}"),
895        }
896    }
897
898    #[test]
899    fn test_plan_update_range_stays_range_scan() {
900        let plan = plan("User filter .age > 30 update { age := 31 }").unwrap();
901        match plan {
902            PlanNode::Update { input, .. } => {
903                assert!(
904                    matches!(*input, PlanNode::RangeScan { .. }),
905                    "expected Update(RangeScan), got {input:?}"
906                );
907            }
908            other => panic!("expected Update, got {other:?}"),
909        }
910    }
911
912    #[test]
913    fn test_plan_delete_by_pk_becomes_index_scan() {
914        let plan = plan("User filter .id = 7 delete").unwrap();
915        match plan {
916            PlanNode::Delete { input, .. } => {
917                assert!(matches!(*input, PlanNode::IndexScan { .. }));
918            }
919            other => panic!("expected Delete, got {other:?}"),
920        }
921    }
922
923    #[test]
924    fn test_plan_inner_join_builds_nested_loop() {
925        // Mission E1.2: a join query should plan to NestedLoopJoin with
926        // AliasScan leaves on both sides.
927        let plan = plan("User as u join Order as o on u.id = o.user_id").unwrap();
928        match plan {
929            PlanNode::NestedLoopJoin {
930                left,
931                right,
932                on,
933                kind,
934            } => {
935                assert_eq!(kind, JoinKind::Inner);
936                assert!(on.is_some());
937                assert!(matches!(*left, PlanNode::AliasScan { .. }));
938                assert!(matches!(*right, PlanNode::AliasScan { .. }));
939            }
940            other => panic!("expected NestedLoopJoin, got {other:?}"),
941        }
942    }
943
944    #[test]
945    fn test_plan_right_join_rewritten_as_left_with_swapped_inputs() {
946        let plan = plan("User as u right join Order as o on u.id = o.user_id").unwrap();
947        match plan {
948            PlanNode::NestedLoopJoin {
949                left, right, kind, ..
950            } => {
951                assert_eq!(kind, JoinKind::LeftOuter);
952                // Swapped: Order is now on the left, User on the right.
953                match *left {
954                    PlanNode::AliasScan { table, .. } => assert_eq!(table, "Order"),
955                    other => panic!("expected AliasScan(Order), got {other:?}"),
956                }
957                match *right {
958                    PlanNode::AliasScan { table, .. } => assert_eq!(table, "User"),
959                    other => panic!("expected AliasScan(User), got {other:?}"),
960                }
961            }
962            other => panic!("expected NestedLoopJoin, got {other:?}"),
963        }
964    }
965
966    #[test]
967    fn test_plan_multi_join_is_left_deep() {
968        // Three sources → two NestedLoopJoins, left-deep.
969        let plan = plan(
970            "User as u join Order as o on u.id = o.user_id \
971             join Product as p on o.product_id = p.id",
972        )
973        .unwrap();
974        match plan {
975            PlanNode::NestedLoopJoin { left, right, .. } => {
976                // Outer (Product) join: right is AliasScan(Product)
977                match *right {
978                    PlanNode::AliasScan { table, .. } => assert_eq!(table, "Product"),
979                    other => panic!("expected AliasScan(Product), got {other:?}"),
980                }
981                // Outer.left is inner (Order) NestedLoopJoin
982                assert!(matches!(*left, PlanNode::NestedLoopJoin { .. }));
983            }
984            other => panic!("expected NestedLoopJoin, got {other:?}"),
985        }
986    }
987
988    #[test]
989    fn test_plan_join_with_filter_tail_wraps_filter_on_top() {
990        let plan =
991            plan("User as u join Order as o on u.id = o.user_id filter o.total > 100").unwrap();
992        match plan {
993            PlanNode::Filter { input, .. } => {
994                assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
995            }
996            other => panic!("expected Filter(NestedLoopJoin), got {other:?}"),
997        }
998    }
999
1000    #[test]
1001    fn test_plan_group_by_builds_groupby_node() {
1002        let plan = plan("User group .status { .status, n: count(.name) }").unwrap();
1003        // Should be Project(GroupBy(SeqScan)).
1004        match plan {
1005            PlanNode::Project { input, fields } => {
1006                assert_eq!(fields.len(), 2);
1007                match *input {
1008                    PlanNode::GroupBy {
1009                        input: inner,
1010                        keys,
1011                        aggregates,
1012                        having,
1013                    } => {
1014                        assert!(matches!(*inner, PlanNode::SeqScan { .. }));
1015                        assert_eq!(keys, vec![GroupKey::Unqualified("status".into())]);
1016                        assert_eq!(aggregates.len(), 1);
1017                        assert_eq!(aggregates[0].function, AggFunc::Count);
1018                        assert_eq!(aggregates[0].field, "name");
1019                        assert!(having.is_none());
1020                    }
1021                    other => panic!("expected GroupBy, got {other:?}"),
1022                }
1023            }
1024            other => panic!("expected Project, got {other:?}"),
1025        }
1026    }
1027
1028    #[test]
1029    fn test_plan_group_by_having_rewrites_agg_in_having() {
1030        let plan = plan("User group .status having count(.name) > 1 { .status }").unwrap();
1031        match plan {
1032            PlanNode::Project { input, .. } => {
1033                match *input {
1034                    PlanNode::GroupBy {
1035                        having, aggregates, ..
1036                    } => {
1037                        // The planner should have extracted count(.name) into
1038                        // aggregates and rewritten the HAVING to reference __agg_0.
1039                        assert_eq!(aggregates.len(), 1);
1040                        assert_eq!(aggregates[0].output_name, "__agg_0");
1041                        let h = having.expect("having should be Some");
1042                        match h {
1043                            Expr::BinaryOp(l, BinOp::Gt, _) => {
1044                                assert!(
1045                                    matches!(*l, Expr::Field(ref name) if name == "__agg_0"),
1046                                    "expected Field(__agg_0), got {l:?}"
1047                                );
1048                            }
1049                            other => panic!("expected BinaryOp, got {other:?}"),
1050                        }
1051                    }
1052                    other => panic!("expected GroupBy, got {other:?}"),
1053                }
1054            }
1055            other => panic!("expected Project, got {other:?}"),
1056        }
1057    }
1058
1059    #[test]
1060    fn test_plan_window_inserts_window_node_before_project() {
1061        let plan = plan("User { .name, rn: row_number() over (order .age) }").unwrap();
1062        // Expected shape: Project(Window(SeqScan))
1063        match plan {
1064            PlanNode::Project { input, fields } => {
1065                assert_eq!(fields.len(), 2);
1066                // The window expr should have been replaced with Field("__win_0")
1067                assert!(
1068                    matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"),
1069                    "expected Field(__win_0), got {:?}",
1070                    fields[1].expr
1071                );
1072                match *input {
1073                    PlanNode::Window {
1074                        input: inner,
1075                        windows,
1076                    } => {
1077                        assert_eq!(windows.len(), 1);
1078                        assert_eq!(windows[0].output_name, "__win_0");
1079                        assert!(matches!(*inner, PlanNode::SeqScan { .. }));
1080                    }
1081                    other => panic!("expected Window, got {other:?}"),
1082                }
1083            }
1084            other => panic!("expected Project, got {other:?}"),
1085        }
1086    }
1087
1088    #[test]
1089    fn test_plan_multiple_windows() {
1090        let plan = plan(
1091            "User { .name, rn: row_number() over (order .age), s: sum(.salary) over (partition .dept order .salary) }"
1092        ).unwrap();
1093        match plan {
1094            PlanNode::Project { input, fields } => {
1095                assert_eq!(fields.len(), 3);
1096                assert!(matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"));
1097                assert!(matches!(&fields[2].expr, Expr::Field(name) if name == "__win_1"));
1098                match *input {
1099                    PlanNode::Window { windows, .. } => {
1100                        assert_eq!(windows.len(), 2);
1101                        assert_eq!(windows[0].output_name, "__win_0");
1102                        assert_eq!(windows[1].output_name, "__win_1");
1103                    }
1104                    other => panic!("expected Window, got {other:?}"),
1105                }
1106            }
1107            other => panic!("expected Project, got {other:?}"),
1108        }
1109    }
1110
1111    #[test]
1112    fn test_plan_no_window_without_over() {
1113        // Plain aggregate in projection should not create a Window node.
1114        let plan = plan("User group .dept { .dept, total: sum(.salary) }").unwrap();
1115        match plan {
1116            PlanNode::Project { input, .. } => {
1117                // Input should be GroupBy, not Window.
1118                assert!(
1119                    matches!(*input, PlanNode::GroupBy { .. }),
1120                    "expected GroupBy under Project, got {:?}",
1121                    input
1122                );
1123            }
1124            other => panic!("expected Project, got {other:?}"),
1125        }
1126    }
1127
1128    #[test]
1129    fn test_plan_explain_wraps_inner() {
1130        let plan = plan("explain User filter .age > 30").unwrap();
1131        match plan {
1132            PlanNode::Explain { input } => {
1133                assert!(
1134                    matches!(*input, PlanNode::RangeScan { .. }),
1135                    "expected Explain(RangeScan), got {:?}",
1136                    input
1137                );
1138            }
1139            other => panic!("expected Explain, got {other:?}"),
1140        }
1141    }
1142
1143    #[test]
1144    fn test_plan_explain_simple_scan() {
1145        let plan = plan("explain User").unwrap();
1146        match plan {
1147            PlanNode::Explain { input } => {
1148                assert!(matches!(*input, PlanNode::SeqScan { .. }));
1149            }
1150            other => panic!("expected Explain(SeqScan), got {other:?}"),
1151        }
1152    }
1153
1154    #[test]
1155    fn test_plan_explain_join() {
1156        let plan = plan("explain User as u join Order as o on u.id = o.user_id").unwrap();
1157        match plan {
1158            PlanNode::Explain { input } => {
1159                assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
1160            }
1161            other => panic!("expected Explain(NestedLoopJoin), got {other:?}"),
1162        }
1163    }
1164}