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