Skip to main content

powdb_query/
planner.rs

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