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