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 land here too once the
727            // v0.12 grammar exists; they will be added alongside that feature.
728            let field_name = match inner.as_ref() {
729                Expr::Field(name) => Some(name.clone()),
730                Expr::QualifiedField { qualifier, field } => Some(format!("{qualifier}.{field}")),
731                _ => None,
732            };
733            if let Some(name) = field_name {
734                let output = find_or_insert_agg(aggs, *func, &name, counter);
735                *expr = Expr::Field(output);
736            }
737        }
738        Expr::BinaryOp(l, _, r) => {
739            rewrite_agg_expr(l, aggs, counter);
740            rewrite_agg_expr(r, aggs, counter);
741        }
742        Expr::UnaryOp(_, inner) => rewrite_agg_expr(inner, aggs, counter),
743        Expr::Coalesce(l, r) => {
744            rewrite_agg_expr(l, aggs, counter);
745            rewrite_agg_expr(r, aggs, counter);
746        }
747        Expr::InList { expr: e, list, .. } => {
748            rewrite_agg_expr(e, aggs, counter);
749            for item in list {
750                rewrite_agg_expr(item, aggs, counter);
751            }
752        }
753        Expr::InSubquery { expr: e, .. } => {
754            rewrite_agg_expr(e, aggs, counter);
755        }
756        _ => {}
757    }
758}
759
760fn find_or_insert_agg(
761    aggs: &mut Vec<GroupAgg>,
762    func: AggFunc,
763    field: &str,
764    counter: &mut usize,
765) -> String {
766    for existing in aggs.iter() {
767        if existing.function == func && existing.field == field {
768            return existing.output_name.clone();
769        }
770    }
771    let output_name = format!("__agg_{counter}");
772    aggs.push(GroupAgg {
773        function: func,
774        field: field.to_string(),
775        output_name: output_name.clone(),
776    });
777    *counter += 1;
778    output_name
779}
780
781#[cfg(test)]
782mod tests {
783    use super::*;
784    use crate::plan::PlanNode;
785
786    #[test]
787    fn test_plan_simple_scan() {
788        let plan = plan("User").unwrap();
789        assert!(matches!(plan, PlanNode::SeqScan { table } if table == "User"));
790    }
791
792    #[test]
793    fn test_plan_filter() {
794        let plan = plan("User filter .age > 30").unwrap();
795        assert!(matches!(plan, PlanNode::RangeScan { .. }));
796    }
797
798    #[test]
799    fn test_plan_filter_with_projection() {
800        let plan = plan("User filter .age > 30 { name, email }").unwrap();
801        assert!(matches!(plan, PlanNode::Project { .. }));
802    }
803
804    #[test]
805    fn test_plan_insert() {
806        let plan = plan(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
807        assert!(matches!(plan, PlanNode::Insert { .. }));
808    }
809
810    #[test]
811    fn test_plan_order_limit() {
812        let plan = plan("User order .name limit 10").unwrap();
813        match plan {
814            PlanNode::Limit { input, .. } => {
815                assert!(matches!(*input, PlanNode::Sort { .. }));
816            }
817            _ => panic!("expected Limit(Sort(SeqScan))"),
818        }
819    }
820
821    #[test]
822    fn test_plan_count() {
823        let plan = plan("count(User)").unwrap();
824        assert!(matches!(plan, PlanNode::Aggregate { .. }));
825    }
826
827    #[test]
828    fn test_plan_eq_becomes_index_scan() {
829        // `filter .col = literal` should fold into an IndexScan — the executor
830        // falls back to a scan if the column happens to lack an index.
831        let plan = plan("User filter .id = 42").unwrap();
832        match plan {
833            PlanNode::IndexScan { table, column, key } => {
834                assert_eq!(table, "User");
835                assert_eq!(column, "id");
836                assert!(matches!(key, Expr::Literal(Literal::Int(42))));
837            }
838            other => panic!("expected IndexScan, got {other:?}"),
839        }
840    }
841
842    #[test]
843    fn test_plan_eq_reversed_becomes_index_scan() {
844        // Literal-on-the-left form should fold the same way.
845        let plan = plan(r#"User filter "NYC" = .city"#).unwrap();
846        assert!(matches!(plan, PlanNode::IndexScan { .. }));
847    }
848
849    #[test]
850    fn test_plan_non_eq_stays_filter() {
851        // `>` now emits a RangeScan instead of SeqScan+Filter.
852        let plan = plan("User filter .age > 30").unwrap();
853        match plan {
854            PlanNode::RangeScan {
855                column, start, end, ..
856            } => {
857                assert_eq!(column, "age");
858                assert!(start.is_some(), "expected lower bound");
859                assert!(end.is_none(), "expected no upper bound");
860                let (_, inclusive) = start.unwrap();
861                assert!(!inclusive, "expected exclusive lower bound for >");
862            }
863            other => panic!("expected RangeScan, got {other:?}"),
864        }
865    }
866
867    #[test]
868    fn test_plan_index_scan_with_projection() {
869        // Projection on top of an IndexScan should layer correctly.
870        let plan = plan("User filter .id = 1 { .name }").unwrap();
871        match plan {
872            PlanNode::Project { input, .. } => {
873                assert!(matches!(*input, PlanNode::IndexScan { .. }));
874            }
875            other => panic!("expected Project(IndexScan), got {other:?}"),
876        }
877    }
878
879    #[test]
880    fn test_plan_update_by_pk_becomes_index_scan() {
881        // `.id = literal` update should fold to Update(IndexScan), not
882        // Update(Filter(SeqScan)).
883        let plan = plan("User filter .id = 42 update { age := 31 }").unwrap();
884        match plan {
885            PlanNode::Update { input, .. } => {
886                assert!(
887                    matches!(*input, PlanNode::IndexScan { .. }),
888                    "expected Update(IndexScan), got {input:?}"
889                );
890            }
891            other => panic!("expected Update, got {other:?}"),
892        }
893    }
894
895    #[test]
896    fn test_plan_update_range_stays_range_scan() {
897        let plan = plan("User filter .age > 30 update { age := 31 }").unwrap();
898        match plan {
899            PlanNode::Update { input, .. } => {
900                assert!(
901                    matches!(*input, PlanNode::RangeScan { .. }),
902                    "expected Update(RangeScan), got {input:?}"
903                );
904            }
905            other => panic!("expected Update, got {other:?}"),
906        }
907    }
908
909    #[test]
910    fn test_plan_delete_by_pk_becomes_index_scan() {
911        let plan = plan("User filter .id = 7 delete").unwrap();
912        match plan {
913            PlanNode::Delete { input, .. } => {
914                assert!(matches!(*input, PlanNode::IndexScan { .. }));
915            }
916            other => panic!("expected Delete, got {other:?}"),
917        }
918    }
919
920    #[test]
921    fn test_plan_inner_join_builds_nested_loop() {
922        // Mission E1.2: a join query should plan to NestedLoopJoin with
923        // AliasScan leaves on both sides.
924        let plan = plan("User as u join Order as o on u.id = o.user_id").unwrap();
925        match plan {
926            PlanNode::NestedLoopJoin {
927                left,
928                right,
929                on,
930                kind,
931            } => {
932                assert_eq!(kind, JoinKind::Inner);
933                assert!(on.is_some());
934                assert!(matches!(*left, PlanNode::AliasScan { .. }));
935                assert!(matches!(*right, PlanNode::AliasScan { .. }));
936            }
937            other => panic!("expected NestedLoopJoin, got {other:?}"),
938        }
939    }
940
941    #[test]
942    fn test_plan_right_join_rewritten_as_left_with_swapped_inputs() {
943        let plan = plan("User as u right join Order as o on u.id = o.user_id").unwrap();
944        match plan {
945            PlanNode::NestedLoopJoin {
946                left, right, kind, ..
947            } => {
948                assert_eq!(kind, JoinKind::LeftOuter);
949                // Swapped: Order is now on the left, User on the right.
950                match *left {
951                    PlanNode::AliasScan { table, .. } => assert_eq!(table, "Order"),
952                    other => panic!("expected AliasScan(Order), got {other:?}"),
953                }
954                match *right {
955                    PlanNode::AliasScan { table, .. } => assert_eq!(table, "User"),
956                    other => panic!("expected AliasScan(User), got {other:?}"),
957                }
958            }
959            other => panic!("expected NestedLoopJoin, got {other:?}"),
960        }
961    }
962
963    #[test]
964    fn test_plan_multi_join_is_left_deep() {
965        // Three sources → two NestedLoopJoins, left-deep.
966        let plan = plan(
967            "User as u join Order as o on u.id = o.user_id \
968             join Product as p on o.product_id = p.id",
969        )
970        .unwrap();
971        match plan {
972            PlanNode::NestedLoopJoin { left, right, .. } => {
973                // Outer (Product) join: right is AliasScan(Product)
974                match *right {
975                    PlanNode::AliasScan { table, .. } => assert_eq!(table, "Product"),
976                    other => panic!("expected AliasScan(Product), got {other:?}"),
977                }
978                // Outer.left is inner (Order) NestedLoopJoin
979                assert!(matches!(*left, PlanNode::NestedLoopJoin { .. }));
980            }
981            other => panic!("expected NestedLoopJoin, got {other:?}"),
982        }
983    }
984
985    #[test]
986    fn test_plan_join_with_filter_tail_wraps_filter_on_top() {
987        let plan =
988            plan("User as u join Order as o on u.id = o.user_id filter o.total > 100").unwrap();
989        match plan {
990            PlanNode::Filter { input, .. } => {
991                assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
992            }
993            other => panic!("expected Filter(NestedLoopJoin), got {other:?}"),
994        }
995    }
996
997    #[test]
998    fn test_plan_group_by_builds_groupby_node() {
999        let plan = plan("User group .status { .status, n: count(.name) }").unwrap();
1000        // Should be Project(GroupBy(SeqScan)).
1001        match plan {
1002            PlanNode::Project { input, fields } => {
1003                assert_eq!(fields.len(), 2);
1004                match *input {
1005                    PlanNode::GroupBy {
1006                        input: inner,
1007                        keys,
1008                        aggregates,
1009                        having,
1010                    } => {
1011                        assert!(matches!(*inner, PlanNode::SeqScan { .. }));
1012                        assert_eq!(keys, vec![GroupKey::Unqualified("status".into())]);
1013                        assert_eq!(aggregates.len(), 1);
1014                        assert_eq!(aggregates[0].function, AggFunc::Count);
1015                        assert_eq!(aggregates[0].field, "name");
1016                        assert!(having.is_none());
1017                    }
1018                    other => panic!("expected GroupBy, got {other:?}"),
1019                }
1020            }
1021            other => panic!("expected Project, got {other:?}"),
1022        }
1023    }
1024
1025    #[test]
1026    fn test_plan_group_by_having_rewrites_agg_in_having() {
1027        let plan = plan("User group .status having count(.name) > 1 { .status }").unwrap();
1028        match plan {
1029            PlanNode::Project { input, .. } => {
1030                match *input {
1031                    PlanNode::GroupBy {
1032                        having, aggregates, ..
1033                    } => {
1034                        // The planner should have extracted count(.name) into
1035                        // aggregates and rewritten the HAVING to reference __agg_0.
1036                        assert_eq!(aggregates.len(), 1);
1037                        assert_eq!(aggregates[0].output_name, "__agg_0");
1038                        let h = having.expect("having should be Some");
1039                        match h {
1040                            Expr::BinaryOp(l, BinOp::Gt, _) => {
1041                                assert!(
1042                                    matches!(*l, Expr::Field(ref name) if name == "__agg_0"),
1043                                    "expected Field(__agg_0), got {l:?}"
1044                                );
1045                            }
1046                            other => panic!("expected BinaryOp, got {other:?}"),
1047                        }
1048                    }
1049                    other => panic!("expected GroupBy, got {other:?}"),
1050                }
1051            }
1052            other => panic!("expected Project, got {other:?}"),
1053        }
1054    }
1055
1056    #[test]
1057    fn test_plan_window_inserts_window_node_before_project() {
1058        let plan = plan("User { .name, rn: row_number() over (order .age) }").unwrap();
1059        // Expected shape: Project(Window(SeqScan))
1060        match plan {
1061            PlanNode::Project { input, fields } => {
1062                assert_eq!(fields.len(), 2);
1063                // The window expr should have been replaced with Field("__win_0")
1064                assert!(
1065                    matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"),
1066                    "expected Field(__win_0), got {:?}",
1067                    fields[1].expr
1068                );
1069                match *input {
1070                    PlanNode::Window {
1071                        input: inner,
1072                        windows,
1073                    } => {
1074                        assert_eq!(windows.len(), 1);
1075                        assert_eq!(windows[0].output_name, "__win_0");
1076                        assert!(matches!(*inner, PlanNode::SeqScan { .. }));
1077                    }
1078                    other => panic!("expected Window, got {other:?}"),
1079                }
1080            }
1081            other => panic!("expected Project, got {other:?}"),
1082        }
1083    }
1084
1085    #[test]
1086    fn test_plan_multiple_windows() {
1087        let plan = plan(
1088            "User { .name, rn: row_number() over (order .age), s: sum(.salary) over (partition .dept order .salary) }"
1089        ).unwrap();
1090        match plan {
1091            PlanNode::Project { input, fields } => {
1092                assert_eq!(fields.len(), 3);
1093                assert!(matches!(&fields[1].expr, Expr::Field(name) if name == "__win_0"));
1094                assert!(matches!(&fields[2].expr, Expr::Field(name) if name == "__win_1"));
1095                match *input {
1096                    PlanNode::Window { windows, .. } => {
1097                        assert_eq!(windows.len(), 2);
1098                        assert_eq!(windows[0].output_name, "__win_0");
1099                        assert_eq!(windows[1].output_name, "__win_1");
1100                    }
1101                    other => panic!("expected Window, got {other:?}"),
1102                }
1103            }
1104            other => panic!("expected Project, got {other:?}"),
1105        }
1106    }
1107
1108    #[test]
1109    fn test_plan_no_window_without_over() {
1110        // Plain aggregate in projection should not create a Window node.
1111        let plan = plan("User group .dept { .dept, total: sum(.salary) }").unwrap();
1112        match plan {
1113            PlanNode::Project { input, .. } => {
1114                // Input should be GroupBy, not Window.
1115                assert!(
1116                    matches!(*input, PlanNode::GroupBy { .. }),
1117                    "expected GroupBy under Project, got {:?}",
1118                    input
1119                );
1120            }
1121            other => panic!("expected Project, got {other:?}"),
1122        }
1123    }
1124
1125    #[test]
1126    fn test_plan_explain_wraps_inner() {
1127        let plan = plan("explain User filter .age > 30").unwrap();
1128        match plan {
1129            PlanNode::Explain { input } => {
1130                assert!(
1131                    matches!(*input, PlanNode::RangeScan { .. }),
1132                    "expected Explain(RangeScan), got {:?}",
1133                    input
1134                );
1135            }
1136            other => panic!("expected Explain, got {other:?}"),
1137        }
1138    }
1139
1140    #[test]
1141    fn test_plan_explain_simple_scan() {
1142        let plan = plan("explain User").unwrap();
1143        match plan {
1144            PlanNode::Explain { input } => {
1145                assert!(matches!(*input, PlanNode::SeqScan { .. }));
1146            }
1147            other => panic!("expected Explain(SeqScan), got {other:?}"),
1148        }
1149    }
1150
1151    #[test]
1152    fn test_plan_explain_join() {
1153        let plan = plan("explain User as u join Order as o on u.id = o.user_id").unwrap();
1154        match plan {
1155            PlanNode::Explain { input } => {
1156                assert!(matches!(*input, PlanNode::NestedLoopJoin { .. }));
1157            }
1158            other => panic!("expected Explain(NestedLoopJoin), got {other:?}"),
1159        }
1160    }
1161}