Skip to main content

alopex_sql/planner/
mod.rs

1//! Query planning module for the Alopex SQL dialect.
2//!
3//! This module provides:
4//! - [`PlannerError`]: Error types for planning phase
5//! - [`ResolvedType`]: Normalized type information for type checking
6//! - [`TypedExpr`]: Type-checked expressions with resolved types
7//! - [`LogicalPlan`]: Logical query plan representation
8//! - [`NameResolver`]: Table and column reference resolution
9//! - [`TypeChecker`]: Expression type inference and validation
10//! - [`Planner`]: Main entry point for converting AST to LogicalPlan
11
12pub mod aggregate_expr;
13mod error;
14pub mod knn_optimizer;
15pub mod logical_plan;
16pub mod name_resolver;
17mod named_window;
18pub mod type_checker;
19pub mod typed_expr;
20pub mod types;
21
22#[cfg(test)]
23mod planner_tests;
24
25pub use aggregate_expr::{AggregateExpr, AggregateFunction};
26pub use error::PlannerError;
27pub use knn_optimizer::{KnnPattern, SortDirection, detect_knn_pattern};
28pub use logical_plan::{
29    JoinType, LogicalPlan, OffsetWindowFunction, RecursiveCteLimits, SetOperator,
30    TableFunctionKind, ValueWindowFunction, WindowExpr, WindowFunction,
31};
32pub use name_resolver::{NameResolver, ResolvedColumn};
33pub use type_checker::{ScopedTable, TypeChecker};
34pub use typed_expr::{
35    ProjectedColumn, Projection, SortExpr, TypedAssignment, TypedCaseWhen, TypedExpr, TypedExprKind,
36};
37pub use types::ResolvedType;
38
39use crate::ast::ddl::{
40    ColumnConstraint, ColumnDef, CreateIndex, CreateTable, DropIndex, DropTable,
41};
42use crate::ast::dml::{
43    Delete, FromItem, GroupByItem, Insert, InsertSource, LITERAL_TABLE, OrderByExpr, QueryBody,
44    Select, SelectItem, SetOperation as AstSetOperation, SetOperator as AstSetOperator, Update,
45    Values,
46};
47use crate::ast::expr::{Expr, ExprKind, Literal};
48use crate::ast::{PragmaValue, Spanned, Statement, StatementKind};
49use crate::catalog::{Catalog, ColumnMetadata, IndexMetadata, TableMetadata};
50use crate::{AlopexDialect, DataSourceFormat, Parser, SqlError, TableType};
51use named_window::resolve_named_windows;
52use std::collections::{HashMap, HashSet};
53
54#[derive(Clone)]
55struct PlannedRelation {
56    plan: LogicalPlan,
57    schema: Vec<ColumnMetadata>,
58    scope: Vec<ScopedTable>,
59}
60
61struct WindowSelectStages {
62    plan: LogicalPlan,
63    windows: Vec<WindowExpr>,
64    qualify: Option<TypedExpr>,
65    projection: Projection,
66    order_by: Vec<SortExpr>,
67    window_schema: Vec<ColumnMetadata>,
68}
69
70type CtePlans = HashMap<String, PlannedRelation>;
71
72fn direct_from_reference_count(items: &[FromItem], name: &str) -> usize {
73    items
74        .iter()
75        .map(|item| match item {
76            FromItem::Table {
77                name: table_name, ..
78            } => usize::from(table_name == name),
79            FromItem::Join { left, right, .. } => {
80                direct_from_item_reference_count(left, name)
81                    + direct_from_item_reference_count(right, name)
82            }
83            FromItem::Derived { .. } | FromItem::Function { .. } => 0,
84        })
85        .sum()
86}
87
88fn direct_from_item_reference_count(item: &FromItem, name: &str) -> usize {
89    match item {
90        FromItem::Table {
91            name: table_name, ..
92        } => usize::from(table_name == name),
93        FromItem::Join { left, right, .. } => {
94            direct_from_item_reference_count(left, name)
95                + direct_from_item_reference_count(right, name)
96        }
97        FromItem::Derived { .. } | FromItem::Function { .. } => 0,
98    }
99}
100
101fn select_table_reference_count(select: &Select, name: &str) -> usize {
102    fn from_item_count(item: &FromItem, name: &str) -> usize {
103        match item {
104            FromItem::Table {
105                name: table_name, ..
106            } => usize::from(table_name == name),
107            FromItem::Join { left, right, .. } => {
108                from_item_count(left, name) + from_item_count(right, name)
109            }
110            FromItem::Derived { subquery, .. } => query_body_table_reference_count(subquery, name),
111            // A table-function argument is an expression; expression-level
112            // subqueries are outside this FROM-shape count, exactly as they
113            // are for WHERE and the projection.
114            FromItem::Function { .. } => 0,
115        }
116    }
117
118    let from_count = select
119        .from
120        .iter()
121        .map(|item| from_item_count(item, name))
122        .sum::<usize>();
123    let set_count = select
124        .set_operations
125        .iter()
126        .map(|operation| query_body_table_reference_count(&operation.right, name))
127        .sum::<usize>();
128    let with_count = select.with.as_ref().map_or(0, |with| {
129        with.ctes
130            .iter()
131            .map(|cte| query_body_table_reference_count(&cte.query, name))
132            .sum()
133    });
134    from_count + set_count + with_count
135}
136
137fn values_table_reference_count(values: &Values, name: &str) -> usize {
138    let set_count = values
139        .set_operations
140        .iter()
141        .map(|operation| query_body_table_reference_count(&operation.right, name))
142        .sum::<usize>();
143    let with_count = values.with.as_ref().map_or(0, |with| {
144        with.ctes
145            .iter()
146            .map(|cte| query_body_table_reference_count(&cte.query, name))
147            .sum()
148    });
149    set_count + with_count
150}
151
152fn query_body_table_reference_count(body: &QueryBody, name: &str) -> usize {
153    match body {
154        QueryBody::Select(select) => select_table_reference_count(select, name),
155        QueryBody::Values(values) => values_table_reference_count(values, name),
156    }
157}
158
159fn cte_dependency_cycle(with: &crate::ast::WithClause) -> bool {
160    fn visit(node: usize, dependencies: &[Vec<usize>], state: &mut [u8]) -> bool {
161        state[node] = 1;
162        for &dependency in &dependencies[node] {
163            if state[dependency] == 1
164                || (state[dependency] == 0 && visit(dependency, dependencies, state))
165            {
166                return true;
167            }
168        }
169        state[node] = 2;
170        false
171    }
172
173    let dependencies = with
174        .ctes
175        .iter()
176        .map(|cte| {
177            with.ctes
178                .iter()
179                .enumerate()
180                .filter_map(|(dependency, candidate)| {
181                    (query_body_table_reference_count(&cte.query, &candidate.name) > 0)
182                        .then_some(dependency)
183                })
184                .collect::<Vec<_>>()
185        })
186        .collect::<Vec<_>>();
187    let mut state = vec![0; dependencies.len()];
188    (0..dependencies.len()).any(|node| state[node] == 0 && visit(node, &dependencies, &mut state))
189}
190
191fn expr_contains_subquery(expr: &Expr) -> bool {
192    match &expr.kind {
193        ExprKind::Literal { .. } | ExprKind::ColumnRef { .. } | ExprKind::VectorLiteral { .. } => {
194            false
195        }
196        ExprKind::BinaryOp { left, right, .. } => {
197            expr_contains_subquery(left) || expr_contains_subquery(right)
198        }
199        ExprKind::UnaryOp { operand, .. } => expr_contains_subquery(operand),
200        ExprKind::TruthPredicate { expr, .. } => expr_contains_subquery(expr),
201        ExprKind::IsDistinctFrom { left, right, .. } => {
202            expr_contains_subquery(left) || expr_contains_subquery(right)
203        }
204        ExprKind::Row { items } => items.iter().any(expr_contains_subquery),
205        ExprKind::Case {
206            operand,
207            branches,
208            else_expr,
209        } => {
210            operand.as_deref().is_some_and(expr_contains_subquery)
211                || branches.iter().any(|branch| {
212                    expr_contains_subquery(&branch.when) || expr_contains_subquery(&branch.then)
213                })
214                || else_expr.as_deref().is_some_and(expr_contains_subquery)
215        }
216        ExprKind::FunctionCall {
217            args,
218            order_by,
219            within_group,
220            filter,
221            over,
222            ..
223        } => {
224            args.iter().any(expr_contains_subquery)
225                || order_by
226                    .iter()
227                    .any(|order| expr_contains_subquery(&order.expr))
228                || within_group
229                    .iter()
230                    .any(|order| expr_contains_subquery(&order.expr))
231                || filter.as_deref().is_some_and(expr_contains_subquery)
232                || over.as_ref().is_some_and(|window| {
233                    window.partition_by.iter().any(expr_contains_subquery)
234                        || window
235                            .order_by
236                            .iter()
237                            .any(|order| expr_contains_subquery(&order.expr))
238                })
239        }
240        ExprKind::Cast { expr, .. }
241        | ExprKind::TryCast { expr, .. }
242        | ExprKind::IsNull { expr, .. } => expr_contains_subquery(expr),
243        ExprKind::Between {
244            expr, low, high, ..
245        } => {
246            expr_contains_subquery(expr)
247                || expr_contains_subquery(low)
248                || expr_contains_subquery(high)
249        }
250        ExprKind::Like {
251            expr,
252            pattern,
253            escape,
254            ..
255        } => {
256            expr_contains_subquery(expr)
257                || expr_contains_subquery(pattern)
258                || escape.as_deref().is_some_and(expr_contains_subquery)
259        }
260        ExprKind::InList { expr, list, .. } => {
261            expr_contains_subquery(expr) || list.iter().any(expr_contains_subquery)
262        }
263        ExprKind::ScalarSubquery { .. }
264        | ExprKind::InSubquery { .. }
265        | ExprKind::Exists { .. }
266        | ExprKind::Quantified { .. } => true,
267    }
268}
269
270fn from_item_contains_subquery(item: &FromItem) -> bool {
271    match item {
272        FromItem::Table { .. } => false,
273        // A table function is a correlated relation source, so it is treated
274        // as a subquery boundary wherever one is rejected (issue #151).
275        FromItem::Derived { .. } | FromItem::Function { .. } => true,
276        FromItem::Join {
277            left,
278            right,
279            condition,
280            ..
281        } => {
282            from_item_contains_subquery(left)
283                || from_item_contains_subquery(right)
284                || condition.as_ref().is_some_and(expr_contains_subquery)
285        }
286    }
287}
288
289fn select_contains_subquery(select: &Select) -> bool {
290    select.projection.iter().any(|item| match item {
291        SelectItem::Expr { expr, .. } => expr_contains_subquery(expr),
292        SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => false,
293    }) || select.from.iter().any(from_item_contains_subquery)
294        || select
295            .selection
296            .as_ref()
297            .is_some_and(expr_contains_subquery)
298        || select.group_by.as_ref().is_some_and(|group| {
299            group
300                .iter()
301                .flat_map(GroupByItem::exprs)
302                .any(expr_contains_subquery)
303        })
304        || select.having.as_ref().is_some_and(expr_contains_subquery)
305        || select
306            .set_operations
307            .iter()
308            .any(|operation| query_body_contains_subquery(&operation.right))
309        || select
310            .order_by
311            .iter()
312            .any(|order| expr_contains_subquery(&order.expr))
313        || select.limit.as_ref().is_some_and(expr_contains_subquery)
314        || select.offset.as_ref().is_some_and(expr_contains_subquery)
315        || select.with.as_ref().is_some_and(|with| {
316            with.ctes
317                .iter()
318                .any(|cte| query_body_contains_subquery(&cte.query))
319        })
320}
321
322fn values_contains_subquery(values: &Values) -> bool {
323    values.rows.iter().flatten().any(expr_contains_subquery)
324        || values
325            .set_operations
326            .iter()
327            .any(|operation| query_body_contains_subquery(&operation.right))
328        || values
329            .order_by
330            .iter()
331            .any(|order| expr_contains_subquery(&order.expr))
332        || values.limit.as_ref().is_some_and(expr_contains_subquery)
333        || values.offset.as_ref().is_some_and(expr_contains_subquery)
334        || values.with.as_ref().is_some_and(|with| {
335            with.ctes
336                .iter()
337                .any(|cte| query_body_contains_subquery(&cte.query))
338        })
339}
340
341fn query_body_contains_subquery(body: &QueryBody) -> bool {
342    match body {
343        QueryBody::Select(select) => select_contains_subquery(select),
344        QueryBody::Values(values) => values_contains_subquery(values),
345    }
346}
347
348fn common_values_type(
349    current: &ResolvedType,
350    next: &ResolvedType,
351    span: crate::ast::Span,
352) -> Result<ResolvedType, PlannerError> {
353    use ResolvedType::{BigInt, Decimal, Double, Float, Integer, Null};
354
355    if *current == Null {
356        return Ok(next.clone());
357    }
358    if *next == Null || current == next {
359        return Ok(current.clone());
360    }
361    match (current, next) {
362        (Decimal { .. }, Decimal { .. }) => Ok(current.clone()),
363        (Integer, BigInt) | (BigInt, Integer) => Ok(BigInt),
364        (Integer | BigInt | Float | Double, Integer | BigInt | Float | Double) => Ok(Double),
365        _ => Err(PlannerError::type_mismatch(
366            current.type_name(),
367            next.type_name(),
368            span,
369        )),
370    }
371}
372
373/// Planning output used by server-side routing analysis.
374///
375/// This is intentionally owned by `alopex-sql` and contains no
376/// `alopex-cluster` types. Cluster routing layers can translate this DTO into
377/// their own routing model without making SQL depend on cluster metadata.
378#[derive(Debug, Clone)]
379pub struct PlannedStatement {
380    /// Logical plan produced by the regular SQL planner.
381    pub plan: LogicalPlan,
382    /// SQL-owned routing input derived during planning.
383    pub routing_input: RoutingInput,
384}
385
386impl PlannedStatement {
387    /// Statement kind associated with this plan.
388    pub fn statement_kind(&self) -> &StatementKind {
389        &self.routing_input.statement_kind
390    }
391
392    /// Table references extracted for routing analysis.
393    pub fn table_references(&self) -> &[TableReference] {
394        &self.routing_input.table_references
395    }
396
397    /// Planning diagnostics available for routing layers to attach to their
398    /// own decision diagnostics.
399    pub fn diagnostics(&self) -> &[PlanningDiagnostic] {
400        &self.routing_input.diagnostics
401    }
402}
403
404/// SQL-owned input for routing decision composition.
405#[derive(Debug, Clone)]
406pub struct RoutingInput {
407    /// Original statement kind. Consumers should match on variants rather than
408    /// reparsing SQL.
409    pub statement_kind: StatementKind,
410    /// Conservative table references extracted from the planned statement.
411    pub table_references: Vec<TableReference>,
412    /// Diagnostics produced while preparing routing input.
413    pub diagnostics: Vec<PlanningDiagnostic>,
414}
415
416/// A table reference visible at the SQL planning boundary.
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub struct TableReference {
419    /// Table name as resolved by the current planner/catalog view.
420    pub table_name: String,
421    /// Access class requested by the statement.
422    pub access: TableReferenceAccess,
423    /// Extraction source for diagnostics and future extractor expansion.
424    pub source: TableReferenceSource,
425}
426
427impl TableReference {
428    pub fn new(
429        table_name: impl Into<String>,
430        access: TableReferenceAccess,
431        source: TableReferenceSource,
432    ) -> Self {
433        Self {
434            table_name: table_name.into(),
435            access,
436            source,
437        }
438    }
439}
440
441/// Access class for a table reference.
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443pub enum TableReferenceAccess {
444    /// Read-only scan/reference.
445    Read,
446    /// Data mutation against an existing table.
447    Write,
448    /// Table creation.
449    Create,
450    /// Table drop/removal.
451    Drop,
452    /// Metadata operation related to a table, such as CREATE INDEX.
453    Metadata,
454}
455
456/// Where a table reference was extracted from.
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub enum TableReferenceSource {
459    /// The existing `LogicalPlan::table_name()` single-table helper.
460    TopLevelPlanTableName,
461    /// A physical table scan in a logical plan tree.
462    LogicalPlanScan,
463    /// A DML target table.
464    LogicalPlanMutationTarget,
465    /// A DDL target table.
466    LogicalPlanDdlTarget,
467    /// A table referenced by index metadata.
468    LogicalPlanIndexTarget,
469    /// A table reached through a typed subquery expression.
470    TypedExprSubquery,
471}
472
473/// Severity for planning diagnostics.
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum PlanningDiagnosticSeverity {
476    Info,
477    Warning,
478}
479
480/// SQL planning diagnostic attachment point for routing layers.
481#[derive(Debug, Clone, PartialEq, Eq)]
482pub struct PlanningDiagnostic {
483    /// Stable machine-readable diagnostic code.
484    pub code: &'static str,
485    /// Diagnostic severity.
486    pub severity: PlanningDiagnosticSeverity,
487    /// Human-readable context.
488    pub message: String,
489}
490
491impl PlanningDiagnostic {
492    pub fn info(code: &'static str, message: impl Into<String>) -> Self {
493        Self {
494            code,
495            severity: PlanningDiagnosticSeverity::Info,
496            message: message.into(),
497        }
498    }
499
500    pub fn warning(code: &'static str, message: impl Into<String>) -> Self {
501        Self {
502            code,
503            severity: PlanningDiagnosticSeverity::Warning,
504            message: message.into(),
505        }
506    }
507}
508
509/// Parse and plan SQL without executing it, returning SQL-owned routing input.
510pub fn plan_sql_for_routing<C: Catalog + ?Sized>(
511    catalog: &C,
512    sql: &str,
513) -> Result<Vec<PlannedStatement>, SqlError> {
514    let statements = Parser::parse_sql(&AlopexDialect, sql).map_err(SqlError::from)?;
515    statements
516        .iter()
517        .map(|statement| plan_statement_for_routing(catalog, statement).map_err(SqlError::from))
518        .collect()
519}
520
521/// Plan a parsed statement without executing it, returning SQL-owned routing input.
522pub fn plan_statement_for_routing<C: Catalog + ?Sized>(
523    catalog: &C,
524    statement: &Statement,
525) -> Result<PlannedStatement, PlannerError> {
526    let planner = Planner::new(catalog);
527    let plan = planner.plan(statement)?;
528    let routing_input = routing_input_for_plan(statement, &plan)?;
529    Ok(PlannedStatement {
530        plan,
531        routing_input,
532    })
533}
534
535fn routing_input_for_plan(
536    statement: &Statement,
537    plan: &LogicalPlan,
538) -> Result<RoutingInput, PlannerError> {
539    let mut diagnostics = Vec::new();
540    let extractor = TableReferenceExtractor::new();
541    let table_references = extractor.extract_from_logical_plan(
542        plan,
543        table_reference_access(statement)?,
544        &mut diagnostics,
545    );
546
547    Ok(RoutingInput {
548        statement_kind: statement.kind.clone(),
549        table_references,
550        diagnostics,
551    })
552}
553
554/// Extracts physical table references from SQL-owned planner structures.
555#[derive(Debug, Default, Clone, Copy)]
556pub struct TableReferenceExtractor;
557
558impl TableReferenceExtractor {
559    pub fn new() -> Self {
560        Self
561    }
562
563    /// Extract references from a logical plan tree. `root_access` is applied to
564    /// the top-level statement target; nested typed subqueries are read-only.
565    pub fn extract_from_logical_plan(
566        &self,
567        plan: &LogicalPlan,
568        root_access: TableReferenceAccess,
569        diagnostics: &mut Vec<PlanningDiagnostic>,
570    ) -> Vec<TableReference> {
571        let mut references = Vec::new();
572        self.extract_plan(
573            plan,
574            root_access,
575            TableReferenceSource::LogicalPlanScan,
576            diagnostics,
577            &mut references,
578        );
579        if references.is_empty() {
580            diagnostics.push(PlanningDiagnostic::info(
581                "ALOPEX-PLAN-ROUTE-001",
582                "statement has no physical table reference",
583            ));
584        }
585        references
586    }
587
588    /// Extract references from a typed subquery plan embedded in an expression.
589    pub fn extract_from_subquery_context(
590        &self,
591        plan: &LogicalPlan,
592        diagnostics: &mut Vec<PlanningDiagnostic>,
593    ) -> Vec<TableReference> {
594        let mut references = Vec::new();
595        self.extract_plan(
596            plan,
597            TableReferenceAccess::Read,
598            TableReferenceSource::TypedExprSubquery,
599            diagnostics,
600            &mut references,
601        );
602        references
603    }
604
605    fn extract_plan(
606        &self,
607        plan: &LogicalPlan,
608        root_access: TableReferenceAccess,
609        scan_source: TableReferenceSource,
610        diagnostics: &mut Vec<PlanningDiagnostic>,
611        references: &mut Vec<TableReference>,
612    ) {
613        match plan {
614            LogicalPlan::Scan { table, projection } => {
615                if table != LITERAL_TABLE {
616                    push_table_reference(
617                        references,
618                        table,
619                        TableReferenceAccess::Read,
620                        scan_source,
621                    );
622                }
623                self.extract_projection(projection, diagnostics, references);
624            }
625            LogicalPlan::Values { rows, .. } => {
626                for row in rows {
627                    for value in row {
628                        self.extract_typed_expr(value, diagnostics, references);
629                    }
630                }
631            }
632            LogicalPlan::Filter { input, predicate } => {
633                self.extract_plan(input, root_access, scan_source, diagnostics, references);
634                self.extract_typed_expr(predicate, diagnostics, references);
635            }
636            LogicalPlan::Project { input, projection } => {
637                self.extract_plan(input, root_access, scan_source, diagnostics, references);
638                self.extract_projection(projection, diagnostics, references);
639            }
640            LogicalPlan::Join {
641                left,
642                right,
643                condition,
644                ..
645            }
646            | LogicalPlan::LateralJoin {
647                left,
648                right,
649                condition,
650                ..
651            } => {
652                self.extract_plan(
653                    left,
654                    TableReferenceAccess::Read,
655                    scan_source,
656                    diagnostics,
657                    references,
658                );
659                self.extract_plan(
660                    right,
661                    TableReferenceAccess::Read,
662                    scan_source,
663                    diagnostics,
664                    references,
665                );
666                if let Some(condition) = condition {
667                    self.extract_typed_expr(condition, diagnostics, references);
668                }
669            }
670            LogicalPlan::TableFunction { args, .. } => {
671                for arg in args {
672                    self.extract_typed_expr(arg, diagnostics, references);
673                }
674            }
675            LogicalPlan::Aggregate {
676                input,
677                group_keys,
678                aggregates,
679                having,
680                projection,
681                grouping_sets: _,
682            } => {
683                self.extract_plan(input, root_access, scan_source, diagnostics, references);
684                for expr in group_keys {
685                    self.extract_typed_expr(expr, diagnostics, references);
686                }
687                for aggregate in aggregates {
688                    if let Some(arg) = &aggregate.arg {
689                        self.extract_typed_expr(arg, diagnostics, references);
690                    }
691                }
692                if let Some(having) = having {
693                    self.extract_typed_expr(having, diagnostics, references);
694                }
695                self.extract_projection(projection, diagnostics, references);
696            }
697            LogicalPlan::Window { input, windows } => {
698                self.extract_plan(input, root_access, scan_source, diagnostics, references);
699                for window in windows {
700                    for expr in &window.partition_by {
701                        self.extract_typed_expr(expr, diagnostics, references);
702                    }
703                    for sort_expr in &window.order_by {
704                        self.extract_typed_expr(&sort_expr.expr, diagnostics, references);
705                    }
706                    match &window.function {
707                        WindowFunction::Aggregate(aggregate) => {
708                            if let Some(arg) = &aggregate.arg {
709                                self.extract_typed_expr(arg, diagnostics, references);
710                            }
711                        }
712                        WindowFunction::Lag(function) | WindowFunction::Lead(function) => {
713                            self.extract_typed_expr(&function.value, diagnostics, references);
714                            if let Some(offset) = &function.offset {
715                                self.extract_typed_expr(offset, diagnostics, references);
716                            }
717                            if let Some(default) = &function.default {
718                                self.extract_typed_expr(default, diagnostics, references);
719                            }
720                        }
721                        WindowFunction::Ntile(argument) => {
722                            self.extract_typed_expr(argument, diagnostics, references);
723                        }
724                        WindowFunction::Value(function) => match function {
725                            ValueWindowFunction::FirstValue(value)
726                            | ValueWindowFunction::LastValue(value) => {
727                                self.extract_typed_expr(value, diagnostics, references);
728                            }
729                            ValueWindowFunction::NthValue { value, nth } => {
730                                self.extract_typed_expr(value, diagnostics, references);
731                                self.extract_typed_expr(nth, diagnostics, references);
732                            }
733                        },
734                        WindowFunction::RowNumber
735                        | WindowFunction::Rank
736                        | WindowFunction::DenseRank
737                        | WindowFunction::PercentRank
738                        | WindowFunction::CumeDist => {}
739                    }
740                }
741            }
742            LogicalPlan::SetOperation { left, right, .. } => {
743                self.extract_plan(left, root_access, scan_source, diagnostics, references);
744                self.extract_plan(right, root_access, scan_source, diagnostics, references);
745            }
746            LogicalPlan::RecursiveCte {
747                anchor,
748                recursive_term,
749                ..
750            } => {
751                self.extract_plan(anchor, root_access, scan_source, diagnostics, references);
752                self.extract_plan(
753                    recursive_term,
754                    root_access,
755                    scan_source,
756                    diagnostics,
757                    references,
758                );
759            }
760            LogicalPlan::RecursiveReference { .. } => {}
761            LogicalPlan::Sort { input, order_by } => {
762                self.extract_plan(input, root_access, scan_source, diagnostics, references);
763                for sort_expr in order_by {
764                    self.extract_typed_expr(&sort_expr.expr, diagnostics, references);
765                }
766            }
767            LogicalPlan::DistinctOn {
768                input, order_by, ..
769            } => {
770                self.extract_plan(input, root_access, scan_source, diagnostics, references);
771                for sort_expr in order_by {
772                    self.extract_typed_expr(&sort_expr.expr, diagnostics, references);
773                }
774            }
775            LogicalPlan::Limit { input, .. } => {
776                self.extract_plan(input, root_access, scan_source, diagnostics, references);
777            }
778            LogicalPlan::Insert { table, values, .. } => {
779                push_table_reference(
780                    references,
781                    table,
782                    root_access,
783                    TableReferenceSource::LogicalPlanMutationTarget,
784                );
785                for row in values {
786                    for value in row {
787                        self.extract_typed_expr(value, diagnostics, references);
788                    }
789                }
790            }
791            LogicalPlan::InsertSelect { table, source, .. } => {
792                push_table_reference(
793                    references,
794                    table,
795                    root_access,
796                    TableReferenceSource::LogicalPlanMutationTarget,
797                );
798                self.extract_plan(
799                    source,
800                    TableReferenceAccess::Read,
801                    scan_source,
802                    diagnostics,
803                    references,
804                );
805            }
806            LogicalPlan::Update {
807                table,
808                assignments,
809                filter,
810            } => {
811                push_table_reference(
812                    references,
813                    table,
814                    root_access,
815                    TableReferenceSource::LogicalPlanMutationTarget,
816                );
817                for assignment in assignments {
818                    self.extract_typed_expr(&assignment.value, diagnostics, references);
819                }
820                if let Some(filter) = filter {
821                    self.extract_typed_expr(filter, diagnostics, references);
822                }
823            }
824            LogicalPlan::Delete { table, filter } => {
825                push_table_reference(
826                    references,
827                    table,
828                    root_access,
829                    TableReferenceSource::LogicalPlanMutationTarget,
830                );
831                if let Some(filter) = filter {
832                    self.extract_typed_expr(filter, diagnostics, references);
833                }
834            }
835            LogicalPlan::CreateTable { table, .. } => push_table_reference(
836                references,
837                &table.name,
838                root_access,
839                TableReferenceSource::LogicalPlanDdlTarget,
840            ),
841            LogicalPlan::DropTable { name, .. } => push_table_reference(
842                references,
843                name,
844                root_access,
845                TableReferenceSource::LogicalPlanDdlTarget,
846            ),
847            LogicalPlan::CreateIndex { index, .. } => push_table_reference(
848                references,
849                &index.table,
850                root_access,
851                TableReferenceSource::LogicalPlanIndexTarget,
852            ),
853            LogicalPlan::DropIndex { name, .. } => diagnostics.push(PlanningDiagnostic::warning(
854                "ALOPEX-PLAN-ROUTE-003",
855                format!(
856                    "DROP INDEX {name} does not expose a target table in the current logical plan"
857                ),
858            )),
859            LogicalPlan::Pragma { .. } => {}
860        }
861    }
862
863    fn extract_projection(
864        &self,
865        projection: &Projection,
866        diagnostics: &mut Vec<PlanningDiagnostic>,
867        references: &mut Vec<TableReference>,
868    ) {
869        if let Projection::Columns(columns) = projection {
870            for column in columns {
871                self.extract_typed_expr(&column.expr, diagnostics, references);
872            }
873        }
874    }
875
876    fn extract_typed_expr(
877        &self,
878        expr: &TypedExpr,
879        diagnostics: &mut Vec<PlanningDiagnostic>,
880        references: &mut Vec<TableReference>,
881    ) {
882        match &expr.kind {
883            TypedExprKind::Literal(_)
884            | TypedExprKind::ColumnRef { .. }
885            | TypedExprKind::VectorLiteral(_) => {}
886            TypedExprKind::BinaryOp { left, right, .. } => {
887                self.extract_typed_expr(left, diagnostics, references);
888                self.extract_typed_expr(right, diagnostics, references);
889            }
890            TypedExprKind::UnaryOp { operand, .. }
891            | TypedExprKind::Cast { expr: operand, .. }
892            | TypedExprKind::TryCast { expr: operand, .. }
893            | TypedExprKind::IsNull { expr: operand, .. } => {
894                self.extract_typed_expr(operand, diagnostics, references);
895            }
896            TypedExprKind::Case {
897                operand,
898                branches,
899                else_expr,
900            } => {
901                if let Some(operand) = operand {
902                    self.extract_typed_expr(operand, diagnostics, references);
903                }
904                for branch in branches {
905                    self.extract_typed_expr(&branch.when, diagnostics, references);
906                    self.extract_typed_expr(&branch.then, diagnostics, references);
907                }
908                if let Some(else_expr) = else_expr {
909                    self.extract_typed_expr(else_expr, diagnostics, references);
910                }
911            }
912            TypedExprKind::FunctionCall {
913                args,
914                filter,
915                order_by,
916                ..
917            } => {
918                for arg in args {
919                    self.extract_typed_expr(arg, diagnostics, references);
920                }
921                if let Some(filter) = filter {
922                    self.extract_typed_expr(filter, diagnostics, references);
923                }
924                for sort in order_by {
925                    self.extract_typed_expr(&sort.expr, diagnostics, references);
926                }
927            }
928            TypedExprKind::Between {
929                expr, low, high, ..
930            } => {
931                self.extract_typed_expr(expr, diagnostics, references);
932                self.extract_typed_expr(low, diagnostics, references);
933                self.extract_typed_expr(high, diagnostics, references);
934            }
935            TypedExprKind::Like {
936                expr,
937                pattern,
938                escape,
939                ..
940            } => {
941                self.extract_typed_expr(expr, diagnostics, references);
942                self.extract_typed_expr(pattern, diagnostics, references);
943                if let Some(escape) = escape {
944                    self.extract_typed_expr(escape, diagnostics, references);
945                }
946            }
947            TypedExprKind::InList { expr, list, .. } => {
948                self.extract_typed_expr(expr, diagnostics, references);
949                for item in list {
950                    self.extract_typed_expr(item, diagnostics, references);
951                }
952            }
953            TypedExprKind::ScalarSubquery(subquery) => self.extract_plan(
954                subquery,
955                TableReferenceAccess::Read,
956                TableReferenceSource::TypedExprSubquery,
957                diagnostics,
958                references,
959            ),
960            TypedExprKind::InSubquery { expr, subquery, .. } => {
961                self.extract_typed_expr(expr, diagnostics, references);
962                self.extract_plan(
963                    subquery,
964                    TableReferenceAccess::Read,
965                    TableReferenceSource::TypedExprSubquery,
966                    diagnostics,
967                    references,
968                );
969            }
970            TypedExprKind::Exists { subquery, .. } => self.extract_plan(
971                subquery,
972                TableReferenceAccess::Read,
973                TableReferenceSource::TypedExprSubquery,
974                diagnostics,
975                references,
976            ),
977            TypedExprKind::Quantified { expr, subquery, .. } => {
978                self.extract_typed_expr(expr, diagnostics, references);
979                self.extract_plan(
980                    subquery,
981                    TableReferenceAccess::Read,
982                    TableReferenceSource::TypedExprSubquery,
983                    diagnostics,
984                    references,
985                );
986            }
987        }
988    }
989}
990
991fn push_table_reference(
992    references: &mut Vec<TableReference>,
993    table_name: &str,
994    access: TableReferenceAccess,
995    source: TableReferenceSource,
996) {
997    if !references.iter().any(|reference| {
998        reference.table_name == table_name
999            && reference.access == access
1000            && reference.source == source
1001    }) {
1002        references.push(TableReference::new(table_name, access, source));
1003    }
1004}
1005
1006#[derive(Debug)]
1007enum GenericHostStatement<'a> {
1008    CreateTable(&'a CreateTable),
1009    DropTable(&'a DropTable),
1010    CreateIndex(&'a CreateIndex),
1011    DropIndex(&'a DropIndex),
1012    Pragma {
1013        name: &'a str,
1014        value: &'a Option<PragmaValue>,
1015    },
1016    Select(&'a Select),
1017    Values(&'a Values),
1018    Insert(&'a Insert),
1019    Update(&'a Update),
1020    Delete(&'a Delete),
1021    Unsupported,
1022}
1023
1024fn classify_generic_host_statement(statement_kind: &StatementKind) -> GenericHostStatement<'_> {
1025    // The fallback is intentionally unreachable for the current enum. It
1026    // becomes the safe route before a future statement-specific host is added.
1027    #[allow(unreachable_patterns)]
1028    match statement_kind {
1029        StatementKind::CreateTable(statement) => GenericHostStatement::CreateTable(statement),
1030        StatementKind::DropTable(statement) => GenericHostStatement::DropTable(statement),
1031        StatementKind::CreateIndex(statement) => GenericHostStatement::CreateIndex(statement),
1032        StatementKind::DropIndex(statement) => GenericHostStatement::DropIndex(statement),
1033        StatementKind::Pragma { name, value } => GenericHostStatement::Pragma { name, value },
1034        StatementKind::Select(statement) => GenericHostStatement::Select(statement),
1035        StatementKind::Values(statement) => GenericHostStatement::Values(statement),
1036        StatementKind::Insert(statement) => GenericHostStatement::Insert(statement),
1037        StatementKind::Update(statement) => GenericHostStatement::Update(statement),
1038        StatementKind::Delete(statement) => GenericHostStatement::Delete(statement),
1039        _ => GenericHostStatement::Unsupported,
1040    }
1041}
1042
1043fn unsupported_generic_statement(statement: &Statement) -> PlannerError {
1044    PlannerError::unsupported_feature(
1045        "statement kind for the generic SQL planner",
1046        "a statement-specific planner",
1047        statement.span,
1048    )
1049}
1050
1051fn table_reference_access(statement: &Statement) -> Result<TableReferenceAccess, PlannerError> {
1052    table_reference_access_for_classified(
1053        statement,
1054        classify_generic_host_statement(&statement.kind),
1055    )
1056}
1057
1058fn table_reference_access_for_classified(
1059    statement: &Statement,
1060    classified: GenericHostStatement<'_>,
1061) -> Result<TableReferenceAccess, PlannerError> {
1062    match classified {
1063        GenericHostStatement::Select(_) | GenericHostStatement::Values(_) => {
1064            Ok(TableReferenceAccess::Read)
1065        }
1066        GenericHostStatement::Insert(_)
1067        | GenericHostStatement::Update(_)
1068        | GenericHostStatement::Delete(_) => Ok(TableReferenceAccess::Write),
1069        GenericHostStatement::CreateTable(_) => Ok(TableReferenceAccess::Create),
1070        GenericHostStatement::DropTable(_) => Ok(TableReferenceAccess::Drop),
1071        GenericHostStatement::CreateIndex(_)
1072        | GenericHostStatement::DropIndex(_)
1073        | GenericHostStatement::Pragma { .. } => Ok(TableReferenceAccess::Metadata),
1074        GenericHostStatement::Unsupported => Err(unsupported_generic_statement(statement)),
1075    }
1076}
1077
1078/// The SQL query planner.
1079///
1080/// The planner converts AST statements into logical plans. It performs:
1081/// - Name resolution: Validates table and column references
1082/// - Type checking: Infers and validates expression types
1083/// - Plan construction: Builds the logical plan tree
1084///
1085/// # Design Notes
1086///
1087/// - The planner uses an immutable reference to the catalog (`&C`)
1088/// - DDL statements produce plans but don't modify the catalog
1089/// - The executor is responsible for applying catalog changes
1090///
1091/// # Examples
1092///
1093/// ```
1094/// use alopex_sql::catalog::MemoryCatalog;
1095/// use alopex_sql::planner::Planner;
1096///
1097/// let catalog = MemoryCatalog::new();
1098/// let planner = Planner::new(&catalog);
1099///
1100/// // Parse and plan a statement
1101/// // let stmt = parser.parse("SELECT * FROM users")?;
1102/// // let plan = planner.plan(&stmt)?;
1103/// ```
1104pub struct Planner<'a, C: Catalog + ?Sized> {
1105    catalog: &'a C,
1106    name_resolver: NameResolver<'a, C>,
1107    type_checker: TypeChecker<'a, C>,
1108}
1109
1110impl<'a, C: Catalog + ?Sized> Planner<'a, C> {
1111    /// Create a new planner with the given catalog.
1112    pub fn new(catalog: &'a C) -> Self {
1113        Self {
1114            catalog,
1115            name_resolver: NameResolver::new(catalog),
1116            type_checker: TypeChecker::new(catalog),
1117        }
1118    }
1119
1120    /// Plan a SQL statement.
1121    ///
1122    /// This is the main entry point for converting an AST statement into a logical plan.
1123    ///
1124    /// # Errors
1125    ///
1126    /// Returns a `PlannerError` if:
1127    /// - Referenced tables or columns don't exist
1128    /// - Type checking fails
1129    /// - DDL validation fails (e.g., table already exists for CREATE TABLE)
1130    pub fn plan(&self, stmt: &Statement) -> Result<LogicalPlan, PlannerError> {
1131        self.plan_classified_statement(stmt, classify_generic_host_statement(&stmt.kind))
1132    }
1133
1134    fn plan_classified_statement(
1135        &self,
1136        stmt: &Statement,
1137        classified: GenericHostStatement<'_>,
1138    ) -> Result<LogicalPlan, PlannerError> {
1139        match classified {
1140            // DDL statements
1141            GenericHostStatement::CreateTable(statement) => self.plan_create_table(statement),
1142            GenericHostStatement::DropTable(statement) => self.plan_drop_table(statement),
1143            GenericHostStatement::CreateIndex(statement) => self.plan_create_index(statement),
1144            GenericHostStatement::DropIndex(statement) => self.plan_drop_index(statement),
1145            GenericHostStatement::Pragma { name, value } => self.plan_pragma(name, value),
1146
1147            // DML statements
1148            GenericHostStatement::Select(statement) => self.plan_select(statement),
1149            GenericHostStatement::Values(statement) => self.plan_values(statement),
1150            GenericHostStatement::Insert(statement) => self.plan_insert(statement),
1151            GenericHostStatement::Update(statement) => self.plan_update(statement),
1152            GenericHostStatement::Delete(statement) => self.plan_delete(statement),
1153            GenericHostStatement::Unsupported => Err(unsupported_generic_statement(stmt)),
1154        }
1155    }
1156
1157    fn plan_pragma(
1158        &self,
1159        raw_name: &str,
1160        value: &Option<PragmaValue>,
1161    ) -> Result<LogicalPlan, PlannerError> {
1162        let name = raw_name.to_ascii_lowercase();
1163        if !matches!(name.as_str(), "cache_size" | "memory_limit" | "io_stats") {
1164            return Err(PlannerError::InvalidPragma {
1165                name,
1166                reason: "supported names are cache_size, memory_limit, and io_stats".to_string(),
1167            });
1168        }
1169        match name.as_str() {
1170            "cache_size" => match value {
1171                Some(PragmaValue::Int(v)) if *v > 0 => {}
1172                Some(PragmaValue::Int(_)) => {
1173                    return Err(PlannerError::InvalidPragma {
1174                        name,
1175                        reason: "cache_size must be a positive page count".to_string(),
1176                    });
1177                }
1178                Some(PragmaValue::Text(_)) => {
1179                    return Err(PlannerError::InvalidPragma {
1180                        name,
1181                        reason: "cache_size requires an integer page count".to_string(),
1182                    });
1183                }
1184                None => {}
1185            },
1186            "memory_limit" => {
1187                if let Some(PragmaValue::Int(v)) = value
1188                    && *v < 0
1189                {
1190                    return Err(PlannerError::InvalidPragma {
1191                        name,
1192                        reason: "memory_limit cannot be negative".to_string(),
1193                    });
1194                }
1195            }
1196            "io_stats" => {
1197                if value.is_some() {
1198                    return Err(PlannerError::InvalidPragma {
1199                        name,
1200                        reason: "io_stats does not accept a value".to_string(),
1201                    });
1202                }
1203            }
1204            _ => unreachable!(),
1205        }
1206        Ok(LogicalPlan::Pragma {
1207            name,
1208            value: value.clone(),
1209        })
1210    }
1211
1212    // ============================================================
1213    // DDL Planning Methods (Task 16)
1214    // ============================================================
1215
1216    /// Plan a CREATE TABLE statement.
1217    ///
1218    /// Validates that the table doesn't already exist (unless IF NOT EXISTS is specified),
1219    /// and converts the AST column definitions to catalog metadata.
1220    fn plan_create_table(&self, stmt: &CreateTable) -> Result<LogicalPlan, PlannerError> {
1221        // Check if table already exists
1222        if !stmt.if_not_exists && self.catalog.table_exists(&stmt.name) {
1223            return Err(PlannerError::table_already_exists(&stmt.name));
1224        }
1225
1226        // Convert column definitions to metadata
1227        let columns: Vec<ColumnMetadata> = stmt
1228            .columns
1229            .iter()
1230            .map(|col| self.convert_column_def(col))
1231            .collect();
1232
1233        // Collect primary key from table constraints
1234        let primary_key = Self::extract_primary_key(stmt);
1235
1236        // Build table metadata
1237        // Note: table_id defaults to 0 as placeholder; Executor assigns the actual ID
1238        let mut table = TableMetadata::new(stmt.name.clone(), columns);
1239        if let Some(pk) = primary_key {
1240            table = table.with_primary_key(pk);
1241        }
1242        table.catalog_name = "default".to_string();
1243        table.namespace_name = "default".to_string();
1244        table.table_type = TableType::Managed;
1245        table.data_source_format = DataSourceFormat::Alopex;
1246        table.properties = HashMap::new();
1247
1248        Ok(LogicalPlan::CreateTable {
1249            table,
1250            if_not_exists: stmt.if_not_exists,
1251            with_options: stmt
1252                .with_options
1253                .iter()
1254                .map(|opt| (opt.key.clone(), opt.value.clone()))
1255                .collect(),
1256        })
1257    }
1258
1259    /// Convert an AST column definition to catalog column metadata.
1260    fn convert_column_def(&self, col: &ColumnDef) -> ColumnMetadata {
1261        let data_type = ResolvedType::from_ast(&col.data_type);
1262        let mut meta = ColumnMetadata::new(col.name.clone(), data_type);
1263
1264        // Process constraints
1265        for constraint in &col.constraints {
1266            meta = Self::apply_column_constraint(meta, constraint);
1267        }
1268
1269        meta
1270    }
1271
1272    /// Apply a column constraint to column metadata.
1273    fn apply_column_constraint(
1274        mut meta: ColumnMetadata,
1275        constraint: &ColumnConstraint,
1276    ) -> ColumnMetadata {
1277        match constraint {
1278            ColumnConstraint::NotNull { .. } => {
1279                meta.not_null = true;
1280            }
1281            ColumnConstraint::PrimaryKey { .. } => {
1282                meta.primary_key = true;
1283                meta.not_null = true; // PRIMARY KEY implies NOT NULL
1284            }
1285            ColumnConstraint::Unique { .. } => {
1286                meta.unique = true;
1287            }
1288            ColumnConstraint::Default { value: expr, .. } => {
1289                meta.default = Some(expr.clone());
1290            }
1291        }
1292        meta
1293    }
1294
1295    /// Extract primary key columns from table constraints.
1296    fn extract_primary_key(stmt: &CreateTable) -> Option<Vec<String>> {
1297        use crate::ast::ddl::TableConstraint;
1298
1299        // First check table-level constraints
1300        // Note: Currently only PrimaryKey variant exists; when more variants are added,
1301        // this should iterate to find the first PrimaryKey constraint
1302        if let Some(TableConstraint::PrimaryKey { columns, .. }) = stmt.constraints.first() {
1303            return Some(columns.clone());
1304        }
1305
1306        // Then check column-level PRIMARY KEY constraints
1307        let pk_columns: Vec<String> = stmt
1308            .columns
1309            .iter()
1310            .filter(|col| col.constraints.iter().any(Self::is_primary_key_constraint))
1311            .map(|col| col.name.clone())
1312            .collect();
1313
1314        if pk_columns.is_empty() {
1315            None
1316        } else {
1317            Some(pk_columns)
1318        }
1319    }
1320
1321    /// Check if a column constraint is a PRIMARY KEY constraint.
1322    fn is_primary_key_constraint(constraint: &ColumnConstraint) -> bool {
1323        matches!(constraint, ColumnConstraint::PrimaryKey { .. })
1324    }
1325
1326    /// Plan a DROP TABLE statement.
1327    ///
1328    /// Validates that the table exists (unless IF EXISTS is specified).
1329    fn plan_drop_table(&self, stmt: &DropTable) -> Result<LogicalPlan, PlannerError> {
1330        // Check if table exists
1331        if !stmt.if_exists && !self.table_exists_in_default(&stmt.name) {
1332            return Err(PlannerError::TableNotFound {
1333                name: stmt.name.clone(),
1334                line: stmt.span.start.line,
1335                column: stmt.span.start.column,
1336            });
1337        }
1338
1339        Ok(LogicalPlan::DropTable {
1340            name: stmt.name.clone(),
1341            if_exists: stmt.if_exists,
1342        })
1343    }
1344
1345    fn table_exists_in_default(&self, name: &str) -> bool {
1346        match self.catalog.get_table(name) {
1347            Some(table) => table.catalog_name == "default" && table.namespace_name == "default",
1348            None => false,
1349        }
1350    }
1351
1352    /// Plan a CREATE INDEX statement.
1353    ///
1354    /// Validates that:
1355    /// - The index doesn't already exist (unless IF NOT EXISTS is specified)
1356    /// - The target table exists
1357    /// - The target column exists in the table
1358    fn plan_create_index(&self, stmt: &CreateIndex) -> Result<LogicalPlan, PlannerError> {
1359        // Check if index already exists
1360        if !stmt.if_not_exists && self.catalog.index_exists(&stmt.name) {
1361            return Err(PlannerError::index_already_exists(&stmt.name));
1362        }
1363
1364        // Validate table exists
1365        let table = self.name_resolver.resolve_table(&stmt.table, stmt.span)?;
1366
1367        // Validate column exists
1368        self.name_resolver
1369            .resolve_column(table, &stmt.column, stmt.span)?;
1370
1371        // Build index metadata
1372        // Note: index_id is set to 0 as placeholder; Executor assigns the actual ID
1373        // Note: column_indices will be resolved by Executor when table schema is available
1374        let mut index = IndexMetadata::new(
1375            0,
1376            stmt.name.clone(),
1377            stmt.table.clone(),
1378            vec![stmt.column.clone()],
1379        );
1380
1381        if let Some(method) = stmt.method {
1382            index = index.with_method(method);
1383        }
1384
1385        let options: Vec<(String, String)> = stmt
1386            .options
1387            .iter()
1388            .map(|opt| (opt.key.clone(), opt.value.clone()))
1389            .collect();
1390        if !options.is_empty() {
1391            index = index.with_options(options);
1392        }
1393
1394        Ok(LogicalPlan::CreateIndex {
1395            index,
1396            if_not_exists: stmt.if_not_exists,
1397        })
1398    }
1399
1400    /// Plan a DROP INDEX statement.
1401    ///
1402    /// Validates that the index exists (unless IF EXISTS is specified).
1403    fn plan_drop_index(&self, stmt: &DropIndex) -> Result<LogicalPlan, PlannerError> {
1404        // Check if index exists
1405        if !stmt.if_exists && !self.index_exists_in_default(&stmt.name) {
1406            return Err(PlannerError::index_not_found(&stmt.name));
1407        }
1408
1409        Ok(LogicalPlan::DropIndex {
1410            name: stmt.name.clone(),
1411            if_exists: stmt.if_exists,
1412        })
1413    }
1414
1415    fn index_exists_in_default(&self, name: &str) -> bool {
1416        match self.catalog.get_index(name) {
1417            Some(index) => index.catalog_name == "default" && index.namespace_name == "default",
1418            None => false,
1419        }
1420    }
1421
1422    // ============================================================
1423    // DML Planning Methods (Task 17 & 18)
1424    // ============================================================
1425
1426    /// Plan a SELECT statement.
1427    ///
1428    /// Builds a logical plan tree: Scan -> Filter -> Sort -> Limit
1429    /// Each layer is optional and only added if the corresponding clause is present.
1430    fn plan_select(&self, stmt: &Select) -> Result<LogicalPlan, PlannerError> {
1431        self.plan_select_relation(stmt, &[], &CtePlans::new())
1432            .map(|relation| relation.plan)
1433    }
1434
1435    fn plan_values(&self, stmt: &Values) -> Result<LogicalPlan, PlannerError> {
1436        self.plan_values_relation(stmt, &[], &CtePlans::new())
1437            .map(|relation| relation.plan)
1438    }
1439
1440    fn plan_query_body_relation(
1441        &self,
1442        body: &QueryBody,
1443        outer_scope: &[ScopedTable],
1444        enclosing_ctes: &CtePlans,
1445    ) -> Result<PlannedRelation, PlannerError> {
1446        match body {
1447            QueryBody::Select(select) => {
1448                self.plan_select_relation(select, outer_scope, enclosing_ctes)
1449            }
1450            QueryBody::Values(values) => {
1451                self.plan_values_relation(values, outer_scope, enclosing_ctes)
1452            }
1453        }
1454    }
1455
1456    fn plan_ctes(
1457        &self,
1458        stmt: &Select,
1459        enclosing_ctes: &CtePlans,
1460    ) -> Result<CtePlans, PlannerError> {
1461        self.plan_with_clause(stmt.with.as_ref(), enclosing_ctes)
1462    }
1463
1464    fn plan_with_clause(
1465        &self,
1466        with: Option<&crate::ast::WithClause>,
1467        enclosing_ctes: &CtePlans,
1468    ) -> Result<CtePlans, PlannerError> {
1469        let Some(with) = with else {
1470            return Ok(enclosing_ctes.clone());
1471        };
1472        let mut declared_names = HashSet::new();
1473        for cte in &with.ctes {
1474            if !declared_names.insert(&cte.name) {
1475                return Err(PlannerError::invalid_expression(format!(
1476                    "common table expression '{}' is defined more than once",
1477                    cte.name
1478                )));
1479            }
1480        }
1481        if with.recursive && cte_dependency_cycle(with) {
1482            return self.plan_recursive_cte(with, enclosing_ctes);
1483        }
1484
1485        let mut plans = enclosing_ctes.clone();
1486        let mut local_names = HashSet::new();
1487        for cte in &with.ctes {
1488            if !local_names.insert(cte.name.clone()) {
1489                return Err(PlannerError::invalid_expression(format!(
1490                    "common table expression '{}' is defined more than once",
1491                    cte.name
1492                )));
1493            }
1494            let mut relation = self.plan_query_body_relation(&cte.query, &[], &plans)?;
1495            if !cte.columns.is_empty() {
1496                if cte.columns.len() != relation.schema.len() {
1497                    return Err(PlannerError::cte_column_count_mismatch(
1498                        &cte.name,
1499                        cte.columns.len(),
1500                        relation.schema.len(),
1501                        cte.span,
1502                    ));
1503                }
1504
1505                let mut column_names = HashSet::new();
1506                for column_name in &cte.columns {
1507                    if !column_names.insert(column_name) {
1508                        return Err(PlannerError::duplicate_cte_column(
1509                            &cte.name,
1510                            column_name,
1511                            cte.span,
1512                        ));
1513                    }
1514                }
1515
1516                for (column, column_name) in relation.schema.iter_mut().zip(&cte.columns) {
1517                    column.name.clone_from(column_name);
1518                }
1519            }
1520            plans.insert(cte.name.clone(), relation);
1521        }
1522        Ok(plans)
1523    }
1524
1525    fn plan_recursive_cte(
1526        &self,
1527        with: &crate::ast::WithClause,
1528        enclosing_ctes: &CtePlans,
1529    ) -> Result<CtePlans, PlannerError> {
1530        if with.ctes.len() != 1 {
1531            return Err(PlannerError::unsupported_feature(
1532                "recursive WITH containing anything other than exactly one common table expression",
1533                "a future version",
1534                with.span,
1535            ));
1536        }
1537
1538        let cte = &with.ctes[0];
1539        let mut column_names = HashSet::new();
1540        for column_name in &cte.columns {
1541            if !column_names.insert(column_name) {
1542                return Err(PlannerError::duplicate_cte_column(
1543                    &cte.name,
1544                    column_name,
1545                    cte.span,
1546                ));
1547            }
1548        }
1549
1550        let QueryBody::Select(body) = cte.query.as_ref() else {
1551            return Err(PlannerError::unsupported_feature(
1552                "recursive common table expression whose outer body is not SELECT",
1553                "a future version",
1554                cte.span,
1555            ));
1556        };
1557        if body.set_operations.len() != 1 {
1558            return Err(PlannerError::unsupported_feature(
1559                "recursive common table expression without exactly one UNION or UNION ALL",
1560                "a future version",
1561                cte.span,
1562            ));
1563        }
1564        if !body.order_by.is_empty()
1565            || body.limit.is_some()
1566            || body.offset.is_some()
1567            || body.limit_with_ties
1568        {
1569            return Err(PlannerError::unsupported_feature(
1570                "ORDER BY, LIMIT, OFFSET, or FETCH inside a recursive common table expression",
1571                "a future version",
1572                body.span,
1573            ));
1574        }
1575
1576        let operation = &body.set_operations[0];
1577        if operation.operator != AstSetOperator::Union {
1578            return Err(PlannerError::unsupported_feature(
1579                "recursive common table expression using an operator other than UNION or UNION ALL",
1580                "a future version",
1581                operation.span,
1582            ));
1583        }
1584
1585        let mut anchor = body.clone();
1586        anchor.with = None;
1587        anchor.set_operations.clear();
1588        if select_table_reference_count(&anchor, &cte.name) != 0 {
1589            return Err(PlannerError::unsupported_feature(
1590                "recursive common table expression without an anchor term that does not reference itself",
1591                "a future version",
1592                anchor.span,
1593            ));
1594        }
1595
1596        let QueryBody::Select(recursive_term) = operation.right.as_ref() else {
1597            return Err(PlannerError::unsupported_feature(
1598                "recursive common table expression whose recursive term is not SELECT",
1599                "a future version",
1600                operation.span,
1601            ));
1602        };
1603        if select_contains_subquery(recursive_term) {
1604            return Err(PlannerError::unsupported_feature(
1605                "subquery in a recursive term",
1606                "a future version",
1607                recursive_term.span,
1608            ));
1609        }
1610        let total_references = select_table_reference_count(recursive_term, &cte.name);
1611        let direct_references = direct_from_reference_count(&recursive_term.from, &cte.name);
1612        if total_references != 1 || direct_references != 1 {
1613            return Err(PlannerError::unsupported_feature(
1614                "recursive term without exactly one direct self-reference",
1615                "a future version",
1616                recursive_term.span,
1617            ));
1618        }
1619        if recursive_term.with.is_some() || !recursive_term.set_operations.is_empty() {
1620            return Err(PlannerError::unsupported_feature(
1621                "nested WITH or set operation in a recursive term",
1622                "a future version",
1623                recursive_term.span,
1624            ));
1625        }
1626
1627        let mut anchor_relation = self.plan_select_relation(&anchor, &[], enclosing_ctes)?;
1628        if cte.columns.is_empty() {
1629            let mut anchor_names = HashSet::new();
1630            for column in &anchor_relation.schema {
1631                if !anchor_names.insert(&column.name) {
1632                    return Err(PlannerError::duplicate_cte_column(
1633                        &cte.name,
1634                        &column.name,
1635                        cte.span,
1636                    ));
1637                }
1638            }
1639        } else {
1640            if cte.columns.len() != anchor_relation.schema.len() {
1641                return Err(PlannerError::cte_column_count_mismatch(
1642                    &cte.name,
1643                    cte.columns.len(),
1644                    anchor_relation.schema.len(),
1645                    cte.span,
1646                ));
1647            }
1648            for (column, name) in anchor_relation.schema.iter_mut().zip(&cte.columns) {
1649                column.name.clone_from(name);
1650            }
1651        }
1652
1653        let mut recursive_scope = enclosing_ctes.clone();
1654        recursive_scope.insert(
1655            cte.name.clone(),
1656            PlannedRelation {
1657                plan: LogicalPlan::RecursiveReference {
1658                    name: cte.name.clone(),
1659                    schema: anchor_relation.schema.clone(),
1660                },
1661                schema: anchor_relation.schema.clone(),
1662                scope: vec![ScopedTable::new(
1663                    TableMetadata::new(&cte.name, anchor_relation.schema.clone()),
1664                    0,
1665                )],
1666            },
1667        );
1668        let recursive_relation =
1669            self.plan_select_relation(recursive_term, &[], &recursive_scope)?;
1670        if recursive_relation.schema.len() != anchor_relation.schema.len() {
1671            return Err(PlannerError::set_operation_column_count_mismatch(
1672                anchor_relation.schema.len(),
1673                recursive_relation.schema.len(),
1674                operation.span,
1675            ));
1676        }
1677        for (anchor_column, recursive_column) in anchor_relation
1678            .schema
1679            .iter()
1680            .zip(&recursive_relation.schema)
1681        {
1682            if anchor_column.data_type != recursive_column.data_type {
1683                return Err(PlannerError::type_mismatch(
1684                    anchor_column.data_type.type_name(),
1685                    recursive_column.data_type.type_name(),
1686                    operation.span,
1687                ));
1688            }
1689        }
1690
1691        let schema = anchor_relation.schema.clone();
1692        let relation = PlannedRelation {
1693            plan: LogicalPlan::RecursiveCte {
1694                name: cte.name.clone(),
1695                anchor: Box::new(anchor_relation.plan),
1696                recursive_term: Box::new(recursive_relation.plan),
1697                union_all: operation.all,
1698                schema: schema.clone(),
1699                limits: RecursiveCteLimits::default(),
1700            },
1701            schema: schema.clone(),
1702            scope: vec![ScopedTable::new(TableMetadata::new(&cte.name, schema), 0)],
1703        };
1704        let mut plans = enclosing_ctes.clone();
1705        plans.insert(cte.name.clone(), relation);
1706        Ok(plans)
1707    }
1708
1709    fn plan_values_relation(
1710        &self,
1711        stmt: &Values,
1712        outer_scope: &[ScopedTable],
1713        enclosing_ctes: &CtePlans,
1714    ) -> Result<PlannedRelation, PlannerError> {
1715        let ctes = self.plan_with_clause(stmt.with.as_ref(), enclosing_ctes)?;
1716        let relation = self.plan_values_core(stmt, outer_scope, &ctes)?;
1717        self.apply_set_operations_and_tail(
1718            relation,
1719            &stmt.set_operations,
1720            &stmt.order_by,
1721            &stmt.limit,
1722            &stmt.offset,
1723            stmt.limit_with_ties,
1724            outer_scope,
1725            &ctes,
1726        )
1727    }
1728
1729    fn plan_values_core(
1730        &self,
1731        stmt: &Values,
1732        outer_scope: &[ScopedTable],
1733        ctes: &CtePlans,
1734    ) -> Result<PlannedRelation, PlannerError> {
1735        if stmt.rows.is_empty() {
1736            let schema = Vec::new();
1737            return Ok(PlannedRelation {
1738                plan: LogicalPlan::Values {
1739                    rows: Vec::new(),
1740                    schema: schema.clone(),
1741                },
1742                schema: schema.clone(),
1743                scope: vec![ScopedTable::new(
1744                    TableMetadata::new(LITERAL_TABLE, schema),
1745                    0,
1746                )],
1747            });
1748        }
1749
1750        let width = stmt.rows[0].len();
1751        if width == 0 {
1752            return Err(PlannerError::values_column_count_mismatch(
1753                1, 1, 0, stmt.span,
1754            ));
1755        }
1756
1757        let mut typed_rows = Vec::with_capacity(stmt.rows.len());
1758        for (row_index, row) in stmt.rows.iter().enumerate() {
1759            if row.len() != width {
1760                return Err(PlannerError::values_column_count_mismatch(
1761                    row_index + 1,
1762                    width,
1763                    row.len(),
1764                    stmt.span,
1765                ));
1766            }
1767            let mut typed_row = Vec::with_capacity(width);
1768            for expr in row {
1769                if expr_contains_subquery(expr) {
1770                    return Err(PlannerError::invalid_expression(
1771                        "VALUES expressions cannot contain subqueries".to_string(),
1772                    ));
1773                }
1774                let typed = self.infer_expr_with_scope(expr, outer_scope, ctes)?;
1775                if typed_expr_contains_aggregate(&typed) || typed_expr_contains_window(&typed) {
1776                    return Err(PlannerError::invalid_expression(
1777                        "VALUES expressions must be scalar".to_string(),
1778                    ));
1779                }
1780                typed_row.push(typed);
1781            }
1782            typed_rows.push(typed_row);
1783        }
1784
1785        let mut common_types = vec![ResolvedType::Null; width];
1786        for row in &typed_rows {
1787            for (column_index, value) in row.iter().enumerate() {
1788                common_types[column_index] = common_values_type(
1789                    &common_types[column_index],
1790                    &value.resolved_type,
1791                    value.span,
1792                )?;
1793            }
1794        }
1795        for row in &mut typed_rows {
1796            for (value, target) in row.iter_mut().zip(&common_types) {
1797                if value.resolved_type != *target && value.resolved_type != ResolvedType::Null {
1798                    *value = TypedExpr::cast(value.clone(), target.clone(), value.span);
1799                }
1800            }
1801        }
1802
1803        let schema = common_types
1804            .into_iter()
1805            .enumerate()
1806            .map(|(index, data_type)| {
1807                ColumnMetadata::new(format!("column{}", index + 1), data_type)
1808            })
1809            .collect::<Vec<_>>();
1810        Ok(PlannedRelation {
1811            plan: LogicalPlan::Values {
1812                rows: typed_rows,
1813                schema: schema.clone(),
1814            },
1815            schema: schema.clone(),
1816            scope: vec![ScopedTable::new(
1817                TableMetadata::new(LITERAL_TABLE, schema),
1818                0,
1819            )],
1820        })
1821    }
1822
1823    #[allow(clippy::too_many_arguments)]
1824    fn apply_set_operations_and_tail(
1825        &self,
1826        mut relation: PlannedRelation,
1827        operations: &[AstSetOperation],
1828        order_by: &[OrderByExpr],
1829        limit: &Option<Expr>,
1830        offset: &Option<Expr>,
1831        limit_with_ties: bool,
1832        outer_scope: &[ScopedTable],
1833        ctes: &CtePlans,
1834    ) -> Result<PlannedRelation, PlannerError> {
1835        for operation in operations {
1836            let right = self.plan_query_body_relation(&operation.right, outer_scope, ctes)?;
1837            if relation.schema.len() != right.schema.len() {
1838                return Err(PlannerError::set_operation_column_count_mismatch(
1839                    relation.schema.len(),
1840                    right.schema.len(),
1841                    operation.span,
1842                ));
1843            }
1844            for (left_column, right_column) in relation.schema.iter().zip(&right.schema) {
1845                if left_column.data_type != right_column.data_type {
1846                    return Err(PlannerError::type_mismatch(
1847                        left_column.data_type.type_name(),
1848                        right_column.data_type.type_name(),
1849                        operation.span,
1850                    ));
1851                }
1852            }
1853
1854            relation.plan = LogicalPlan::SetOperation {
1855                left: Box::new(relation.plan),
1856                right: Box::new(right.plan),
1857                operator: match operation.operator {
1858                    AstSetOperator::Union => SetOperator::Union,
1859                    AstSetOperator::Intersect => SetOperator::Intersect,
1860                    AstSetOperator::Except => SetOperator::Except,
1861                },
1862                all: operation.all,
1863            };
1864        }
1865
1866        relation.scope = vec![ScopedTable::new(
1867            TableMetadata::new(LITERAL_TABLE, relation.schema.clone()),
1868            0,
1869        )];
1870        if !order_by.is_empty() {
1871            let order_by =
1872                self.build_sort_exprs_with_scope(order_by, &relation.scope, &HashMap::new(), ctes)?;
1873            relation.plan = LogicalPlan::Sort {
1874                input: Box::new(relation.plan),
1875                order_by,
1876            };
1877        }
1878        relation.plan = self.apply_pagination(relation.plan, limit, offset, limit_with_ties)?;
1879        Ok(relation)
1880    }
1881
1882    fn plan_select_relation(
1883        &self,
1884        stmt: &Select,
1885        outer_scope: &[ScopedTable],
1886        enclosing_ctes: &CtePlans,
1887    ) -> Result<PlannedRelation, PlannerError> {
1888        let resolved_stmt = resolve_named_windows(stmt)?;
1889        let stmt = &resolved_stmt;
1890        let ctes = self.plan_ctes(stmt, enclosing_ctes)?;
1891        if !stmt.set_operations.is_empty() {
1892            // D7: the trailing ORDER BY belongs to the whole set operation,
1893            // so the DISTINCT ON prefix contract has no owner here. A nested
1894            // operand SELECT with its own DISTINCT ON remains supported.
1895            if !stmt.distinct_on.is_empty() {
1896                return Err(PlannerError::unsupported_feature(
1897                    "DISTINCT ON combined with UNION, INTERSECT, or EXCEPT",
1898                    "a future version",
1899                    stmt.span,
1900                ));
1901            }
1902            let mut left_select = stmt.clone();
1903            left_select.with = None;
1904            left_select.set_operations.clear();
1905            left_select.order_by.clear();
1906            left_select.limit = None;
1907            left_select.offset = None;
1908            left_select.limit_with_ties = false;
1909            let relation = self.plan_select_relation(&left_select, outer_scope, &ctes)?;
1910
1911            return self.apply_set_operations_and_tail(
1912                relation,
1913                &stmt.set_operations,
1914                &stmt.order_by,
1915                &stmt.limit,
1916                &stmt.offset,
1917                stmt.limit_with_ties,
1918                outer_scope,
1919                &ctes,
1920            );
1921        }
1922
1923        let mut relation = self.plan_from_items(&stmt.from, stmt.span, outer_scope, &ctes)?;
1924        let expr_scope = relation
1925            .scope
1926            .iter()
1927            .cloned()
1928            .chain(offset_scope(outer_scope, relation.schema.len()))
1929            .collect::<Vec<_>>();
1930
1931        let has_group_by = stmt
1932            .group_by
1933            .as_ref()
1934            .is_some_and(|items| !items.is_empty());
1935        let has_aggregate = self.select_contains_aggregate(stmt);
1936        let has_window = select_contains_window(stmt);
1937        if stmt.qualify.is_some() && !has_window {
1938            return Err(PlannerError::invalid_expression(
1939                "QUALIFY requires at least one window function in the query block".to_string(),
1940            ));
1941        }
1942        let distinct_only =
1943            stmt.distinct && !has_group_by && !has_aggregate && stmt.having.is_none();
1944
1945        let has_distinct_on = !stmt.distinct_on.is_empty();
1946        if has_distinct_on {
1947            // D10: the grammar cannot produce both, so a hand-built AST with
1948            // both set is a defect of the producer, not a user error.
1949            if stmt.distinct {
1950                return Err(PlannerError::invalid_expression(
1951                    "DISTINCT and DISTINCT ON cannot be combined".to_string(),
1952                ));
1953            }
1954            // D6: DISTINCT ON keys are scalar sort keys.
1955            for key in &stmt.distinct_on {
1956                if expr_contains_aggregate(key) || expr_contains_window(key) {
1957                    return Err(PlannerError::invalid_expression(
1958                        "DISTINCT ON expressions cannot contain aggregate or window functions"
1959                            .to_string(),
1960                    ));
1961                }
1962                if expr_contains_subquery(key) {
1963                    return Err(PlannerError::invalid_expression(
1964                        "DISTINCT ON expressions cannot contain subqueries".to_string(),
1965                    ));
1966                }
1967            }
1968            // D7: v1 rejects combinations whose deduplication order is not
1969            // covered by the DistinctOn sort contract.
1970            if has_group_by || has_aggregate || stmt.having.is_some() {
1971                return Err(PlannerError::unsupported_feature(
1972                    "DISTINCT ON with GROUP BY, aggregate functions, or HAVING",
1973                    "a future version",
1974                    stmt.span,
1975                ));
1976            }
1977            if has_window || stmt.qualify.is_some() {
1978                return Err(PlannerError::unsupported_feature(
1979                    "DISTINCT ON with window functions or QUALIFY",
1980                    "a future version",
1981                    stmt.span,
1982                ));
1983            }
1984        }
1985
1986        if stmt.having.as_ref().is_some_and(expr_contains_window) {
1987            return Err(PlannerError::invalid_expression(
1988                "HAVING cannot contain window functions".to_string(),
1989            ));
1990        }
1991        if stmt.group_by.as_ref().is_some_and(|items| {
1992            items
1993                .iter()
1994                .flat_map(GroupByItem::exprs)
1995                .any(expr_contains_window)
1996        }) {
1997            return Err(PlannerError::invalid_expression(
1998                "GROUP BY cannot contain window functions".to_string(),
1999            ));
2000        }
2001        // D5: GROUPING/GROUPING_ID are meaningful only over aggregate output.
2002        if stmt.group_by.as_ref().is_some_and(|items| {
2003            items
2004                .iter()
2005                .flat_map(GroupByItem::exprs)
2006                .any(expr_contains_grouping)
2007        }) {
2008            return Err(PlannerError::invalid_expression(
2009                "GROUPING is not allowed in GROUP BY".to_string(),
2010            ));
2011        }
2012        if stmt.selection.as_ref().is_some_and(expr_contains_grouping) {
2013            return Err(PlannerError::invalid_expression(
2014                "GROUPING is not allowed in WHERE".to_string(),
2015            ));
2016        }
2017        if !(has_group_by || has_aggregate || stmt.having.is_some()) {
2018            let grouping_present = stmt.projection.iter().any(|item| match item {
2019                SelectItem::Expr { expr, .. } => expr_contains_grouping(expr),
2020                SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => false,
2021            }) || stmt
2022                .order_by
2023                .iter()
2024                .any(|order| expr_contains_grouping(&order.expr))
2025                || stmt.qualify.as_ref().is_some_and(expr_contains_grouping);
2026            if grouping_present {
2027                return Err(PlannerError::invalid_expression(
2028                    "GROUPING is only allowed in grouped queries".to_string(),
2029                ));
2030            }
2031        }
2032
2033        // SELECT-list aliases are visible to HAVING, QUALIFY, and ORDER BY.
2034        // `expr_scope` above stays alias-free so that WHERE and GROUP BY keep
2035        // resolving against the FROM-derived base relations.
2036        let projection_aliases = collect_projection_aliases(&stmt.projection);
2037
2038        let final_projection = self.build_projection_with_scope(
2039            &stmt.projection,
2040            &relation.schema,
2041            &expr_scope,
2042            &ctes,
2043        )?;
2044        if !has_window {
2045            install_base_projection(&mut relation.plan, &final_projection);
2046        }
2047        let needs_project_boundary = !matches!(relation.plan, LogicalPlan::Scan { .. });
2048        let base_schema = relation.schema.clone();
2049        let mut plan = relation.plan;
2050
2051        // 3. Add Filter if WHERE clause is present
2052        if let Some(ref selection) = stmt.selection {
2053            if expr_contains_window(selection) {
2054                return Err(PlannerError::invalid_expression(
2055                    "WHERE cannot contain window functions".to_string(),
2056                ));
2057            }
2058            let predicate = self.infer_expr_with_scope(selection, &expr_scope, &ctes)?;
2059
2060            // Verify predicate returns Boolean
2061            if predicate.resolved_type != ResolvedType::Boolean {
2062                return Err(PlannerError::type_mismatch(
2063                    "Boolean",
2064                    predicate.resolved_type.to_string(),
2065                    selection.span,
2066                ));
2067            }
2068
2069            plan = LogicalPlan::Filter {
2070                input: Box::new(plan),
2071                predicate,
2072            };
2073        }
2074
2075        if has_window && (has_group_by || has_aggregate || stmt.having.is_some()) {
2076            return self.plan_grouped_window_select(
2077                stmt,
2078                &ctes,
2079                &expr_scope,
2080                &projection_aliases,
2081                plan,
2082            );
2083        }
2084
2085        if has_window {
2086            let mut windows = Vec::new();
2087            let mut window_map = HashMap::new();
2088            if let Projection::Columns(columns) = &final_projection {
2089                for column in columns {
2090                    self.collect_windows_from_typed_expr(
2091                        &column.expr,
2092                        &mut windows,
2093                        &mut window_map,
2094                    )?;
2095                }
2096            }
2097
2098            let mut outer_order_by = Vec::new();
2099            for order_expr in &stmt.order_by {
2100                let sort_source =
2101                    substitute_projection_aliases(&order_expr.expr, &projection_aliases);
2102                let typed = self.infer_expr_with_scope(&sort_source, &expr_scope, &ctes)?;
2103                self.collect_windows_from_typed_expr(&typed, &mut windows, &mut window_map)?;
2104                outer_order_by.push(SortExpr::new(
2105                    typed,
2106                    order_expr.asc.unwrap_or(true),
2107                    order_expr.nulls_first.unwrap_or(false),
2108                ));
2109            }
2110
2111            let qualify = if let Some(qualify) = &stmt.qualify {
2112                let source = substitute_projection_aliases(qualify, &projection_aliases);
2113                let typed = self.infer_expr_with_scope(&source, &expr_scope, &ctes)?;
2114                if typed.resolved_type != ResolvedType::Boolean {
2115                    return Err(PlannerError::type_mismatch(
2116                        "Boolean",
2117                        typed.resolved_type.type_name().to_string(),
2118                        typed.span,
2119                    ));
2120                }
2121                self.collect_windows_from_typed_expr(&typed, &mut windows, &mut window_map)?;
2122                Some(typed)
2123            } else {
2124                None
2125            };
2126
2127            let window_names = (0..windows.len())
2128                .map(|idx| format!("__window_{idx}"))
2129                .collect::<Vec<_>>();
2130            let mut window_schema = base_schema;
2131            window_schema.extend(windows.iter().enumerate().map(|(idx, window)| {
2132                ColumnMetadata::new(window_names[idx].clone(), window.result_type.clone())
2133            }));
2134
2135            let projection = rewrite_projection_for_windows(
2136                &final_projection,
2137                &window_map,
2138                relation.schema.len(),
2139                &window_names,
2140            )?;
2141            let order_by = outer_order_by
2142                .into_iter()
2143                .map(|sort| {
2144                    Ok(SortExpr::new(
2145                        rewrite_expr_for_windows(
2146                            &sort.expr,
2147                            &window_map,
2148                            relation.schema.len(),
2149                            &window_names,
2150                        )?,
2151                        sort.asc,
2152                        sort.nulls_first,
2153                    ))
2154                })
2155                .collect::<Result<Vec<_>, PlannerError>>()?;
2156            let qualify = qualify
2157                .as_ref()
2158                .map(|expr| {
2159                    rewrite_expr_for_windows(
2160                        expr,
2161                        &window_map,
2162                        relation.schema.len(),
2163                        &window_names,
2164                    )
2165                })
2166                .transpose()?;
2167
2168            return self.finish_window_select(
2169                stmt,
2170                WindowSelectStages {
2171                    plan,
2172                    windows,
2173                    qualify,
2174                    projection,
2175                    order_by,
2176                    window_schema,
2177                },
2178            );
2179        }
2180
2181        if has_group_by || has_aggregate || stmt.having.is_some() || stmt.distinct {
2182            if !has_group_by && !has_aggregate && stmt.having.is_some() {
2183                return Err(PlannerError::invalid_expression(
2184                    "HAVING requires GROUP BY or aggregate functions".to_string(),
2185                ));
2186            }
2187
2188            let (group_keys, grouping_sets, projected) = if distinct_only {
2189                let projected = self.build_projected_columns_for_distinct_with_scope(
2190                    &stmt.projection,
2191                    &relation.schema,
2192                    &expr_scope,
2193                    &ctes,
2194                )?;
2195                let group_keys = projected.iter().map(|col| col.expr.clone()).collect();
2196                (group_keys, None, projected)
2197            } else {
2198                let expanded = self.expand_group_by_items(stmt, &expr_scope, &ctes)?;
2199                let projected = self.build_projected_columns_for_aggregate_with_scope(
2200                    &stmt.projection,
2201                    &expr_scope,
2202                    &ctes,
2203                )?;
2204                (expanded.group_keys, expanded.grouping_sets, projected)
2205            };
2206            let mut aggregates = Vec::new();
2207            let mut agg_map = HashMap::new();
2208
2209            for col in &projected {
2210                self.collect_aggregates_from_typed_expr(&col.expr, &mut aggregates, &mut agg_map)?;
2211            }
2212
2213            let having_typed = if let Some(having) = &stmt.having {
2214                let having = substitute_projection_aliases(having, &projection_aliases);
2215                let typed = self.infer_expr_with_scope(&having, &expr_scope, &ctes)?;
2216                if typed.resolved_type != ResolvedType::Boolean {
2217                    return Err(PlannerError::type_mismatch(
2218                        "Boolean",
2219                        typed.resolved_type.type_name().to_string(),
2220                        typed.span,
2221                    ));
2222                }
2223                self.collect_aggregates_from_typed_expr(&typed, &mut aggregates, &mut agg_map)?;
2224                Some(typed)
2225            } else {
2226                None
2227            };
2228
2229            let mut order_by = Vec::new();
2230            if !stmt.order_by.is_empty() {
2231                for order_expr in &stmt.order_by {
2232                    let sort_source =
2233                        substitute_projection_aliases(&order_expr.expr, &projection_aliases);
2234                    let typed = self.infer_expr_with_scope(&sort_source, &expr_scope, &ctes)?;
2235                    self.collect_aggregates_from_typed_expr(&typed, &mut aggregates, &mut agg_map)?;
2236                    let asc = order_expr.asc.unwrap_or(true);
2237                    let nulls_first = order_expr.nulls_first.unwrap_or(false);
2238                    order_by.push(SortExpr::new(typed, asc, nulls_first));
2239                }
2240            }
2241
2242            if let Some(ref having) = having_typed {
2243                self.type_checker
2244                    .validate_having_expr(having, &group_keys, &aggregates)?;
2245            }
2246
2247            let mut output_schema = build_aggregate_schema(&group_keys, &aggregates);
2248            // GROUPING rewrites resolve indexes against the key+aggregate
2249            // names only; the trailing __grouping_id column is appended to
2250            // the schema afterwards so the group-key arithmetic inside
2251            // rewrite_expr_with_maps stays exact (issue #149).
2252            let output_names: Vec<String> = output_schema.iter().map(|c| c.name.clone()).collect();
2253            let grouping_rewrite = GroupingRewrite::new(&group_keys, &aggregates, &grouping_sets);
2254            if grouping_sets.is_some() {
2255                output_schema.push(ColumnMetadata::new(
2256                    GROUPING_ID_COLUMN,
2257                    ResolvedType::BigInt,
2258                ));
2259            }
2260
2261            let projection = self.build_aggregate_projection(
2262                projected,
2263                &group_keys,
2264                &aggregates,
2265                &output_names,
2266                Some(&grouping_rewrite),
2267            )?;
2268
2269            let having = if let Some(having) = having_typed {
2270                Some(self.rewrite_expr_for_aggregate(
2271                    &having,
2272                    &group_keys,
2273                    &aggregates,
2274                    &output_names,
2275                    Some(&grouping_rewrite),
2276                )?)
2277            } else {
2278                None
2279            };
2280
2281            let order_by = order_by
2282                .into_iter()
2283                .map(|expr| {
2284                    let rewritten = self.rewrite_expr_for_aggregate(
2285                        &expr.expr,
2286                        &group_keys,
2287                        &aggregates,
2288                        &output_names,
2289                        Some(&grouping_rewrite),
2290                    )?;
2291                    Ok(SortExpr::new(rewritten, expr.asc, expr.nulls_first))
2292                })
2293                .collect::<Result<Vec<_>, PlannerError>>()?;
2294
2295            let schema = projection_schema(&projection, &output_schema);
2296            plan = LogicalPlan::Aggregate {
2297                input: Box::new(plan),
2298                group_keys,
2299                aggregates,
2300                having,
2301                projection,
2302                grouping_sets,
2303            };
2304
2305            if !order_by.is_empty() {
2306                plan = LogicalPlan::Sort {
2307                    input: Box::new(plan),
2308                    order_by,
2309                };
2310            }
2311
2312            plan = self.apply_pagination(plan, &stmt.limit, &stmt.offset, stmt.limit_with_ties)?;
2313
2314            return Ok(PlannedRelation {
2315                plan,
2316                schema: schema.clone(),
2317                scope: vec![ScopedTable::new(
2318                    TableMetadata::new(LITERAL_TABLE, schema),
2319                    0,
2320                )],
2321            });
2322        }
2323
2324        // Non-aggregate path: ORDER BY + LIMIT/OFFSET
2325        let mut distinct_on_tie_keys: Option<Vec<SortExpr>> = None;
2326        if has_distinct_on {
2327            // D2/D3/D4: type the deduplicated ON keys and the user ORDER BY,
2328            // verify the prefix contract, and synthesize the complete
2329            // deterministic sort specification. The DistinctOn node emits
2330            // rows already ordered by that specification, so no separate
2331            // Sort node is planned (D8). LIMIT/OFFSET applies after
2332            // deduplication.
2333            let mut key_exprs: Vec<TypedExpr> = Vec::new();
2334            let mut key_signatures = HashSet::new();
2335            for key in &stmt.distinct_on {
2336                let source = substitute_projection_aliases(key, &projection_aliases);
2337                let typed = self.infer_expr_with_scope(&source, &expr_scope, &ctes)?;
2338                if key_signatures.insert(distinct_on_expr_signature(&typed)) {
2339                    key_exprs.push(typed);
2340                }
2341            }
2342            let user_order_by = self.build_sort_exprs_with_scope(
2343                &stmt.order_by,
2344                &expr_scope,
2345                &projection_aliases,
2346                &ctes,
2347            )?;
2348            // D13: `build_distinct_on_sort_spec` places the user's ORDER BY
2349            // items first, so the leading `user_order_by_len` entries of the
2350            // effective specification are exactly that ORDER BY. WITH TIES
2351            // must read its peer groups from those entries alone: the
2352            // synthesized implicit ON keys and the all-column tie-breaker
2353            // tail make every surviving row unique, which would silently
2354            // degrade WITH TIES to a plain LIMIT.
2355            let user_order_by_len = user_order_by.len();
2356            let (key_count, order_by) =
2357                build_distinct_on_sort_spec(key_exprs, user_order_by, &base_schema, stmt.span)?;
2358            distinct_on_tie_keys = Some(order_by[..user_order_by_len.min(order_by.len())].to_vec());
2359            plan = LogicalPlan::DistinctOn {
2360                input: Box::new(plan),
2361                key_count,
2362                order_by,
2363            };
2364        } else if !stmt.order_by.is_empty() {
2365            let order_by = self.build_sort_exprs_with_scope(
2366                &stmt.order_by,
2367                &expr_scope,
2368                &projection_aliases,
2369                &ctes,
2370            )?;
2371            plan = LogicalPlan::Sort {
2372                input: Box::new(plan),
2373                order_by,
2374            };
2375        }
2376
2377        plan = self.apply_pagination_with_tie_keys(
2378            plan,
2379            &stmt.limit,
2380            &stmt.offset,
2381            stmt.limit_with_ties,
2382            distinct_on_tie_keys.as_deref(),
2383        )?;
2384
2385        let output_schema = projection_schema(&final_projection, &relation.schema);
2386        if needs_project_boundary {
2387            plan = LogicalPlan::Project {
2388                input: Box::new(plan),
2389                projection: final_projection,
2390            };
2391        }
2392        Ok(PlannedRelation {
2393            plan,
2394            schema: output_schema.clone(),
2395            scope: vec![ScopedTable::new(
2396                TableMetadata::new(LITERAL_TABLE, output_schema),
2397                0,
2398            )],
2399        })
2400    }
2401
2402    /// Plan the aggregate/HAVING stages that feed a window query.
2403    ///
2404    /// Grouped aggregation deliberately exposes its internal group-key plus
2405    /// aggregate schema to the window stage. The user-facing projection stays
2406    /// above the window so aggregate results can participate in window
2407    /// arguments and ordering without being evaluated against base rows.
2408    fn plan_grouped_window_select(
2409        &self,
2410        stmt: &Select,
2411        ctes: &CtePlans,
2412        expr_scope: &[ScopedTable],
2413        projection_aliases: &HashMap<String, crate::ast::expr::Expr>,
2414        mut plan: LogicalPlan,
2415    ) -> Result<PlannedRelation, PlannerError> {
2416        let expanded = self.expand_group_by_items(stmt, expr_scope, ctes)?;
2417        let group_keys = expanded.group_keys;
2418        let grouping_sets = expanded.grouping_sets;
2419        let projected = self.build_projected_columns_for_aggregate_with_scope(
2420            &stmt.projection,
2421            expr_scope,
2422            ctes,
2423        )?;
2424        let mut aggregates = Vec::new();
2425        let mut aggregate_map = HashMap::new();
2426        for column in &projected {
2427            self.collect_aggregates_from_typed_expr(
2428                &column.expr,
2429                &mut aggregates,
2430                &mut aggregate_map,
2431            )?;
2432        }
2433
2434        let having_typed = if let Some(having) = &stmt.having {
2435            let having = substitute_projection_aliases(having, projection_aliases);
2436            if expr_contains_window(&having) {
2437                return Err(PlannerError::invalid_expression(
2438                    "HAVING cannot contain window functions".to_string(),
2439                ));
2440            }
2441            let typed = self.infer_expr_with_scope(&having, expr_scope, ctes)?;
2442            if typed.resolved_type != ResolvedType::Boolean {
2443                return Err(PlannerError::type_mismatch(
2444                    "Boolean",
2445                    typed.resolved_type.type_name().to_string(),
2446                    typed.span,
2447                ));
2448            }
2449            self.collect_aggregates_from_typed_expr(&typed, &mut aggregates, &mut aggregate_map)?;
2450            Some(typed)
2451        } else {
2452            None
2453        };
2454
2455        let qualify_typed = if let Some(qualify) = &stmt.qualify {
2456            let qualify = substitute_projection_aliases(qualify, projection_aliases);
2457            let typed = self.infer_expr_with_scope(&qualify, expr_scope, ctes)?;
2458            if typed.resolved_type != ResolvedType::Boolean {
2459                return Err(PlannerError::type_mismatch(
2460                    "Boolean",
2461                    typed.resolved_type.type_name().to_string(),
2462                    typed.span,
2463                ));
2464            }
2465            self.collect_aggregates_from_typed_expr(&typed, &mut aggregates, &mut aggregate_map)?;
2466            Some(typed)
2467        } else {
2468            None
2469        };
2470
2471        let mut outer_order_by = Vec::new();
2472        for order_expr in &stmt.order_by {
2473            let source = substitute_projection_aliases(&order_expr.expr, projection_aliases);
2474            let typed = self.infer_expr_with_scope(&source, expr_scope, ctes)?;
2475            self.collect_aggregates_from_typed_expr(&typed, &mut aggregates, &mut aggregate_map)?;
2476            outer_order_by.push(SortExpr::new(
2477                typed,
2478                order_expr.asc.unwrap_or(true),
2479                order_expr.nulls_first.unwrap_or(false),
2480            ));
2481        }
2482
2483        if let Some(having) = &having_typed {
2484            self.type_checker
2485                .validate_having_expr(having, &group_keys, &aggregates)?;
2486        }
2487
2488        let mut aggregate_schema = build_aggregate_schema(&group_keys, &aggregates);
2489        // GROUPING rewrites resolve indexes against key+aggregate names only;
2490        // the __grouping_id column joins the schema afterwards (issue #149).
2491        let rewrite_names = aggregate_schema
2492            .iter()
2493            .map(|column| column.name.clone())
2494            .collect::<Vec<_>>();
2495        let grouping_rewrite = GroupingRewrite::new(&group_keys, &aggregates, &grouping_sets);
2496        if grouping_sets.is_some() {
2497            aggregate_schema.push(ColumnMetadata::new(
2498                GROUPING_ID_COLUMN,
2499                ResolvedType::BigInt,
2500            ));
2501        }
2502        let aggregate_names = aggregate_schema
2503            .iter()
2504            .map(|column| column.name.clone())
2505            .collect::<Vec<_>>();
2506        let projection = self.build_aggregate_projection(
2507            projected,
2508            &group_keys,
2509            &aggregates,
2510            &rewrite_names,
2511            Some(&grouping_rewrite),
2512        )?;
2513        let having = having_typed
2514            .as_ref()
2515            .map(|expr| {
2516                self.rewrite_expr_for_aggregate(
2517                    expr,
2518                    &group_keys,
2519                    &aggregates,
2520                    &rewrite_names,
2521                    Some(&grouping_rewrite),
2522                )
2523            })
2524            .transpose()?;
2525        let outer_order_by = outer_order_by
2526            .into_iter()
2527            .map(|sort| {
2528                Ok(SortExpr::new(
2529                    self.rewrite_expr_for_aggregate(
2530                        &sort.expr,
2531                        &group_keys,
2532                        &aggregates,
2533                        &rewrite_names,
2534                        Some(&grouping_rewrite),
2535                    )?,
2536                    sort.asc,
2537                    sort.nulls_first,
2538                ))
2539            })
2540            .collect::<Result<Vec<_>, PlannerError>>()?;
2541        let qualify = qualify_typed
2542            .as_ref()
2543            .map(|expr| {
2544                self.rewrite_expr_for_aggregate(
2545                    expr,
2546                    &group_keys,
2547                    &aggregates,
2548                    &rewrite_names,
2549                    Some(&grouping_rewrite),
2550                )
2551            })
2552            .transpose()?;
2553
2554        plan = LogicalPlan::Aggregate {
2555            input: Box::new(plan),
2556            group_keys,
2557            aggregates,
2558            having,
2559            projection: Projection::All(aggregate_names),
2560            grouping_sets,
2561        };
2562
2563        let mut windows = Vec::new();
2564        let mut window_map = HashMap::new();
2565        if let Projection::Columns(columns) = &projection {
2566            for column in columns {
2567                self.collect_windows_from_typed_expr(&column.expr, &mut windows, &mut window_map)?;
2568            }
2569        }
2570        for sort in &outer_order_by {
2571            self.collect_windows_from_typed_expr(&sort.expr, &mut windows, &mut window_map)?;
2572        }
2573        if let Some(qualify) = &qualify {
2574            self.collect_windows_from_typed_expr(qualify, &mut windows, &mut window_map)?;
2575        }
2576
2577        let window_names = (0..windows.len())
2578            .map(|index| format!("__window_{index}"))
2579            .collect::<Vec<_>>();
2580        let mut window_schema = aggregate_schema;
2581        window_schema.extend(windows.iter().enumerate().map(|(index, window)| {
2582            ColumnMetadata::new(window_names[index].clone(), window.result_type.clone())
2583        }));
2584        let projection = rewrite_projection_for_windows(
2585            &projection,
2586            &window_map,
2587            window_schema.len() - windows.len(),
2588            &window_names,
2589        )?;
2590        let outer_order_by = outer_order_by
2591            .into_iter()
2592            .map(|sort| {
2593                Ok(SortExpr::new(
2594                    rewrite_expr_for_windows(
2595                        &sort.expr,
2596                        &window_map,
2597                        window_schema.len() - windows.len(),
2598                        &window_names,
2599                    )?,
2600                    sort.asc,
2601                    sort.nulls_first,
2602                ))
2603            })
2604            .collect::<Result<Vec<_>, PlannerError>>()?;
2605        let qualify = qualify
2606            .as_ref()
2607            .map(|expr| {
2608                rewrite_expr_for_windows(
2609                    expr,
2610                    &window_map,
2611                    window_schema.len() - windows.len(),
2612                    &window_names,
2613                )
2614            })
2615            .transpose()?;
2616
2617        self.finish_window_select(
2618            stmt,
2619            WindowSelectStages {
2620                plan,
2621                windows,
2622                qualify,
2623                projection,
2624                order_by: outer_order_by,
2625                window_schema,
2626            },
2627        )
2628    }
2629
2630    /// Finish the stages shared by base-row and grouped window queries.
2631    fn finish_window_select(
2632        &self,
2633        stmt: &Select,
2634        stages: WindowSelectStages,
2635    ) -> Result<PlannedRelation, PlannerError> {
2636        let WindowSelectStages {
2637            mut plan,
2638            windows,
2639            qualify,
2640            projection,
2641            order_by,
2642            window_schema,
2643        } = stages;
2644        plan = LogicalPlan::Window {
2645            input: Box::new(plan),
2646            windows,
2647        };
2648        if let Some(predicate) = qualify {
2649            plan = LogicalPlan::Filter {
2650                input: Box::new(plan),
2651                predicate,
2652            };
2653        }
2654
2655        let output_schema = projection_schema(&projection, &window_schema);
2656        let mut hidden_order_keys = Vec::new();
2657        let mut projected_order_by = Vec::with_capacity(order_by.len());
2658        for sort in order_by {
2659            let expr =
2660                match rewrite_expr_for_projected_output(&sort.expr, &projection, &output_schema) {
2661                    Ok(expr) => expr,
2662                    Err(_) if !stmt.distinct => {
2663                        let hidden_index = hidden_order_keys.len();
2664                        let name = format!("__order_{hidden_index}");
2665                        let output_index = output_schema.len() + hidden_index;
2666                        hidden_order_keys.push(ProjectedColumn {
2667                            expr: sort.expr.clone(),
2668                            alias: Some(name.clone()),
2669                        });
2670                        TypedExpr::column_ref(
2671                            "__project__".to_string(),
2672                            name,
2673                            output_index,
2674                            sort.expr.resolved_type.clone(),
2675                            sort.expr.span,
2676                        )
2677                    }
2678                    Err(error) => return Err(error),
2679                };
2680            projected_order_by.push(SortExpr::new(expr, sort.asc, sort.nulls_first));
2681        }
2682        let has_hidden_order_keys = !hidden_order_keys.is_empty();
2683        let projection = if has_hidden_order_keys {
2684            let mut columns = match projection {
2685                Projection::Columns(columns) => columns,
2686                Projection::All(_) => output_schema
2687                    .iter()
2688                    .enumerate()
2689                    .map(|(index, column)| ProjectedColumn {
2690                        expr: TypedExpr::column_ref(
2691                            "__window__".to_string(),
2692                            column.name.clone(),
2693                            index,
2694                            column.data_type.clone(),
2695                            stmt.span,
2696                        ),
2697                        alias: None,
2698                    })
2699                    .collect(),
2700            };
2701            columns.extend(hidden_order_keys);
2702            Projection::Columns(columns)
2703        } else {
2704            projection
2705        };
2706        plan = LogicalPlan::Project {
2707            input: Box::new(plan),
2708            projection,
2709        };
2710
2711        if stmt.distinct {
2712            let group_keys = output_schema
2713                .iter()
2714                .enumerate()
2715                .map(|(index, column)| {
2716                    TypedExpr::column_ref(
2717                        LITERAL_TABLE.to_string(),
2718                        column.name.clone(),
2719                        index,
2720                        column.data_type.clone(),
2721                        stmt.span,
2722                    )
2723                })
2724                .collect();
2725            plan = LogicalPlan::Aggregate {
2726                input: Box::new(plan),
2727                group_keys,
2728                aggregates: Vec::new(),
2729                having: None,
2730                projection: Projection::All(
2731                    output_schema
2732                        .iter()
2733                        .map(|column| column.name.clone())
2734                        .collect(),
2735                ),
2736                grouping_sets: None,
2737            };
2738        }
2739        if !projected_order_by.is_empty() {
2740            plan = LogicalPlan::Sort {
2741                input: Box::new(plan),
2742                order_by: projected_order_by,
2743            };
2744        }
2745        plan = self.apply_pagination(plan, &stmt.limit, &stmt.offset, stmt.limit_with_ties)?;
2746        if has_hidden_order_keys {
2747            plan = LogicalPlan::Project {
2748                input: Box::new(plan),
2749                projection: Projection::Columns(
2750                    output_schema
2751                        .iter()
2752                        .enumerate()
2753                        .map(|(index, column)| ProjectedColumn {
2754                            expr: TypedExpr::column_ref(
2755                                "__ordered__".to_string(),
2756                                column.name.clone(),
2757                                index,
2758                                column.data_type.clone(),
2759                                stmt.span,
2760                            ),
2761                            alias: Some(column.name.clone()),
2762                        })
2763                        .collect(),
2764                ),
2765            };
2766        }
2767
2768        Ok(PlannedRelation {
2769            plan,
2770            schema: output_schema.clone(),
2771            scope: vec![ScopedTable::new(
2772                TableMetadata::new(LITERAL_TABLE, output_schema),
2773                0,
2774            )],
2775        })
2776    }
2777
2778    /// Build the projection for a SELECT statement.
2779    ///
2780    /// Handles wildcard expansion and expression type checking.
2781    fn plan_from_items(
2782        &self,
2783        items: &[FromItem],
2784        select_span: crate::ast::Span,
2785        outer_scope: &[ScopedTable],
2786        ctes: &CtePlans,
2787    ) -> Result<PlannedRelation, PlannerError> {
2788        match items {
2789            [] => {
2790                let schema = Vec::new();
2791                Ok(PlannedRelation {
2792                    plan: LogicalPlan::Scan {
2793                        table: LITERAL_TABLE.to_string(),
2794                        projection: Projection::All(Vec::new()),
2795                    },
2796                    schema: schema.clone(),
2797                    scope: vec![ScopedTable::new(
2798                        TableMetadata::new(LITERAL_TABLE, schema),
2799                        0,
2800                    )],
2801                })
2802            }
2803            [single] => self.plan_from_item(single, 0, outer_scope, outer_scope, ctes),
2804            [first, rest @ ..] => {
2805                let mut relation = self.plan_from_item(first, 0, outer_scope, outer_scope, ctes)?;
2806                for item in rest {
2807                    // Comma-separated items are an implicit cross join, so a
2808                    // later item may be LATERAL over everything to its left.
2809                    let left_width = relation.schema.len();
2810                    let lateral_scope =
2811                        lateral_outer_scope(&relation.scope, 0, left_width, outer_scope);
2812                    let right =
2813                        self.plan_from_item(item, left_width, outer_scope, &lateral_scope, ctes)?;
2814                    relation = if from_item_is_lateral(item) {
2815                        self.combine_lateral_join_relation(
2816                            relation,
2817                            right,
2818                            JoinType::Cross,
2819                            None,
2820                            None,
2821                            select_span,
2822                        )?
2823                    } else {
2824                        self.combine_join_relation(
2825                            relation,
2826                            right,
2827                            JoinType::Cross,
2828                            None,
2829                            None,
2830                            select_span,
2831                        )?
2832                    };
2833                }
2834                Ok(relation)
2835            }
2836        }
2837    }
2838
2839    fn plan_from_item(
2840        &self,
2841        item: &FromItem,
2842        start_index: usize,
2843        outer_scope: &[ScopedTable],
2844        lateral_scope: &[ScopedTable],
2845        ctes: &CtePlans,
2846    ) -> Result<PlannedRelation, PlannerError> {
2847        match item {
2848            FromItem::Table {
2849                name,
2850                alias,
2851                columns,
2852                span,
2853            } => {
2854                if let Some(cte) = ctes.get(name) {
2855                    let mut relation = cte.clone();
2856                    relation.plan = LogicalPlan::Project {
2857                        input: Box::new(relation.plan),
2858                        projection: Projection::All(
2859                            relation.schema.iter().map(|col| col.name.clone()).collect(),
2860                        ),
2861                    };
2862                    let relation_name = alias.clone().unwrap_or_else(|| name.clone());
2863                    apply_alias_columns(&relation_name, columns, &mut relation.schema, *span)?;
2864                    relation.scope = vec![ScopedTable::new(
2865                        TableMetadata::new(relation_name, relation.schema.clone()),
2866                        start_index,
2867                    )];
2868                    return Ok(relation);
2869                }
2870                let table = self.name_resolver.resolve_table(name, *span)?.clone();
2871                let mut scope_table = table.clone();
2872                if let Some(alias) = alias {
2873                    scope_table.name = alias.clone();
2874                }
2875                let mut schema = table.columns.clone();
2876                // The physical scan keeps the stored column names; only the
2877                // relation the query sees is renamed (issue #151, D8).
2878                let projection =
2879                    Projection::All(schema.iter().map(|col| col.name.clone()).collect());
2880                apply_alias_columns(&scope_table.name, columns, &mut schema, *span)?;
2881                scope_table.columns = schema.clone();
2882                Ok(PlannedRelation {
2883                    plan: LogicalPlan::Scan {
2884                        table: name.clone(),
2885                        projection,
2886                    },
2887                    schema,
2888                    scope: vec![ScopedTable::new(scope_table, start_index)],
2889                })
2890            }
2891            FromItem::Function {
2892                name,
2893                args,
2894                alias,
2895                columns,
2896                with_ordinality,
2897                span,
2898                ..
2899            } => self.plan_table_function(
2900                name,
2901                args,
2902                alias.as_deref(),
2903                columns,
2904                *span,
2905                start_index,
2906                lateral_scope,
2907                ctes,
2908                *with_ordinality,
2909            ),
2910            FromItem::Join {
2911                left,
2912                right,
2913                join_type,
2914                condition,
2915                using,
2916                natural,
2917                span,
2918            } => {
2919                let right_lateral = from_item_is_lateral(right);
2920                if right_lateral {
2921                    // PostgreSQL rejects the same shape: a correlated relation
2922                    // cannot be the null-supplying side of the join (D3).
2923                    let unsupported = match join_type {
2924                        crate::ast::dml::JoinType::Right => Some("RIGHT"),
2925                        crate::ast::dml::JoinType::Full => Some("FULL"),
2926                        _ => None,
2927                    };
2928                    if let Some(kind) = unsupported {
2929                        return Err(PlannerError::lateral_join_type_unsupported(kind, *span));
2930                    }
2931                }
2932                let left_relation =
2933                    self.plan_from_item(left, start_index, outer_scope, lateral_scope, ctes)?;
2934                // A LATERAL right side is planned against the left row, which
2935                // is the row the executor supplies as its outer row. An
2936                // ordinary right side never reads this scope, so the rebase is
2937                // only paid where it is used.
2938                let right_lateral_scope = right_lateral.then(|| {
2939                    lateral_outer_scope(
2940                        &left_relation.scope,
2941                        start_index,
2942                        left_relation.schema.len(),
2943                        outer_scope,
2944                    )
2945                });
2946                let right_relation = self.plan_from_item(
2947                    right,
2948                    start_index + left_relation.schema.len(),
2949                    outer_scope,
2950                    right_lateral_scope.as_deref().unwrap_or(lateral_scope),
2951                    ctes,
2952                )?;
2953                let expr_scope = left_relation
2954                    .scope
2955                    .iter()
2956                    .cloned()
2957                    .chain(right_relation.scope.iter().cloned())
2958                    .chain(offset_scope(
2959                        outer_scope,
2960                        left_relation.schema.len() + right_relation.schema.len(),
2961                    ))
2962                    .collect::<Vec<_>>();
2963                let using = if *natural {
2964                    Some(natural_join_columns(
2965                        &left_relation.schema,
2966                        &right_relation.schema,
2967                    ))
2968                } else {
2969                    using.clone()
2970                };
2971                let typed_condition = if let Some(expr) = condition {
2972                    let typed = self.infer_expr_with_scope(expr, &expr_scope, ctes)?;
2973                    if typed.resolved_type != ResolvedType::Boolean {
2974                        return Err(PlannerError::type_mismatch(
2975                            "Boolean",
2976                            typed.resolved_type.to_string(),
2977                            expr.span,
2978                        ));
2979                    }
2980                    Some(typed)
2981                } else {
2982                    self.build_using_condition(
2983                        using.as_deref(),
2984                        &left_relation,
2985                        &right_relation,
2986                        *span,
2987                    )?
2988                };
2989                if right_lateral {
2990                    self.combine_lateral_join_relation(
2991                        left_relation,
2992                        right_relation,
2993                        map_join_type(*join_type),
2994                        typed_condition,
2995                        using.as_deref(),
2996                        *span,
2997                    )
2998                } else {
2999                    self.combine_join_relation(
3000                        left_relation,
3001                        right_relation,
3002                        map_join_type(*join_type),
3003                        typed_condition,
3004                        using,
3005                        *span,
3006                    )
3007                }
3008            }
3009            FromItem::Derived {
3010                subquery,
3011                alias,
3012                columns,
3013                lateral,
3014                span,
3015            } => {
3016                // A derived table is evaluated independently of the query it
3017                // sits in, so nothing from the enclosing scopes is visible
3018                // inside it (D4). LATERAL is what lifts that restriction:
3019                // passing the scope through unconditionally would resolve an
3020                // outer name into a correlated reference the user never wrote.
3021                let visible: &[ScopedTable] = if *lateral { lateral_scope } else { &[] };
3022                let mut relation = self.plan_query_body_relation(subquery, visible, ctes)?;
3023                let alias = alias.clone().ok_or_else(|| {
3024                    PlannerError::invalid_expression("derived table requires an alias".to_string())
3025                })?;
3026                let source_names = relation
3027                    .schema
3028                    .iter()
3029                    .map(|column| column.name.clone())
3030                    .collect();
3031                relation.plan = LogicalPlan::Project {
3032                    input: Box::new(relation.plan),
3033                    projection: Projection::All(source_names),
3034                };
3035                apply_alias_columns(&alias, columns, &mut relation.schema, *span)?;
3036                relation.scope = vec![ScopedTable::new(
3037                    TableMetadata::new(alias, relation.schema.clone()),
3038                    start_index,
3039                )];
3040                Ok(relation)
3041            }
3042        }
3043    }
3044
3045    /// Plan a FROM-clause table function (issue #151).
3046    ///
3047    /// Arguments are typed against `lateral_scope`, which addresses the row the
3048    /// executor supplies as the outer row: the preceding FROM items followed by
3049    /// the enclosing query's own outer row. That holds whether or not LATERAL
3050    /// was written, because table-function arguments are implicitly lateral
3051    /// (D2).
3052    #[allow(clippy::too_many_arguments)]
3053    fn plan_table_function(
3054        &self,
3055        name: &str,
3056        args: &[Expr],
3057        alias: Option<&str>,
3058        columns: &[String],
3059        span: crate::ast::Span,
3060        start_index: usize,
3061        lateral_scope: &[ScopedTable],
3062        ctes: &CtePlans,
3063        with_ordinality: bool,
3064    ) -> Result<PlannedRelation, PlannerError> {
3065        let Some(mut function) = TableFunctionKind::from_name(name) else {
3066            return Err(PlannerError::unknown_table_function(name, span));
3067        };
3068        if with_ordinality {
3069            if function != TableFunctionKind::Unnest {
3070                return Err(PlannerError::invalid_expression(
3071                    "WITH ORDINALITY is supported only for UNNEST",
3072                ));
3073            }
3074            function = TableFunctionKind::UnnestWithOrdinality;
3075        }
3076        let (typed_args, mut schema) = match function {
3077            TableFunctionKind::Unnest | TableFunctionKind::UnnestWithOrdinality => {
3078                if args.len() != 1 {
3079                    return Err(PlannerError::invalid_expression(format!(
3080                        "table function UNNEST takes exactly 1 argument, found {}",
3081                        args.len()
3082                    )));
3083                }
3084                let typed = self.infer_expr_with_scope(&args[0], lateral_scope, ctes)?;
3085                if !matches!(
3086                    typed.resolved_type,
3087                    ResolvedType::Vector { .. } | ResolvedType::Array(_) | ResolvedType::Null
3088                ) {
3089                    return Err(PlannerError::type_mismatch(
3090                        "ARRAY or VECTOR",
3091                        typed.resolved_type.to_string(),
3092                        args[0].span,
3093                    ));
3094                }
3095                let element_type = match &typed.resolved_type {
3096                    ResolvedType::Array(element) => (**element).clone(),
3097                    ResolvedType::Vector { .. } => ResolvedType::Float,
3098                    ResolvedType::Null => ResolvedType::Null,
3099                    _ => unreachable!(),
3100                };
3101                let mut schema = vec![ColumnMetadata::new("unnest", element_type)];
3102                if function == TableFunctionKind::UnnestWithOrdinality {
3103                    schema.push(ColumnMetadata::new("ordinality", ResolvedType::BigInt));
3104                }
3105                (vec![typed], schema)
3106            }
3107            TableFunctionKind::GenerateSeries => {
3108                if !(2..=3).contains(&args.len()) {
3109                    return Err(PlannerError::invalid_expression(format!(
3110                        "table function GENERATE_SERIES takes 2 or 3 arguments, found {}",
3111                        args.len()
3112                    )));
3113                }
3114                let mut typed = Vec::with_capacity(args.len());
3115                for arg in args {
3116                    typed.push(self.infer_expr_with_scope(arg, lateral_scope, ctes)?);
3117                }
3118
3119                let timestamp_series = typed.len() == 3
3120                    && matches!(
3121                        typed[0].resolved_type,
3122                        ResolvedType::Timestamp | ResolvedType::Null
3123                    )
3124                    && matches!(
3125                        typed[1].resolved_type,
3126                        ResolvedType::Timestamp | ResolvedType::Null
3127                    )
3128                    && matches!(
3129                        typed[2].resolved_type,
3130                        ResolvedType::Interval | ResolvedType::Null
3131                    )
3132                    && typed
3133                        .iter()
3134                        .any(|arg| !matches!(arg.resolved_type, ResolvedType::Null));
3135                let output_type = if timestamp_series {
3136                    ResolvedType::Timestamp
3137                } else {
3138                    let mut output_type = ResolvedType::Integer;
3139                    for (arg, value) in args.iter().zip(&typed) {
3140                        match value.resolved_type {
3141                            ResolvedType::BigInt => output_type = ResolvedType::BigInt,
3142                            ResolvedType::Integer | ResolvedType::Null => {}
3143                            _ => {
3144                                return Err(PlannerError::type_mismatch(
3145                                    "INTEGER/BIGINT or (TIMESTAMP, TIMESTAMP, INTERVAL)",
3146                                    value.resolved_type.to_string(),
3147                                    arg.span,
3148                                ));
3149                            }
3150                        }
3151                    }
3152                    output_type
3153                };
3154                (
3155                    typed,
3156                    vec![ColumnMetadata::new("generate_series", output_type)],
3157                )
3158            }
3159            TableFunctionKind::JsonEach | TableFunctionKind::JsonTree => {
3160                if !(1..=2).contains(&args.len()) {
3161                    return Err(PlannerError::invalid_expression(format!(
3162                        "table function {} takes 1 or 2 arguments, found {}",
3163                        function.name(),
3164                        args.len()
3165                    )));
3166                }
3167                let mut typed = Vec::with_capacity(args.len());
3168                for arg in args {
3169                    let value = self.infer_expr_with_scope(arg, lateral_scope, ctes)?;
3170                    if !matches!(value.resolved_type, ResolvedType::Text | ResolvedType::Null) {
3171                        return Err(PlannerError::type_mismatch(
3172                            "TEXT",
3173                            value.resolved_type.to_string(),
3174                            arg.span,
3175                        ));
3176                    }
3177                    typed.push(value);
3178                }
3179                let schema = vec![
3180                    ColumnMetadata::new("key", ResolvedType::Text),
3181                    ColumnMetadata::new("value", ResolvedType::Text),
3182                    ColumnMetadata::new("type", ResolvedType::Text),
3183                    ColumnMetadata::new("atom", ResolvedType::Text),
3184                    ColumnMetadata::new("id", ResolvedType::BigInt),
3185                    ColumnMetadata::new("parent", ResolvedType::BigInt),
3186                    ColumnMetadata::new("fullkey", ResolvedType::Text),
3187                    ColumnMetadata::new("path", ResolvedType::Text),
3188                ];
3189                (typed, schema)
3190            }
3191            TableFunctionKind::FtsSearch => {
3192                if !(3..=4).contains(&args.len()) {
3193                    return Err(PlannerError::invalid_expression(format!(
3194                        "table function FTS_SEARCH takes 3 or 4 arguments, found {}",
3195                        args.len()
3196                    )));
3197                }
3198                let mut typed = Vec::with_capacity(args.len());
3199                for arg in args {
3200                    let value = self.infer_expr_with_scope(arg, lateral_scope, ctes)?;
3201                    if !matches!(value.resolved_type, ResolvedType::Text | ResolvedType::Null) {
3202                        return Err(PlannerError::type_mismatch(
3203                            "TEXT",
3204                            value.resolved_type.to_string(),
3205                            arg.span,
3206                        ));
3207                    }
3208                    typed.push(value);
3209                }
3210                let schema = vec![
3211                    ColumnMetadata::new("row_id", ResolvedType::BigInt),
3212                    ColumnMetadata::new("document", ResolvedType::Text),
3213                    ColumnMetadata::new("rank", ResolvedType::Double),
3214                    ColumnMetadata::new("headline", ResolvedType::Text),
3215                ];
3216                (typed, schema)
3217            }
3218        };
3219
3220        let relation_name = alias
3221            .map(str::to_string)
3222            .unwrap_or_else(|| function.default_relation_name().to_string());
3223        apply_alias_columns(&relation_name, columns, &mut schema, span)?;
3224
3225        Ok(PlannedRelation {
3226            plan: LogicalPlan::TableFunction {
3227                function,
3228                args: typed_args,
3229                schema: schema.clone(),
3230            },
3231            schema: schema.clone(),
3232            scope: vec![ScopedTable::new(
3233                TableMetadata::new(relation_name, schema),
3234                start_index,
3235            )],
3236        })
3237    }
3238
3239    fn combine_lateral_join_relation(
3240        &self,
3241        left: PlannedRelation,
3242        right: PlannedRelation,
3243        join_type: JoinType,
3244        condition: Option<TypedExpr>,
3245        using: Option<&[String]>,
3246        _span: crate::ast::Span,
3247    ) -> Result<PlannedRelation, PlannerError> {
3248        // USING/NATURAL merges the common columns the same way it does for an
3249        // ordinary join; only the execution strategy differs. The merged
3250        // equality already lives in `condition`, so the node itself does not
3251        // carry the column list.
3252        let (schema, scope) = combine_join_shape(&left, &right, using);
3253        Ok(PlannedRelation {
3254            plan: LogicalPlan::LateralJoin {
3255                left: Box::new(left.plan),
3256                right: Box::new(right.plan),
3257                join_type,
3258                condition,
3259                right_schema: right.schema,
3260            },
3261            schema,
3262            scope,
3263        })
3264    }
3265
3266    fn combine_join_relation(
3267        &self,
3268        left: PlannedRelation,
3269        right: PlannedRelation,
3270        join_type: JoinType,
3271        condition: Option<TypedExpr>,
3272        using: Option<Vec<String>>,
3273        _span: crate::ast::Span,
3274    ) -> Result<PlannedRelation, PlannerError> {
3275        let (schema, scope) = combine_join_shape(&left, &right, using.as_deref());
3276        Ok(PlannedRelation {
3277            plan: LogicalPlan::Join {
3278                left: Box::new(left.plan),
3279                right: Box::new(right.plan),
3280                join_type,
3281                condition,
3282                using,
3283            },
3284            schema,
3285            scope,
3286        })
3287    }
3288
3289    fn build_using_condition(
3290        &self,
3291        using: Option<&[String]>,
3292        left: &PlannedRelation,
3293        right: &PlannedRelation,
3294        span: crate::ast::Span,
3295    ) -> Result<Option<TypedExpr>, PlannerError> {
3296        let Some(columns) = using else {
3297            return Ok(None);
3298        };
3299        let mut condition = None;
3300        for column in columns {
3301            let left_col = find_scoped_column(&left.scope, column, span)?;
3302            let right_col = find_scoped_column(&right.scope, column, span)?;
3303            let left_expr = merged_scoped_column_expr(&left_col, column, span);
3304            let right_expr = merged_scoped_column_expr(&right_col, column, span);
3305            self.type_checker
3306                .check_comparison_op(&left_col.ty, &right_col.ty, span)?;
3307            let eq = TypedExpr::binary_op(
3308                left_expr,
3309                crate::ast::expr::BinaryOp::Eq,
3310                right_expr,
3311                ResolvedType::Boolean,
3312                span,
3313            );
3314            condition = Some(match condition {
3315                Some(prev) => TypedExpr::binary_op(
3316                    prev,
3317                    crate::ast::expr::BinaryOp::And,
3318                    eq,
3319                    ResolvedType::Boolean,
3320                    span,
3321                ),
3322                None => eq,
3323            });
3324        }
3325        Ok(condition)
3326    }
3327
3328    fn infer_expr_with_scope(
3329        &self,
3330        expr: &crate::ast::expr::Expr,
3331        scope: &[ScopedTable],
3332        ctes: &CtePlans,
3333    ) -> Result<TypedExpr, PlannerError> {
3334        self.type_checker
3335            .infer_type_with_scope(expr, scope, &|stmt, outer_scope| {
3336                let relation = match &stmt.kind {
3337                    StatementKind::Select(select) => {
3338                        self.plan_select_relation(select, outer_scope, ctes)?
3339                    }
3340                    StatementKind::Values(values) => {
3341                        self.plan_values_relation(values, outer_scope, ctes)?
3342                    }
3343                    _ => {
3344                        return Err(PlannerError::unsupported_feature(
3345                            "non-query subquery",
3346                            "a future version",
3347                            stmt.span(),
3348                        ));
3349                    }
3350                };
3351                Ok((relation.plan, relation.schema))
3352            })
3353    }
3354
3355    #[allow(dead_code)]
3356    fn build_projection(
3357        &self,
3358        items: &[SelectItem],
3359        table: &TableMetadata,
3360    ) -> Result<Projection, PlannerError> {
3361        // Check for wildcard - if present, expand it
3362        if items.len() == 1 && matches!(&items[0], SelectItem::Wildcard { .. }) {
3363            let columns = self.name_resolver.expand_wildcard(table);
3364            return Ok(Projection::All(columns));
3365        }
3366
3367        // Process each select item
3368        let mut projected_columns = Vec::new();
3369        for item in items {
3370            match item {
3371                SelectItem::Wildcard { span } => {
3372                    // Wildcard mixed with other items - expand inline
3373                    for col in &table.columns {
3374                        let column_index = table.get_column_index(&col.name).unwrap();
3375                        let typed_expr = TypedExpr::column_ref(
3376                            table.name.clone(),
3377                            col.name.clone(),
3378                            column_index,
3379                            col.data_type.clone(),
3380                            *span,
3381                        );
3382                        projected_columns.push(ProjectedColumn::new(typed_expr));
3383                    }
3384                }
3385                SelectItem::QualifiedWildcard {
3386                    table: qualifier,
3387                    span,
3388                } => {
3389                    if qualifier != &table.name {
3390                        return Err(PlannerError::invalid_expression(format!(
3391                            "table '{qualifier}' is not available for wildcard projection"
3392                        )));
3393                    }
3394                    for col in &table.columns {
3395                        let column_index = table.get_column_index(&col.name).unwrap();
3396                        projected_columns.push(ProjectedColumn::new(TypedExpr::column_ref(
3397                            table.name.clone(),
3398                            col.name.clone(),
3399                            column_index,
3400                            col.data_type.clone(),
3401                            *span,
3402                        )));
3403                    }
3404                }
3405                SelectItem::Expr { expr, alias, .. } => {
3406                    let typed_expr = self.type_checker.infer_type(expr, table)?;
3407                    let projected = if let Some(alias) = alias {
3408                        ProjectedColumn::with_alias(typed_expr, alias.clone())
3409                    } else {
3410                        ProjectedColumn::new(typed_expr)
3411                    };
3412                    projected_columns.push(projected);
3413                }
3414            }
3415        }
3416
3417        Ok(Projection::Columns(projected_columns))
3418    }
3419
3420    fn build_projection_with_scope(
3421        &self,
3422        items: &[SelectItem],
3423        schema: &[ColumnMetadata],
3424        scope: &[ScopedTable],
3425        ctes: &CtePlans,
3426    ) -> Result<Projection, PlannerError> {
3427        if items.len() == 1 && matches!(&items[0], SelectItem::Wildcard { .. }) {
3428            return Ok(Projection::All(visible_wildcard_columns(schema, scope)));
3429        }
3430
3431        let mut projected_columns = Vec::new();
3432        for item in items {
3433            match item {
3434                SelectItem::Wildcard { span } => {
3435                    for scoped in scope {
3436                        for (local_idx, col) in scoped.table.columns.iter().enumerate() {
3437                            projected_columns.push(ProjectedColumn::new(TypedExpr::column_ref(
3438                                scoped.table.name.clone(),
3439                                col.name.clone(),
3440                                scoped.start_index + local_idx,
3441                                col.data_type.clone(),
3442                                *span,
3443                            )));
3444                        }
3445                    }
3446                }
3447                SelectItem::QualifiedWildcard { table, span } => {
3448                    let scoped = scope
3449                        .iter()
3450                        .filter(|scoped| scoped.table.name == *table)
3451                        .collect::<Vec<_>>();
3452                    match scoped.as_slice() {
3453                        [] => {
3454                            return Err(PlannerError::invalid_expression(format!(
3455                                "table '{table}' is not available for wildcard projection"
3456                            )));
3457                        }
3458                        [scoped] => {
3459                            for (local_idx, col) in scoped.table.columns.iter().enumerate() {
3460                                projected_columns.push(ProjectedColumn::new(
3461                                    TypedExpr::column_ref(
3462                                        scoped.table.name.clone(),
3463                                        col.name.clone(),
3464                                        scoped.start_index + local_idx,
3465                                        col.data_type.clone(),
3466                                        *span,
3467                                    ),
3468                                ));
3469                            }
3470                        }
3471                        _ => {
3472                            return Err(PlannerError::ambiguous_column(
3473                                table,
3474                                scoped
3475                                    .iter()
3476                                    .map(|scoped| scoped.table.name.clone())
3477                                    .collect(),
3478                                *span,
3479                            ));
3480                        }
3481                    }
3482                }
3483                SelectItem::Expr { expr, alias, .. } => {
3484                    let typed_expr = self.infer_expr_with_scope(expr, scope, ctes)?;
3485                    let projected = if let Some(alias) = alias {
3486                        ProjectedColumn::with_alias(typed_expr, alias.clone())
3487                    } else {
3488                        ProjectedColumn::new(typed_expr)
3489                    };
3490                    projected_columns.push(projected);
3491                }
3492            }
3493        }
3494
3495        Ok(Projection::Columns(projected_columns))
3496    }
3497
3498    /// Build sort expressions from ORDER BY clause.
3499    #[allow(dead_code)]
3500    fn build_sort_exprs(
3501        &self,
3502        order_by: &[OrderByExpr],
3503        table: &TableMetadata,
3504    ) -> Result<Vec<SortExpr>, PlannerError> {
3505        let mut sort_exprs = Vec::new();
3506
3507        for order_expr in order_by {
3508            let typed_expr = self.type_checker.infer_type(&order_expr.expr, table)?;
3509
3510            // Determine sort direction (default: ASC)
3511            let asc = order_expr.asc.unwrap_or(true);
3512
3513            // Determine NULLS ordering (default: NULLS LAST for both ASC and DESC)
3514            let nulls_first = order_expr.nulls_first.unwrap_or(false);
3515
3516            sort_exprs.push(SortExpr::new(typed_expr, asc, nulls_first));
3517        }
3518
3519        Ok(sort_exprs)
3520    }
3521
3522    fn build_sort_exprs_with_scope(
3523        &self,
3524        order_by: &[OrderByExpr],
3525        scope: &[ScopedTable],
3526        projection_aliases: &HashMap<String, crate::ast::expr::Expr>,
3527        ctes: &CtePlans,
3528    ) -> Result<Vec<SortExpr>, PlannerError> {
3529        let mut sort_exprs = Vec::new();
3530        for order_expr in order_by {
3531            let sort_source = substitute_projection_aliases(&order_expr.expr, projection_aliases);
3532            let typed_expr = self.infer_expr_with_scope(&sort_source, scope, ctes)?;
3533            let asc = order_expr.asc.unwrap_or(true);
3534            let nulls_first = order_expr.nulls_first.unwrap_or(false);
3535            sort_exprs.push(SortExpr::new(typed_expr, asc, nulls_first));
3536        }
3537        Ok(sort_exprs)
3538    }
3539
3540    fn select_contains_aggregate(&self, stmt: &Select) -> bool {
3541        stmt.projection.iter().any(|item| match item {
3542            SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => false,
3543            SelectItem::Expr { expr, .. } => expr_contains_aggregate(expr),
3544        }) || stmt
3545            .group_by
3546            .as_ref()
3547            .map(|items| {
3548                items
3549                    .iter()
3550                    .flat_map(GroupByItem::exprs)
3551                    .any(expr_contains_aggregate)
3552            })
3553            .unwrap_or(false)
3554            || stmt
3555                .having
3556                .as_ref()
3557                .map(expr_contains_aggregate)
3558                .unwrap_or(false)
3559            || stmt
3560                .qualify
3561                .as_ref()
3562                .map(expr_contains_aggregate)
3563                .unwrap_or(false)
3564            || stmt
3565                .order_by
3566                .iter()
3567                .any(|order| expr_contains_aggregate(&order.expr))
3568    }
3569
3570    #[allow(dead_code)]
3571    fn build_group_keys(
3572        &self,
3573        stmt: &Select,
3574        table: &TableMetadata,
3575    ) -> Result<Vec<TypedExpr>, PlannerError> {
3576        let mut keys = Vec::new();
3577        if let Some(items) = &stmt.group_by {
3578            for expr in items.iter().flat_map(GroupByItem::exprs) {
3579                let typed = self.type_checker.infer_type(expr, table)?;
3580                if typed_expr_contains_aggregate(&typed) {
3581                    return Err(PlannerError::invalid_expression(
3582                        "GROUP BY cannot contain aggregate functions".to_string(),
3583                    ));
3584                }
3585                if !matches!(typed.kind, TypedExprKind::ColumnRef { .. }) {
3586                    return Err(PlannerError::invalid_expression(
3587                        "GROUP BY expressions must be column references".to_string(),
3588                    ));
3589                }
3590                keys.push(typed);
3591            }
3592        }
3593        Ok(keys)
3594    }
3595
3596    /// Type one grouping key with the shared GROUP BY constraints.
3597    fn type_group_key_with_scope(
3598        &self,
3599        expr: &Expr,
3600        scope: &[ScopedTable],
3601        ctes: &CtePlans,
3602    ) -> Result<TypedExpr, PlannerError> {
3603        let typed = self.infer_expr_with_scope(expr, scope, ctes)?;
3604        if typed_expr_contains_aggregate(&typed) {
3605            return Err(PlannerError::invalid_expression(
3606                "GROUP BY cannot contain aggregate functions".to_string(),
3607            ));
3608        }
3609        if !matches!(typed.kind, TypedExprKind::ColumnRef { .. }) {
3610            return Err(PlannerError::invalid_expression(
3611                "GROUP BY expressions must be column references".to_string(),
3612            ));
3613        }
3614        Ok(typed)
3615    }
3616
3617    /// Expand GROUP BY items into a flat key list plus grouping-set masks.
3618    ///
3619    /// Without ROLLUP/CUBE/GROUPING SETS the result is the pre-existing key
3620    /// list with `grouping_sets: None`, keeping the legacy single-set plan
3621    /// byte-for-byte identical (issue #149, D12). With modifiers, keys are
3622    /// unioned by expression identity in first-appearance order and every
3623    /// item contributes a set list that is combined by cross product (D2).
3624    fn expand_group_by_items(
3625        &self,
3626        stmt: &Select,
3627        scope: &[ScopedTable],
3628        ctes: &CtePlans,
3629    ) -> Result<ExpandedGroupBy, PlannerError> {
3630        let Some(items) = &stmt.group_by else {
3631            return Ok(ExpandedGroupBy {
3632                group_keys: Vec::new(),
3633                grouping_sets: None,
3634            });
3635        };
3636
3637        if items
3638            .iter()
3639            .all(|item| matches!(item, GroupByItem::Expr { .. }))
3640        {
3641            // Legacy path: no dedup, no masks (D12).
3642            let mut keys = Vec::new();
3643            for item in items {
3644                if let GroupByItem::Expr { expr } = item {
3645                    keys.push(self.type_group_key_with_scope(expr, scope, ctes)?);
3646                }
3647            }
3648            return Ok(ExpandedGroupBy {
3649                group_keys: keys,
3650                grouping_sets: None,
3651            });
3652        }
3653
3654        let mut keys: Vec<TypedExpr> = Vec::new();
3655        let mut key_index: HashMap<String, usize> = HashMap::new();
3656        let mut add_key = |planner: &Self, expr: &Expr| -> Result<usize, PlannerError> {
3657            let typed = planner.type_group_key_with_scope(expr, scope, ctes)?;
3658            let signature = expr_key(&typed);
3659            if let Some(&index) = key_index.get(&signature) {
3660                return Ok(index);
3661            }
3662            let index = keys.len();
3663            keys.push(typed);
3664            key_index.insert(signature, index);
3665            Ok(index)
3666        };
3667
3668        // Cross product of per-item set lists (D2); each set is a list of
3669        // union-key indexes.
3670        let mut sets: Vec<Vec<usize>> = vec![Vec::new()];
3671        for item in items {
3672            let item_sets: Vec<Vec<usize>> = match item {
3673                GroupByItem::Expr { expr } => vec![vec![add_key(self, expr)?]],
3674                GroupByItem::Rollup { exprs } => {
3675                    if exprs.is_empty() {
3676                        return Err(PlannerError::invalid_expression(
3677                            "ROLLUP requires at least one expression".to_string(),
3678                        ));
3679                    }
3680                    let indexes = exprs
3681                        .iter()
3682                        .map(|expr| add_key(self, expr))
3683                        .collect::<Result<Vec<_>, _>>()?;
3684                    (0..=indexes.len())
3685                        .rev()
3686                        .map(|len| indexes[..len].to_vec())
3687                        .collect()
3688                }
3689                GroupByItem::Cube { exprs } => {
3690                    if exprs.is_empty() {
3691                        return Err(PlannerError::invalid_expression(
3692                            "CUBE requires at least one expression".to_string(),
3693                        ));
3694                    }
3695                    if exprs.len() > MAX_CUBE_COLUMNS {
3696                        return Err(PlannerError::invalid_expression(format!(
3697                            "too many grouping sets (max {MAX_GROUPING_SETS})"
3698                        )));
3699                    }
3700                    let indexes = exprs
3701                        .iter()
3702                        .map(|expr| add_key(self, expr))
3703                        .collect::<Result<Vec<_>, _>>()?;
3704                    let n = indexes.len();
3705                    (0..(1usize << n))
3706                        .rev()
3707                        .map(|included| {
3708                            indexes
3709                                .iter()
3710                                .enumerate()
3711                                .filter(|(position, _)| (included >> (n - 1 - position)) & 1 == 1)
3712                                .map(|(_, &index)| index)
3713                                .collect()
3714                        })
3715                        .collect()
3716                }
3717                GroupByItem::GroupingSets { sets: listed } => {
3718                    if listed.is_empty() {
3719                        return Err(PlannerError::invalid_expression(
3720                            "GROUPING SETS requires at least one grouping set".to_string(),
3721                        ));
3722                    }
3723                    listed
3724                        .iter()
3725                        .map(|set| {
3726                            set.iter()
3727                                .map(|expr| add_key(self, expr))
3728                                .collect::<Result<Vec<_>, _>>()
3729                        })
3730                        .collect::<Result<Vec<_>, _>>()?
3731                }
3732            };
3733
3734            let mut combined = Vec::with_capacity(sets.len().saturating_mul(item_sets.len()));
3735            for base in &sets {
3736                for item_set in &item_sets {
3737                    if combined.len() >= MAX_GROUPING_SETS {
3738                        return Err(PlannerError::invalid_expression(format!(
3739                            "too many grouping sets (max {MAX_GROUPING_SETS})"
3740                        )));
3741                    }
3742                    let mut set = base.clone();
3743                    set.extend(item_set.iter().copied());
3744                    combined.push(set);
3745                }
3746            }
3747            sets = combined;
3748        }
3749
3750        if keys.len() > MAX_GROUPING_KEYS {
3751            return Err(PlannerError::invalid_expression(format!(
3752                "too many grouping columns (max {MAX_GROUPING_KEYS})"
3753            )));
3754        }
3755
3756        let key_count = keys.len();
3757        let full_mask = grouping_full_mask(key_count);
3758        let masks = sets
3759            .iter()
3760            .map(|set| {
3761                let mut mask = full_mask;
3762                for &index in set {
3763                    mask &= !(1u64 << (key_count - 1 - index));
3764                }
3765                mask
3766            })
3767            .collect();
3768
3769        Ok(ExpandedGroupBy {
3770            group_keys: keys,
3771            grouping_sets: Some(masks),
3772        })
3773    }
3774
3775    #[allow(dead_code)]
3776    fn build_projected_columns_for_aggregate(
3777        &self,
3778        items: &[SelectItem],
3779        table: &TableMetadata,
3780    ) -> Result<Vec<ProjectedColumn>, PlannerError> {
3781        let mut projected = Vec::new();
3782        for item in items {
3783            match item {
3784                SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => {
3785                    return Err(PlannerError::invalid_expression(
3786                        "wildcard projection not supported with GROUP BY/aggregate".to_string(),
3787                    ));
3788                }
3789                SelectItem::Expr { expr, alias, .. } => {
3790                    let typed = self.type_checker.infer_type(expr, table)?;
3791                    projected.push(ProjectedColumn {
3792                        expr: typed,
3793                        alias: alias.clone(),
3794                    });
3795                }
3796            }
3797        }
3798        Ok(projected)
3799    }
3800
3801    fn build_projected_columns_for_aggregate_with_scope(
3802        &self,
3803        items: &[SelectItem],
3804        scope: &[ScopedTable],
3805        ctes: &CtePlans,
3806    ) -> Result<Vec<ProjectedColumn>, PlannerError> {
3807        let mut projected = Vec::new();
3808        for item in items {
3809            match item {
3810                SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => {
3811                    return Err(PlannerError::invalid_expression(
3812                        "wildcard projection not supported with GROUP BY/aggregate".to_string(),
3813                    ));
3814                }
3815                SelectItem::Expr { expr, alias, .. } => {
3816                    let typed = self.infer_expr_with_scope(expr, scope, ctes)?;
3817                    projected.push(ProjectedColumn {
3818                        expr: typed,
3819                        alias: alias.clone(),
3820                    });
3821                }
3822            }
3823        }
3824        Ok(projected)
3825    }
3826
3827    #[allow(dead_code)]
3828    fn build_projected_columns_for_distinct(
3829        &self,
3830        items: &[SelectItem],
3831        table: &TableMetadata,
3832    ) -> Result<Vec<ProjectedColumn>, PlannerError> {
3833        let projection = self.build_projection(items, table)?;
3834        match projection {
3835            Projection::All(columns) => {
3836                let mut projected = Vec::with_capacity(columns.len());
3837                for column in columns {
3838                    let column_index = table.get_column_index(&column).ok_or_else(|| {
3839                        PlannerError::invalid_expression(format!(
3840                            "column '{column}' not found for DISTINCT projection"
3841                        ))
3842                    })?;
3843                    let column_meta = table.get_column(&column).ok_or_else(|| {
3844                        PlannerError::invalid_expression(format!(
3845                            "column '{column}' not found for DISTINCT projection"
3846                        ))
3847                    })?;
3848                    let typed_expr = TypedExpr::column_ref(
3849                        table.name.clone(),
3850                        column.clone(),
3851                        column_index,
3852                        column_meta.data_type.clone(),
3853                        crate::ast::Span::default(),
3854                    );
3855                    projected.push(ProjectedColumn::new(typed_expr));
3856                }
3857                Ok(projected)
3858            }
3859            Projection::Columns(columns) => Ok(columns),
3860        }
3861    }
3862
3863    fn build_projected_columns_for_distinct_with_scope(
3864        &self,
3865        items: &[SelectItem],
3866        schema: &[ColumnMetadata],
3867        scope: &[ScopedTable],
3868        ctes: &CtePlans,
3869    ) -> Result<Vec<ProjectedColumn>, PlannerError> {
3870        let projection = self.build_projection_with_scope(items, schema, scope, ctes)?;
3871        match projection {
3872            Projection::All(columns) => {
3873                let mut projected = Vec::with_capacity(columns.len());
3874                for (idx, column) in columns.into_iter().enumerate() {
3875                    let column_meta = schema.get(idx).ok_or_else(|| {
3876                        PlannerError::invalid_expression(format!(
3877                            "column '{column}' not found for DISTINCT projection"
3878                        ))
3879                    })?;
3880                    projected.push(ProjectedColumn::new(TypedExpr::column_ref(
3881                        LITERAL_TABLE.to_string(),
3882                        column,
3883                        idx,
3884                        column_meta.data_type.clone(),
3885                        crate::ast::Span::default(),
3886                    )));
3887                }
3888                Ok(projected)
3889            }
3890            Projection::Columns(columns) => Ok(columns),
3891        }
3892    }
3893
3894    fn collect_aggregates_from_typed_expr(
3895        &self,
3896        expr: &TypedExpr,
3897        aggregates: &mut Vec<AggregateExpr>,
3898        aggregate_map: &mut HashMap<AggregateSignature, usize>,
3899    ) -> Result<(), PlannerError> {
3900        match &expr.kind {
3901            TypedExprKind::FunctionCall {
3902                name,
3903                args,
3904                distinct,
3905                star,
3906                filter,
3907                order_by,
3908                over: None,
3909            } if is_aggregate_function(name) => {
3910                if args.iter().any(typed_expr_contains_window) {
3911                    return Err(PlannerError::invalid_expression(
3912                        "aggregate functions cannot contain window functions".to_string(),
3913                    ));
3914                }
3915                for arg in args {
3916                    if typed_expr_contains_aggregate(arg) {
3917                        return Err(PlannerError::invalid_expression(
3918                            "nested aggregate functions are not supported".to_string(),
3919                        ));
3920                    }
3921                }
3922                // The type checker rejects aggregates and window functions in
3923                // FILTER / aggregate ORDER BY; keep a defensive re-check so a
3924                // future construction path cannot smuggle them through.
3925                if let Some(filter) = filter {
3926                    if typed_expr_contains_aggregate(filter) {
3927                        return Err(PlannerError::invalid_expression(
3928                            "aggregate functions are not allowed in FILTER".to_string(),
3929                        ));
3930                    }
3931                    if typed_expr_contains_window(filter) {
3932                        return Err(PlannerError::invalid_expression(
3933                            "window functions are not allowed in FILTER".to_string(),
3934                        ));
3935                    }
3936                }
3937                for sort in order_by {
3938                    if typed_expr_contains_aggregate(&sort.expr) {
3939                        return Err(PlannerError::invalid_expression(
3940                            "aggregate functions are not allowed in aggregate ORDER BY".to_string(),
3941                        ));
3942                    }
3943                    if typed_expr_contains_window(&sort.expr) {
3944                        return Err(PlannerError::invalid_expression(
3945                            "window functions are not allowed in aggregate ORDER BY".to_string(),
3946                        ));
3947                    }
3948                }
3949                let (agg, signature) = self.build_aggregate_expr_from_typed(
3950                    expr,
3951                    name,
3952                    args,
3953                    *distinct,
3954                    *star,
3955                    filter.as_deref(),
3956                    order_by,
3957                )?;
3958                aggregate_map.entry(signature).or_insert_with(|| {
3959                    aggregates.push(agg);
3960                    aggregates.len() - 1
3961                });
3962                Ok(())
3963            }
3964            TypedExprKind::BinaryOp { left, right, .. } => {
3965                self.collect_aggregates_from_typed_expr(left, aggregates, aggregate_map)?;
3966                self.collect_aggregates_from_typed_expr(right, aggregates, aggregate_map)?;
3967                Ok(())
3968            }
3969            TypedExprKind::UnaryOp { operand, .. } => {
3970                self.collect_aggregates_from_typed_expr(operand, aggregates, aggregate_map)
3971            }
3972            TypedExprKind::Cast { expr, .. } | TypedExprKind::TryCast { expr, .. } => {
3973                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)
3974            }
3975            TypedExprKind::Case {
3976                operand,
3977                branches,
3978                else_expr,
3979            } => {
3980                if let Some(operand) = operand {
3981                    self.collect_aggregates_from_typed_expr(operand, aggregates, aggregate_map)?;
3982                }
3983                for branch in branches {
3984                    self.collect_aggregates_from_typed_expr(
3985                        &branch.when,
3986                        aggregates,
3987                        aggregate_map,
3988                    )?;
3989                    self.collect_aggregates_from_typed_expr(
3990                        &branch.then,
3991                        aggregates,
3992                        aggregate_map,
3993                    )?;
3994                }
3995                if let Some(else_expr) = else_expr {
3996                    self.collect_aggregates_from_typed_expr(else_expr, aggregates, aggregate_map)?;
3997                }
3998                Ok(())
3999            }
4000            TypedExprKind::FunctionCall { args, over, .. } => {
4001                for arg in args {
4002                    self.collect_aggregates_from_typed_expr(arg, aggregates, aggregate_map)?;
4003                }
4004                if let Some(window) = over {
4005                    for expr in &window.partition_by {
4006                        self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)?;
4007                    }
4008                    for sort in &window.order_by {
4009                        self.collect_aggregates_from_typed_expr(
4010                            &sort.expr,
4011                            aggregates,
4012                            aggregate_map,
4013                        )?;
4014                    }
4015                }
4016                Ok(())
4017            }
4018            TypedExprKind::Between {
4019                expr, low, high, ..
4020            } => {
4021                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)?;
4022                self.collect_aggregates_from_typed_expr(low, aggregates, aggregate_map)?;
4023                self.collect_aggregates_from_typed_expr(high, aggregates, aggregate_map)?;
4024                Ok(())
4025            }
4026            TypedExprKind::Like {
4027                expr,
4028                pattern,
4029                escape,
4030                ..
4031            } => {
4032                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)?;
4033                self.collect_aggregates_from_typed_expr(pattern, aggregates, aggregate_map)?;
4034                if let Some(esc) = escape {
4035                    self.collect_aggregates_from_typed_expr(esc, aggregates, aggregate_map)?;
4036                }
4037                Ok(())
4038            }
4039            TypedExprKind::InList { expr, list, .. } => {
4040                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)?;
4041                for item in list {
4042                    self.collect_aggregates_from_typed_expr(item, aggregates, aggregate_map)?;
4043                }
4044                Ok(())
4045            }
4046            TypedExprKind::IsNull { expr, .. } => {
4047                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)
4048            }
4049            _ => Ok(()),
4050        }
4051    }
4052
4053    fn collect_windows_from_typed_expr(
4054        &self,
4055        expr: &TypedExpr,
4056        windows: &mut Vec<WindowExpr>,
4057        window_map: &mut HashMap<String, usize>,
4058    ) -> Result<(), PlannerError> {
4059        match &expr.kind {
4060            TypedExprKind::FunctionCall {
4061                name,
4062                args,
4063                distinct,
4064                star,
4065                filter,
4066                order_by,
4067                over: Some(over),
4068            } => {
4069                if filter.is_some() || !order_by.is_empty() {
4070                    // The type checker rejects these combinations (D2); this
4071                    // guard keeps the window planner from silently ignoring a
4072                    // filter if a future path forgets that validation.
4073                    return Err(PlannerError::invalid_expression(
4074                        "FILTER and aggregate ORDER BY cannot be combined with OVER".to_string(),
4075                    ));
4076                }
4077                if args.iter().any(typed_expr_contains_window)
4078                    || over.partition_by.iter().any(typed_expr_contains_window)
4079                    || over
4080                        .order_by
4081                        .iter()
4082                        .any(|sort| typed_expr_contains_window(&sort.expr))
4083                {
4084                    return Err(PlannerError::invalid_expression(
4085                        "nested window functions are not supported".to_string(),
4086                    ));
4087                }
4088
4089                let key = expr_key(expr);
4090                if window_map.contains_key(&key) {
4091                    return Ok(());
4092                }
4093                let function = match name.to_ascii_lowercase().as_str() {
4094                    "row_number" => WindowFunction::RowNumber,
4095                    "rank" => WindowFunction::Rank,
4096                    "dense_rank" => WindowFunction::DenseRank,
4097                    "percent_rank" => WindowFunction::PercentRank,
4098                    "cume_dist" => WindowFunction::CumeDist,
4099                    "ntile" => WindowFunction::Ntile(args[0].clone()),
4100                    "first_value" => {
4101                        WindowFunction::Value(ValueWindowFunction::FirstValue(args[0].clone()))
4102                    }
4103                    "last_value" => {
4104                        WindowFunction::Value(ValueWindowFunction::LastValue(args[0].clone()))
4105                    }
4106                    "nth_value" => WindowFunction::Value(ValueWindowFunction::NthValue {
4107                        value: args[0].clone(),
4108                        nth: args[1].clone(),
4109                    }),
4110                    name if is_aggregate_function(name) => {
4111                        let (aggregate, _) = self.build_aggregate_expr_from_typed(
4112                            expr, name, args, *distinct, *star, None, order_by,
4113                        )?;
4114                        WindowFunction::Aggregate(aggregate)
4115                    }
4116                    "lag" | "lead" => {
4117                        let positional = build_offset_window_function(name, args)?;
4118                        if name.eq_ignore_ascii_case("lag") {
4119                            WindowFunction::Lag(positional)
4120                        } else {
4121                            WindowFunction::Lead(positional)
4122                        }
4123                    }
4124                    _ => {
4125                        return Err(PlannerError::unsupported_feature(
4126                            format!("function '{}' with OVER", name),
4127                            "future",
4128                            expr.span,
4129                        ));
4130                    }
4131                };
4132                let index = windows.len();
4133                windows.push(WindowExpr {
4134                    function,
4135                    partition_by: over.partition_by.clone(),
4136                    order_by: over.order_by.clone(),
4137                    frame: over.frame.clone(),
4138                    result_type: expr.resolved_type.clone(),
4139                });
4140                window_map.insert(key, index);
4141                Ok(())
4142            }
4143            TypedExprKind::FunctionCall { args, .. } => {
4144                for arg in args {
4145                    self.collect_windows_from_typed_expr(arg, windows, window_map)?;
4146                }
4147                Ok(())
4148            }
4149            TypedExprKind::BinaryOp { left, right, .. } => {
4150                self.collect_windows_from_typed_expr(left, windows, window_map)?;
4151                self.collect_windows_from_typed_expr(right, windows, window_map)
4152            }
4153            TypedExprKind::UnaryOp { operand, .. } => {
4154                self.collect_windows_from_typed_expr(operand, windows, window_map)
4155            }
4156            TypedExprKind::Cast { expr, .. }
4157            | TypedExprKind::TryCast { expr, .. }
4158            | TypedExprKind::IsNull { expr, .. } => {
4159                self.collect_windows_from_typed_expr(expr, windows, window_map)
4160            }
4161            TypedExprKind::Between {
4162                expr, low, high, ..
4163            } => {
4164                self.collect_windows_from_typed_expr(expr, windows, window_map)?;
4165                self.collect_windows_from_typed_expr(low, windows, window_map)?;
4166                self.collect_windows_from_typed_expr(high, windows, window_map)
4167            }
4168            TypedExprKind::Like {
4169                expr,
4170                pattern,
4171                escape,
4172                ..
4173            } => {
4174                self.collect_windows_from_typed_expr(expr, windows, window_map)?;
4175                self.collect_windows_from_typed_expr(pattern, windows, window_map)?;
4176                if let Some(escape) = escape {
4177                    self.collect_windows_from_typed_expr(escape, windows, window_map)?;
4178                }
4179                Ok(())
4180            }
4181            TypedExprKind::InList { expr, list, .. } => {
4182                self.collect_windows_from_typed_expr(expr, windows, window_map)?;
4183                for item in list {
4184                    self.collect_windows_from_typed_expr(item, windows, window_map)?;
4185                }
4186                Ok(())
4187            }
4188            _ => Ok(()),
4189        }
4190    }
4191
4192    #[allow(clippy::too_many_arguments)]
4193    fn build_aggregate_expr_from_typed(
4194        &self,
4195        expr: &TypedExpr,
4196        name: &str,
4197        args: &[TypedExpr],
4198        distinct: bool,
4199        star: bool,
4200        filter: Option<&TypedExpr>,
4201        order_by: &[SortExpr],
4202    ) -> Result<(AggregateExpr, AggregateSignature), PlannerError> {
4203        let lower = name.to_lowercase();
4204        let filter_owned = filter.cloned();
4205        // D3: order-insensitive aggregates validate their ORDER BY (names and
4206        // types) and then discard it — the result is order-independent, so
4207        // the sort cost is avoided and the signature matches the unordered
4208        // spelling. Order-sensitive aggregates keep the ordering.
4209        let retained_order_by: Vec<SortExpr> = if is_order_sensitive_aggregate(&lower) {
4210            order_by.to_vec()
4211        } else {
4212            Vec::new()
4213        };
4214        match lower.as_str() {
4215            "count" => {
4216                if star {
4217                    let mut agg = AggregateExpr::count_star();
4218                    agg.filter = filter_owned;
4219                    let signature = aggregate_signature(
4220                        name,
4221                        distinct,
4222                        star,
4223                        None,
4224                        None,
4225                        expr,
4226                        filter,
4227                        &retained_order_by,
4228                    );
4229                    return Ok((agg, signature));
4230                }
4231                if args.len() != 1 {
4232                    return Err(PlannerError::type_mismatch(
4233                        "1 argument",
4234                        format!("{} arguments", args.len()),
4235                        expr.span,
4236                    ));
4237                }
4238                let agg = AggregateExpr {
4239                    function: AggregateFunction::Count,
4240                    arg: Some(args[0].clone()),
4241                    extra_args: Vec::new(),
4242                    distinct,
4243                    result_type: ResolvedType::BigInt,
4244                    filter: filter_owned,
4245                    order_by: retained_order_by.clone(),
4246                };
4247                let signature = aggregate_signature(
4248                    name,
4249                    distinct,
4250                    star,
4251                    Some(&args[0]),
4252                    None,
4253                    expr,
4254                    filter,
4255                    &retained_order_by,
4256                );
4257                Ok((agg, signature))
4258            }
4259            "sum" => {
4260                let arg = self.require_single_aggregate_arg(args, expr.span)?;
4261                let agg = AggregateExpr {
4262                    function: AggregateFunction::Sum,
4263                    arg: Some(arg.clone()),
4264                    extra_args: Vec::new(),
4265                    distinct,
4266                    result_type: crate::planner::aggregate_expr::sum_result_type(
4267                        &arg.resolved_type,
4268                    ),
4269                    filter: filter_owned,
4270                    order_by: retained_order_by.clone(),
4271                };
4272                let signature = aggregate_signature(
4273                    name,
4274                    distinct,
4275                    star,
4276                    Some(arg),
4277                    None,
4278                    expr,
4279                    filter,
4280                    &retained_order_by,
4281                );
4282                Ok((agg, signature))
4283            }
4284            "total" => {
4285                let arg = self.require_single_aggregate_arg(args, expr.span)?;
4286                let agg = AggregateExpr {
4287                    function: AggregateFunction::Total,
4288                    arg: Some(arg.clone()),
4289                    extra_args: Vec::new(),
4290                    distinct: false,
4291                    result_type: ResolvedType::Double,
4292                    filter: filter_owned,
4293                    order_by: retained_order_by.clone(),
4294                };
4295                let signature = aggregate_signature(
4296                    name,
4297                    false,
4298                    star,
4299                    Some(arg),
4300                    None,
4301                    expr,
4302                    filter,
4303                    &retained_order_by,
4304                );
4305                Ok((agg, signature))
4306            }
4307            "avg" => {
4308                let arg = self.require_single_aggregate_arg(args, expr.span)?;
4309                let agg = AggregateExpr {
4310                    function: AggregateFunction::Avg,
4311                    arg: Some(arg.clone()),
4312                    extra_args: Vec::new(),
4313                    distinct,
4314                    result_type: crate::planner::aggregate_expr::avg_result_type(
4315                        &arg.resolved_type,
4316                    ),
4317                    filter: filter_owned,
4318                    order_by: retained_order_by.clone(),
4319                };
4320                let signature = aggregate_signature(
4321                    name,
4322                    distinct,
4323                    star,
4324                    Some(arg),
4325                    None,
4326                    expr,
4327                    filter,
4328                    &retained_order_by,
4329                );
4330                Ok((agg, signature))
4331            }
4332            "min" => {
4333                let arg = self.require_single_aggregate_arg(args, expr.span)?;
4334                let agg = AggregateExpr {
4335                    function: AggregateFunction::Min,
4336                    arg: Some(arg.clone()),
4337                    extra_args: Vec::new(),
4338                    distinct,
4339                    result_type: arg.resolved_type.clone(),
4340                    filter: filter_owned,
4341                    order_by: retained_order_by.clone(),
4342                };
4343                let signature = aggregate_signature(
4344                    name,
4345                    distinct,
4346                    star,
4347                    Some(arg),
4348                    None,
4349                    expr,
4350                    filter,
4351                    &retained_order_by,
4352                );
4353                Ok((agg, signature))
4354            }
4355            "max" => {
4356                let arg = self.require_single_aggregate_arg(args, expr.span)?;
4357                let agg = AggregateExpr {
4358                    function: AggregateFunction::Max,
4359                    arg: Some(arg.clone()),
4360                    extra_args: Vec::new(),
4361                    distinct,
4362                    result_type: arg.resolved_type.clone(),
4363                    filter: filter_owned,
4364                    order_by: retained_order_by.clone(),
4365                };
4366                let signature = aggregate_signature(
4367                    name,
4368                    distinct,
4369                    star,
4370                    Some(arg),
4371                    None,
4372                    expr,
4373                    filter,
4374                    &retained_order_by,
4375                );
4376                Ok((agg, signature))
4377            }
4378            "group_concat" => {
4379                if args.is_empty() || args.len() > 2 {
4380                    return Err(PlannerError::type_mismatch(
4381                        "1 or 2 arguments",
4382                        format!("{} arguments", args.len()),
4383                        expr.span,
4384                    ));
4385                }
4386                let arg = &args[0];
4387                let mut separator = None;
4388                if args.len() == 2 {
4389                    if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
4390                        separator = Some(value.clone());
4391                    } else {
4392                        return Err(PlannerError::invalid_expression(
4393                            "GROUP_CONCAT separator must be a string literal".to_string(),
4394                        ));
4395                    }
4396                }
4397                let agg = AggregateExpr {
4398                    function: AggregateFunction::GroupConcat { separator },
4399                    arg: Some(arg.clone()),
4400                    extra_args: Vec::new(),
4401                    distinct,
4402                    result_type: ResolvedType::Text,
4403                    filter: filter_owned,
4404                    order_by: retained_order_by.clone(),
4405                };
4406                let signature = aggregate_signature(
4407                    name,
4408                    distinct,
4409                    star,
4410                    Some(arg),
4411                    match &agg.function {
4412                        AggregateFunction::GroupConcat { separator } => separator.as_ref(),
4413                        _ => None,
4414                    },
4415                    expr,
4416                    filter,
4417                    &retained_order_by,
4418                );
4419                Ok((agg, signature))
4420            }
4421            "string_agg" => {
4422                if args.len() != 2 {
4423                    return Err(PlannerError::type_mismatch(
4424                        "2 arguments",
4425                        format!("{} arguments", args.len()),
4426                        expr.span,
4427                    ));
4428                }
4429                let arg = &args[0];
4430                let separator =
4431                    if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
4432                        Some(value.clone())
4433                    } else {
4434                        return Err(PlannerError::invalid_expression(
4435                            "STRING_AGG separator must be a string literal".to_string(),
4436                        ));
4437                    };
4438                let agg = AggregateExpr {
4439                    function: AggregateFunction::StringAgg { separator },
4440                    arg: Some(arg.clone()),
4441                    extra_args: Vec::new(),
4442                    distinct,
4443                    result_type: ResolvedType::Text,
4444                    filter: filter_owned,
4445                    order_by: retained_order_by.clone(),
4446                };
4447                let signature = aggregate_signature(
4448                    name,
4449                    distinct,
4450                    star,
4451                    Some(arg),
4452                    match &agg.function {
4453                        AggregateFunction::StringAgg { separator } => separator.as_ref(),
4454                        _ => None,
4455                    },
4456                    expr,
4457                    filter,
4458                    &retained_order_by,
4459                );
4460                Ok((agg, signature))
4461            }
4462            "json_group_array" => {
4463                let arg = self.require_single_aggregate_arg(args, expr.span)?;
4464                let agg = AggregateExpr {
4465                    function: AggregateFunction::JsonGroupArray,
4466                    arg: Some(arg.clone()),
4467                    extra_args: Vec::new(),
4468                    distinct,
4469                    result_type: ResolvedType::Text,
4470                    filter: filter_owned,
4471                    order_by: retained_order_by.clone(),
4472                };
4473                let signature = aggregate_signature(
4474                    name,
4475                    distinct,
4476                    star,
4477                    Some(arg),
4478                    None,
4479                    expr,
4480                    filter,
4481                    &retained_order_by,
4482                );
4483                Ok((agg, signature))
4484            }
4485            "array_agg" => {
4486                let arg = self.require_single_aggregate_arg(args, expr.span)?;
4487                let agg = AggregateExpr {
4488                    function: AggregateFunction::ArrayAgg,
4489                    arg: Some(arg.clone()),
4490                    extra_args: Vec::new(),
4491                    distinct,
4492                    result_type: ResolvedType::Array(Box::new(arg.resolved_type.clone())),
4493                    filter: filter_owned,
4494                    order_by: retained_order_by.clone(),
4495                };
4496                let signature = aggregate_signature(
4497                    name,
4498                    distinct,
4499                    star,
4500                    Some(arg),
4501                    None,
4502                    expr,
4503                    filter,
4504                    &retained_order_by,
4505                );
4506                Ok((agg, signature))
4507            }
4508            "json_group_object" => {
4509                if args.len() != 2 {
4510                    return Err(PlannerError::type_mismatch(
4511                        "2 arguments",
4512                        format!("{} arguments", args.len()),
4513                        expr.span,
4514                    ));
4515                }
4516                let agg = AggregateExpr {
4517                    function: AggregateFunction::JsonGroupObject,
4518                    arg: Some(args[0].clone()),
4519                    extra_args: vec![args[1].clone()],
4520                    distinct,
4521                    result_type: ResolvedType::Text,
4522                    filter: filter_owned,
4523                    order_by: retained_order_by.clone(),
4524                };
4525                let signature = aggregate_signature(
4526                    name,
4527                    distinct,
4528                    star,
4529                    Some(&args[0]),
4530                    None,
4531                    expr,
4532                    filter,
4533                    &retained_order_by,
4534                );
4535                Ok((agg, signature))
4536            }
4537            "jsonb_agg" => {
4538                let arg = self.require_single_aggregate_arg(args, expr.span)?;
4539                let agg = AggregateExpr {
4540                    function: AggregateFunction::JsonbAgg,
4541                    arg: Some(arg.clone()),
4542                    extra_args: Vec::new(),
4543                    distinct,
4544                    result_type: ResolvedType::Json,
4545                    filter: filter_owned,
4546                    order_by: retained_order_by.clone(),
4547                };
4548                let signature = aggregate_signature(
4549                    name,
4550                    distinct,
4551                    star,
4552                    Some(arg),
4553                    None,
4554                    expr,
4555                    filter,
4556                    &retained_order_by,
4557                );
4558                Ok((agg, signature))
4559            }
4560            "jsonb_object_agg" => {
4561                if args.len() != 2 {
4562                    return Err(PlannerError::type_mismatch(
4563                        "2 arguments",
4564                        format!("{} arguments", args.len()),
4565                        expr.span,
4566                    ));
4567                }
4568                let agg = AggregateExpr {
4569                    function: AggregateFunction::JsonbObjectAgg,
4570                    arg: Some(args[0].clone()),
4571                    extra_args: vec![args[1].clone()],
4572                    distinct,
4573                    result_type: ResolvedType::Json,
4574                    filter: filter_owned,
4575                    order_by: retained_order_by.clone(),
4576                };
4577                let signature = aggregate_signature(
4578                    name,
4579                    distinct,
4580                    star,
4581                    Some(&args[0]),
4582                    None,
4583                    expr,
4584                    filter,
4585                    &retained_order_by,
4586                );
4587                Ok((agg, signature))
4588            }
4589            "percentile_disc" => {
4590                if args.len() != 1 {
4591                    return Err(PlannerError::type_mismatch(
4592                        "1 argument",
4593                        format!("{} arguments", args.len()),
4594                        expr.span,
4595                    ));
4596                }
4597                let fraction = type_checker::percentile_fraction(&args[0])?;
4598                if retained_order_by.len() != 1 {
4599                    return Err(PlannerError::invalid_expression(
4600                        "PERCENTILE_DISC requires WITHIN GROUP (ORDER BY ...) with exactly \
4601                         one sort expression"
4602                            .to_string(),
4603                    ));
4604                }
4605                let sort = &retained_order_by[0];
4606                let agg = AggregateExpr {
4607                    function: AggregateFunction::PercentileDisc { fraction },
4608                    arg: Some(sort.expr.clone()),
4609                    extra_args: Vec::new(),
4610                    distinct: false,
4611                    result_type: sort.expr.resolved_type.clone(),
4612                    filter: filter_owned,
4613                    order_by: retained_order_by.clone(),
4614                };
4615                // The fraction rides the separator slot; the sort value's
4616                // identity lives in the order key (see AggregateSignature).
4617                let fraction_key = format!("{fraction:?}");
4618                let signature = aggregate_signature(
4619                    name,
4620                    false,
4621                    star,
4622                    None,
4623                    Some(&fraction_key),
4624                    expr,
4625                    filter,
4626                    &retained_order_by,
4627                );
4628                Ok((agg, signature))
4629            }
4630            "percentile_cont" => {
4631                let fraction = type_checker::percentile_fraction_named(name, &args[0])?;
4632                let sort = retained_order_by.first().ok_or_else(|| {
4633                    PlannerError::invalid_expression(
4634                        "PERCENTILE_CONT requires WITHIN GROUP (ORDER BY ...)".to_string(),
4635                    )
4636                })?;
4637                let agg = AggregateExpr {
4638                    function: AggregateFunction::PercentileCont { fraction },
4639                    arg: Some(sort.expr.clone()),
4640                    extra_args: Vec::new(),
4641                    distinct: false,
4642                    result_type: ResolvedType::Double,
4643                    filter: filter_owned,
4644                    order_by: retained_order_by.clone(),
4645                };
4646                let fraction_key = format!("{fraction:?}");
4647                let signature = aggregate_signature(
4648                    name,
4649                    false,
4650                    star,
4651                    None,
4652                    Some(&fraction_key),
4653                    expr,
4654                    filter,
4655                    &retained_order_by,
4656                );
4657                Ok((agg, signature))
4658            }
4659            "mode" if args.is_empty() => {
4660                let sort = retained_order_by.first().ok_or_else(|| {
4661                    PlannerError::invalid_expression(
4662                        "MODE requires WITHIN GROUP (ORDER BY ...)".to_string(),
4663                    )
4664                })?;
4665                let agg = AggregateExpr {
4666                    function: AggregateFunction::Mode,
4667                    arg: Some(sort.expr.clone()),
4668                    extra_args: Vec::new(),
4669                    distinct: false,
4670                    result_type: sort.expr.resolved_type.clone(),
4671                    filter: filter_owned,
4672                    order_by: retained_order_by.clone(),
4673                };
4674                let signature = aggregate_signature(
4675                    name,
4676                    false,
4677                    star,
4678                    None,
4679                    None,
4680                    expr,
4681                    filter,
4682                    &retained_order_by,
4683                );
4684                Ok((agg, signature))
4685            }
4686            name if type_checker::is_portable_aggregate_name(name) => {
4687                let function = match name {
4688                    "variance" | "var_samp" => AggregateFunction::Variance { sample: true },
4689                    "var_pop" => AggregateFunction::Variance { sample: false },
4690                    "stddev" | "stddev_samp" => AggregateFunction::Stddev { sample: true },
4691                    "stddev_pop" => AggregateFunction::Stddev { sample: false },
4692                    "covar_samp" => AggregateFunction::Covariance { sample: true },
4693                    "covar_pop" => AggregateFunction::Covariance { sample: false },
4694                    "corr" => AggregateFunction::Corr,
4695                    "median" => AggregateFunction::Median,
4696                    "mode" => AggregateFunction::Mode,
4697                    "quantile_cont" => AggregateFunction::QuantileCont {
4698                        fraction: type_checker::percentile_fraction_named(name, &args[1])?,
4699                    },
4700                    "regr_count" => AggregateFunction::RegrCount,
4701                    "regr_avgx" => AggregateFunction::RegrAvgX,
4702                    "regr_avgy" => AggregateFunction::RegrAvgY,
4703                    "regr_sxx" => AggregateFunction::RegrSxx,
4704                    "regr_syy" => AggregateFunction::RegrSyy,
4705                    "regr_sxy" => AggregateFunction::RegrSxy,
4706                    "regr_slope" => AggregateFunction::RegrSlope,
4707                    "regr_intercept" => AggregateFunction::RegrIntercept,
4708                    "regr_r2" => AggregateFunction::RegrR2,
4709                    "any_value" => AggregateFunction::AnyValue,
4710                    "first" => AggregateFunction::First,
4711                    "last" => AggregateFunction::Last,
4712                    "arg_min" | "min_by" => AggregateFunction::ArgMin,
4713                    "arg_max" | "max_by" => AggregateFunction::ArgMax,
4714                    "bit_and" => AggregateFunction::BitAnd,
4715                    "bit_or" => AggregateFunction::BitOr,
4716                    "bit_xor" => AggregateFunction::BitXor,
4717                    "bool_and" => AggregateFunction::BoolAnd,
4718                    "bool_or" => AggregateFunction::BoolOr,
4719                    _ => unreachable!(),
4720                };
4721                let primary = args[0].clone();
4722                let extra_args = if matches!(name, "quantile_cont") {
4723                    Vec::new()
4724                } else {
4725                    args[1..].to_vec()
4726                };
4727                let config = match &function {
4728                    AggregateFunction::PercentileCont { fraction }
4729                    | AggregateFunction::QuantileCont { fraction } => Some(format!("{fraction:?}")),
4730                    _ => None,
4731                };
4732                let agg = AggregateExpr {
4733                    function,
4734                    arg: Some(primary.clone()),
4735                    extra_args: extra_args.clone(),
4736                    distinct: false,
4737                    result_type: expr.resolved_type.clone(),
4738                    filter: filter_owned,
4739                    order_by: retained_order_by.clone(),
4740                };
4741                let mut signature = aggregate_signature(
4742                    name,
4743                    false,
4744                    star,
4745                    Some(&primary),
4746                    config.as_ref(),
4747                    expr,
4748                    filter,
4749                    &retained_order_by,
4750                );
4751                signature.extra_arg_keys = extra_args.iter().map(expr_key).collect();
4752                Ok((agg, signature))
4753            }
4754            _ => Err(PlannerError::unsupported_feature(
4755                format!("function '{}'", name),
4756                "future",
4757                expr.span,
4758            )),
4759        }
4760    }
4761    fn require_single_aggregate_arg<'b>(
4762        &self,
4763        args: &'b [TypedExpr],
4764        span: crate::ast::Span,
4765    ) -> Result<&'b TypedExpr, PlannerError> {
4766        if args.len() != 1 {
4767            return Err(PlannerError::type_mismatch(
4768                "1 argument",
4769                format!("{} arguments", args.len()),
4770                span,
4771            ));
4772        }
4773        Ok(&args[0])
4774    }
4775
4776    fn build_aggregate_projection(
4777        &self,
4778        projected: Vec<ProjectedColumn>,
4779        group_keys: &[TypedExpr],
4780        aggregates: &[AggregateExpr],
4781        output_names: &[String],
4782        grouping: Option<&GroupingRewrite>,
4783    ) -> Result<Projection, PlannerError> {
4784        let mut columns = Vec::new();
4785        for col in projected {
4786            let rewritten = self.rewrite_expr_for_aggregate(
4787                &col.expr,
4788                group_keys,
4789                aggregates,
4790                output_names,
4791                grouping,
4792            )?;
4793            columns.push(ProjectedColumn {
4794                expr: rewritten,
4795                alias: col.alias,
4796            });
4797        }
4798        Ok(Projection::Columns(columns))
4799    }
4800
4801    /// Rewrite an aggregate-context expression onto the aggregate output.
4802    ///
4803    /// `output_names` must name the group keys and aggregates only, never a
4804    /// trailing `__grouping_id` column: `rewrite_expr_with_maps` derives the
4805    /// group-key count from `output_names.len() - aggregate_map.len()`.
4806    /// GROUPING/GROUPING_ID calls are lowered onto `__grouping_id` first via
4807    /// the `grouping` context (issue #149, D4/D5).
4808    fn rewrite_expr_for_aggregate(
4809        &self,
4810        expr: &TypedExpr,
4811        group_keys: &[TypedExpr],
4812        aggregates: &[AggregateExpr],
4813        output_names: &[String],
4814        grouping: Option<&GroupingRewrite>,
4815    ) -> Result<TypedExpr, PlannerError> {
4816        let group_key_map = build_group_key_map(group_keys);
4817        let aggregate_map = build_aggregate_map(aggregates);
4818
4819        let expr = match grouping {
4820            Some(context) => rewrite_grouping_calls(expr, context)?,
4821            None => expr.clone(),
4822        };
4823        rewrite_expr_with_maps(&expr, &group_key_map, &aggregate_map, output_names)
4824    }
4825
4826    /// Resolve a LIMIT/OFFSET/FETCH count expression to a concrete value.
4827    ///
4828    /// The expression must be a constant scalar of an integer type
4829    /// (issue #152, D4/D5): literals, arithmetic, CAST, CASE, and
4830    /// deterministic scalar functions over constants are accepted and
4831    /// const-folded at plan time; column references, subqueries, and
4832    /// aggregate/window functions are rejected. NULL means "no limit"
4833    /// (`LIMIT NULL` / `FETCH FIRST NULL ROWS`) or "no offset"
4834    /// (`OFFSET NULL`), matching PostgreSQL (D6). Negative values are
4835    /// rejected (D7).
4836    fn resolve_pagination_count(
4837        &self,
4838        expr: &Option<crate::ast::expr::Expr>,
4839        clause: &str,
4840    ) -> Result<Option<u64>, PlannerError> {
4841        let Some(expr) = expr else {
4842            return Ok(None);
4843        };
4844        if expr_contains_subquery(expr) {
4845            return Err(PlannerError::unsupported_feature(
4846                format!("subquery in {clause}"),
4847                "a future version",
4848                expr.span,
4849            ));
4850        }
4851        // Empty scope: any column reference fails name resolution here.
4852        let typed = self
4853            .type_checker
4854            .infer_type_with_scope(expr, &[], &|stmt, _outer| {
4855                Err(PlannerError::unsupported_feature(
4856                    format!("subquery in {clause}"),
4857                    "a future version",
4858                    stmt.span(),
4859                ))
4860            })?;
4861        if typed_expr_contains_aggregate(&typed) {
4862            return Err(PlannerError::invalid_expression(format!(
4863                "aggregate functions are not allowed in {clause}"
4864            )));
4865        }
4866        if typed_expr_contains_window(&typed) {
4867            return Err(PlannerError::invalid_expression(format!(
4868                "window functions are not allowed in {clause}"
4869            )));
4870        }
4871        match typed.resolved_type {
4872            ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Null => {}
4873            ref other => {
4874                return Err(PlannerError::type_mismatch(
4875                    "BIGINT",
4876                    other.type_name().to_string(),
4877                    expr.span,
4878                ));
4879            }
4880        }
4881        // Constant-fold with an empty row context; the plan carries the
4882        // concrete value, so distributed execution never re-evaluates it.
4883        let context = crate::executor::evaluator::EvalContext::new(&[]);
4884        let value = crate::executor::evaluator::evaluate(&typed, &context).map_err(|error| {
4885            PlannerError::invalid_expression(format!("{clause} expression is invalid: {error}"))
4886        })?;
4887        let count = match value {
4888            crate::storage::SqlValue::Null => return Ok(None),
4889            crate::storage::SqlValue::Integer(value) => i64::from(value),
4890            crate::storage::SqlValue::BigInt(value) => value,
4891            other => {
4892                return Err(PlannerError::type_mismatch(
4893                    "BIGINT",
4894                    format!("{other:?}"),
4895                    expr.span,
4896                ));
4897            }
4898        };
4899        if count < 0 {
4900            return Err(PlannerError::invalid_expression(format!(
4901                "{clause} must not be negative"
4902            )));
4903        }
4904        Ok(Some(count as u64))
4905    }
4906
4907    /// Apply the LIMIT/OFFSET/FETCH tail to a plan (issue #152).
4908    ///
4909    /// WITH TIES copies the sort keys from the `Sort` node directly beneath
4910    /// the Limit; without ORDER BY it is rejected (D3, PostgreSQL 42P20).
4911    fn apply_pagination(
4912        &self,
4913        plan: LogicalPlan,
4914        limit: &Option<Expr>,
4915        offset: &Option<Expr>,
4916        with_ties: bool,
4917    ) -> Result<LogicalPlan, PlannerError> {
4918        self.apply_pagination_with_tie_keys(plan, limit, offset, with_ties, None)
4919    }
4920
4921    /// Apply the pagination tail with an explicit WITH TIES peer specification.
4922    ///
4923    /// `tie_keys` is `None` for every ordinary query block, where the peer
4924    /// specification is the `Sort` node directly beneath the Limit. The
4925    /// DISTINCT ON path plans no `Sort` node of its own (sql-distinct-on.md
4926    /// D8), so it supplies the user's ORDER BY explicitly (D13); an empty
4927    /// slice there means the query has no ORDER BY and WITH TIES is rejected.
4928    fn apply_pagination_with_tie_keys(
4929        &self,
4930        plan: LogicalPlan,
4931        limit: &Option<Expr>,
4932        offset: &Option<Expr>,
4933        with_ties: bool,
4934        tie_keys: Option<&[SortExpr]>,
4935    ) -> Result<LogicalPlan, PlannerError> {
4936        if limit.is_none() && offset.is_none() && !with_ties {
4937            return Ok(plan);
4938        }
4939        let ties = if with_ties {
4940            let keys = match tie_keys {
4941                Some(keys) => keys.to_vec(),
4942                None => match &plan {
4943                    LogicalPlan::Sort { order_by, .. } => order_by.clone(),
4944                    _ => Vec::new(),
4945                },
4946            };
4947            if keys.is_empty() {
4948                return Err(PlannerError::invalid_expression(
4949                    "FETCH ... WITH TIES requires ORDER BY".to_string(),
4950                ));
4951            }
4952            Some(keys)
4953        } else {
4954            None
4955        };
4956        Ok(LogicalPlan::Limit {
4957            input: Box::new(plan),
4958            limit: self.resolve_pagination_count(limit, "LIMIT")?,
4959            offset: self.resolve_pagination_count(offset, "OFFSET")?,
4960            ties,
4961        })
4962    }
4963
4964    /// Plan an INSERT statement.
4965    ///
4966    /// Handles column list specification or implicit column ordering.
4967    /// When columns are omitted, uses table definition order from TableMetadata.
4968    fn plan_insert(&self, stmt: &Insert) -> Result<LogicalPlan, PlannerError> {
4969        // Resolve the target table
4970        let table = self.name_resolver.resolve_table(&stmt.table, stmt.span)?;
4971
4972        // Determine the column list
4973        let columns: Vec<String> = if let Some(ref cols) = stmt.columns {
4974            // Explicit column list - validate each column exists
4975            for col in cols {
4976                self.name_resolver.resolve_column(table, col, stmt.span)?;
4977            }
4978            cols.clone()
4979        } else {
4980            // Implicit - use all columns in table definition order
4981            table.column_names().into_iter().map(String::from).collect()
4982        };
4983
4984        match &stmt.source {
4985            InsertSource::Values { values } => {
4986                let mut typed_values: Vec<Vec<TypedExpr>> = Vec::new();
4987
4988                for row in values {
4989                    if row.len() != columns.len() {
4990                        return Err(PlannerError::column_value_count_mismatch(
4991                            columns.len(),
4992                            row.len(),
4993                            stmt.span,
4994                        ));
4995                    }
4996
4997                    typed_values.push(self.type_check_insert_values(row, &columns, table)?);
4998                }
4999
5000                Ok(LogicalPlan::Insert {
5001                    table: table.name.clone(),
5002                    columns,
5003                    values: typed_values,
5004                })
5005            }
5006            InsertSource::Select { select } => {
5007                let source = self.plan_select_relation(select, &[], &CtePlans::new())?;
5008                self.finish_insert_query(stmt, table, columns, source)
5009            }
5010            InsertSource::Query { query } => {
5011                let source = self.plan_query_body_relation(query, &[], &CtePlans::new())?;
5012                self.finish_insert_query(stmt, table, columns, source)
5013            }
5014        }
5015    }
5016
5017    fn finish_insert_query(
5018        &self,
5019        stmt: &Insert,
5020        table: &TableMetadata,
5021        columns: Vec<String>,
5022        source: PlannedRelation,
5023    ) -> Result<LogicalPlan, PlannerError> {
5024        if source.schema.len() != columns.len() {
5025            return Err(PlannerError::column_value_count_mismatch(
5026                columns.len(),
5027                source.schema.len(),
5028                stmt.span,
5029            ));
5030        }
5031
5032        for (source_column, target_column) in source.schema.iter().zip(&columns) {
5033            let target = table
5034                .get_column(target_column)
5035                .expect("validated target column");
5036            if target.not_null && source_column.data_type == ResolvedType::Null {
5037                return Err(PlannerError::null_constraint_violation(
5038                    target_column,
5039                    stmt.span,
5040                ));
5041            }
5042            self.validate_resolved_type_assignment(
5043                &source_column.data_type,
5044                &target.data_type,
5045                stmt.span,
5046            )?;
5047        }
5048
5049        Ok(LogicalPlan::InsertSelect {
5050            table: table.name.clone(),
5051            columns,
5052            source: Box::new(source.plan),
5053        })
5054    }
5055
5056    /// Type-check INSERT values against column definitions.
5057    fn type_check_insert_values(
5058        &self,
5059        values: &[crate::ast::expr::Expr],
5060        columns: &[String],
5061        table: &TableMetadata,
5062    ) -> Result<Vec<TypedExpr>, PlannerError> {
5063        let mut typed_values = Vec::new();
5064
5065        for (i, value) in values.iter().enumerate() {
5066            let column_name = &columns[i];
5067            let column_meta = table.get_column(column_name).ok_or_else(|| {
5068                PlannerError::column_not_found(column_name, &table.name, value.span)
5069            })?;
5070
5071            // Type-check the value expression
5072            let typed_value = self.type_checker.infer_type(value, table)?;
5073
5074            // Check for NOT NULL constraint violation (except for NULL literal which is allowed if nullable)
5075            if column_meta.not_null
5076                && matches!(&typed_value.kind, TypedExprKind::Literal(Literal::Null))
5077            {
5078                return Err(PlannerError::null_constraint_violation(
5079                    column_name,
5080                    value.span,
5081                ));
5082            }
5083
5084            // Validate type compatibility
5085            self.validate_type_assignment(&typed_value, &column_meta.data_type, value.span)?;
5086
5087            let typed_value =
5088                self.coerce_assignment_value(typed_value, &column_meta.data_type, value.span);
5089
5090            typed_values.push(typed_value);
5091        }
5092
5093        Ok(typed_values)
5094    }
5095
5096    /// Validate that a value type can be assigned to a column type.
5097    fn validate_type_assignment(
5098        &self,
5099        value: &TypedExpr,
5100        target_type: &ResolvedType,
5101        span: crate::ast::Span,
5102    ) -> Result<(), PlannerError> {
5103        self.validate_resolved_type_assignment(&value.resolved_type, target_type, span)
5104    }
5105
5106    fn validate_resolved_type_assignment(
5107        &self,
5108        source_type: &ResolvedType,
5109        target_type: &ResolvedType,
5110        span: crate::ast::Span,
5111    ) -> Result<(), PlannerError> {
5112        // NULL can be assigned to any nullable column
5113        if *source_type == ResolvedType::Null {
5114            return Ok(());
5115        }
5116
5117        // Check for exact match or implicit conversion compatibility
5118        if self.types_compatible(source_type, target_type) {
5119            return Ok(());
5120        }
5121
5122        Err(PlannerError::type_mismatch(
5123            target_type.to_string(),
5124            source_type.to_string(),
5125            span,
5126        ))
5127    }
5128
5129    /// Check if two types are compatible for assignment.
5130    fn types_compatible(&self, source: &ResolvedType, target: &ResolvedType) -> bool {
5131        use ResolvedType::*;
5132
5133        // Same type is always compatible
5134        if source == target {
5135            return true;
5136        }
5137
5138        // Numeric promotions
5139        match (source, target) {
5140            // Integer can be assigned to BigInt, Float, Double
5141            (Integer, BigInt) | (Integer, Float) | (Integer, Double) => true,
5142            // BigInt can be assigned to Float, Double
5143            (BigInt, Float) | (BigInt, Double) => true,
5144            // Float can be assigned to Double
5145            (Float, Double) => true,
5146            // A decimal literal is typed DOUBLE, so a FLOAT column needs this
5147            // narrowing; the value is rounded to f32 at execution time.
5148            (Double, Float) => true,
5149            (Integer | BigInt | Float | Double | Text | Decimal { .. }, Decimal { .. }) => true,
5150            // TIMESTAMP is stored as microseconds; text and numeric input is
5151            // converted by the assignment expression at execution time.
5152            (Text | Integer | BigInt | Float | Double, Timestamp) => true,
5153            (Text, Date | Time | Interval) => true,
5154            // Vector dimensions must match
5155            (Vector { dimension: d1, .. }, Vector { dimension: d2, .. }) => d1 == d2,
5156            _ => false,
5157        }
5158    }
5159
5160    fn coerce_assignment_value(
5161        &self,
5162        value: TypedExpr,
5163        target_type: &ResolvedType,
5164        span: crate::ast::Span,
5165    ) -> TypedExpr {
5166        if value.resolved_type != *target_type
5167            && value.resolved_type != ResolvedType::Null
5168            && matches!(
5169                target_type,
5170                ResolvedType::Integer
5171                    | ResolvedType::BigInt
5172                    | ResolvedType::Float
5173                    | ResolvedType::Double
5174                    | ResolvedType::Timestamp
5175                    | ResolvedType::Date
5176                    | ResolvedType::Time
5177                    | ResolvedType::Interval
5178                    | ResolvedType::Decimal { .. }
5179            )
5180        {
5181            TypedExpr::cast(value, target_type.clone(), span)
5182        } else {
5183            value
5184        }
5185    }
5186
5187    /// Plan an UPDATE statement.
5188    ///
5189    /// Validates assignments and optional WHERE clause.
5190    fn plan_update(&self, stmt: &Update) -> Result<LogicalPlan, PlannerError> {
5191        // Resolve the target table
5192        let table = self.name_resolver.resolve_table(&stmt.table, stmt.span)?;
5193
5194        // Process assignments
5195        let mut typed_assignments = Vec::new();
5196
5197        for assignment in &stmt.assignments {
5198            // Resolve the column
5199            let column_meta =
5200                self.name_resolver
5201                    .resolve_column(table, &assignment.column, assignment.span)?;
5202            let column_index = table.get_column_index(&assignment.column).unwrap();
5203
5204            // Type-check the value expression
5205            let typed_value = self.type_checker.infer_type(&assignment.value, table)?;
5206
5207            // Check NOT NULL constraint
5208            if column_meta.not_null
5209                && matches!(&typed_value.kind, TypedExprKind::Literal(Literal::Null))
5210            {
5211                return Err(PlannerError::null_constraint_violation(
5212                    &assignment.column,
5213                    assignment.value.span,
5214                ));
5215            }
5216
5217            // Validate type compatibility
5218            self.validate_type_assignment(
5219                &typed_value,
5220                &column_meta.data_type,
5221                assignment.value.span,
5222            )?;
5223
5224            let typed_value = self.coerce_assignment_value(
5225                typed_value,
5226                &column_meta.data_type,
5227                assignment.value.span,
5228            );
5229
5230            typed_assignments.push(TypedAssignment::new(
5231                assignment.column.clone(),
5232                column_index,
5233                typed_value,
5234            ));
5235        }
5236
5237        // Process optional WHERE clause
5238        let filter = if let Some(ref selection) = stmt.selection {
5239            let predicate = self.type_checker.infer_type(selection, table)?;
5240
5241            // Verify predicate returns Boolean
5242            if predicate.resolved_type != ResolvedType::Boolean {
5243                return Err(PlannerError::type_mismatch(
5244                    "Boolean",
5245                    predicate.resolved_type.to_string(),
5246                    selection.span,
5247                ));
5248            }
5249
5250            Some(predicate)
5251        } else {
5252            None
5253        };
5254
5255        Ok(LogicalPlan::Update {
5256            table: table.name.clone(),
5257            assignments: typed_assignments,
5258            filter,
5259        })
5260    }
5261
5262    /// Plan a DELETE statement.
5263    ///
5264    /// Validates optional WHERE clause.
5265    fn plan_delete(&self, stmt: &Delete) -> Result<LogicalPlan, PlannerError> {
5266        // Resolve the target table
5267        let table = self.name_resolver.resolve_table(&stmt.table, stmt.span)?;
5268
5269        // Process optional WHERE clause
5270        let filter = if let Some(ref selection) = stmt.selection {
5271            let predicate = self.type_checker.infer_type(selection, table)?;
5272
5273            // Verify predicate returns Boolean
5274            if predicate.resolved_type != ResolvedType::Boolean {
5275                return Err(PlannerError::type_mismatch(
5276                    "Boolean",
5277                    predicate.resolved_type.to_string(),
5278                    selection.span,
5279                ));
5280            }
5281
5282            Some(predicate)
5283        } else {
5284            None
5285        };
5286
5287        Ok(LogicalPlan::Delete {
5288            table: table.name.clone(),
5289            filter,
5290        })
5291    }
5292}
5293
5294#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5295struct AggregateSignature {
5296    name: String,
5297    distinct: bool,
5298    star: bool,
5299    arg_key: Option<String>,
5300    extra_arg_keys: Vec<String>,
5301    separator: Option<String>,
5302    /// FILTER (WHERE ...) identity: aggregates that differ only in their
5303    /// filter are distinct physical aggregates (issue #148, D10).
5304    filter_key: Option<String>,
5305    /// Aggregate-local ordering identity; populated only for order-sensitive
5306    /// aggregates so a discarded ORDER BY (D3) still deduplicates with the
5307    /// unordered spelling.
5308    order_key: Option<String>,
5309}
5310
5311/// Collect the SELECT-list aliases that ORDER BY / HAVING may reference.
5312///
5313/// Per the SQL standard, aliases introduced by the projection are visible to
5314/// HAVING and ORDER BY (which are logically evaluated after the projection),
5315/// but not to WHERE / GROUP BY. Only `SelectItem::Expr` carries an alias;
5316/// wildcards contribute nothing.
5317///
5318/// When the same alias is declared twice the first declaration wins, which
5319/// keeps the substitution deterministic instead of depending on map ordering.
5320fn collect_projection_aliases(items: &[SelectItem]) -> HashMap<String, crate::ast::expr::Expr> {
5321    let mut aliases = HashMap::new();
5322    for item in items {
5323        if let SelectItem::Expr {
5324            expr,
5325            alias: Some(alias),
5326            ..
5327        } = item
5328        {
5329            aliases.entry(alias.clone()).or_insert_with(|| expr.clone());
5330        }
5331    }
5332    aliases
5333}
5334
5335/// Substitute projection aliases inside an ORDER BY / HAVING expression.
5336///
5337/// An unqualified `ColumnRef` whose name matches a projection alias is replaced
5338/// by the aliased source expression, so everything downstream (type inference,
5339/// aggregate collection, `validate_having_expr`, and the aggregate output
5340/// rewrite) observes the very expression the projection already produced.
5341///
5342/// Substitution rules:
5343/// - Only unqualified references are eligible; `t.total` always means the base
5344///   column `total` of table `t`, never an alias.
5345/// - An alias takes precedence over a base column of the same name, per the
5346///   SQL standard. `order_by_prefers_projection_alias_over_shadowed_base_column`
5347///   pins this behaviour.
5348/// - The substituted expression keeps the *reference* site's span so that any
5349///   resulting diagnostic still points at the ORDER BY / HAVING clause.
5350/// - Subqueries are not descended into: an inner SELECT establishes its own
5351///   projection scope, so the outer alias must not leak inside it.
5352fn substitute_projection_aliases(
5353    expr: &crate::ast::expr::Expr,
5354    aliases: &HashMap<String, crate::ast::expr::Expr>,
5355) -> crate::ast::expr::Expr {
5356    use crate::ast::expr::ExprKind;
5357
5358    if aliases.is_empty() {
5359        return expr.clone();
5360    }
5361
5362    let recurse = |e: &crate::ast::expr::Expr| substitute_projection_aliases(e, aliases);
5363
5364    let kind = match &expr.kind {
5365        ExprKind::ColumnRef {
5366            table: None,
5367            column,
5368        } => match aliases.get(column) {
5369            Some(source) => {
5370                let mut replacement = source.clone();
5371                replacement.span = expr.span;
5372                return replacement;
5373            }
5374            None => return expr.clone(),
5375        },
5376        ExprKind::BinaryOp { left, op, right } => ExprKind::BinaryOp {
5377            left: Box::new(recurse(left)),
5378            op: *op,
5379            right: Box::new(recurse(right)),
5380        },
5381        ExprKind::UnaryOp { op, operand } => ExprKind::UnaryOp {
5382            op: *op,
5383            operand: Box::new(recurse(operand)),
5384        },
5385        ExprKind::FunctionCall {
5386            name,
5387            args,
5388            distinct,
5389            star,
5390            order_by,
5391            within_group,
5392            filter,
5393            over,
5394        } => ExprKind::FunctionCall {
5395            name: name.clone(),
5396            args: args.iter().map(recurse).collect(),
5397            distinct: *distinct,
5398            star: *star,
5399            order_by: order_by
5400                .iter()
5401                .map(|order| OrderByExpr {
5402                    expr: recurse(&order.expr),
5403                    asc: order.asc,
5404                    nulls_first: order.nulls_first,
5405                    span: order.span,
5406                })
5407                .collect(),
5408            within_group: within_group
5409                .iter()
5410                .map(|order| OrderByExpr {
5411                    expr: recurse(&order.expr),
5412                    asc: order.asc,
5413                    nulls_first: order.nulls_first,
5414                    span: order.span,
5415                })
5416                .collect(),
5417            filter: filter
5418                .as_deref()
5419                .map(|predicate| Box::new(recurse(predicate))),
5420            over: over.as_ref().map(|window| crate::ast::expr::WindowSpec {
5421                base: window.base.clone(),
5422                partition_by: window.partition_by.iter().map(recurse).collect(),
5423                order_by: window
5424                    .order_by
5425                    .iter()
5426                    .map(|order| OrderByExpr {
5427                        expr: recurse(&order.expr),
5428                        asc: order.asc,
5429                        nulls_first: order.nulls_first,
5430                        span: order.span,
5431                    })
5432                    .collect(),
5433                frame: window.frame.clone(),
5434            }),
5435        },
5436        ExprKind::Case {
5437            operand,
5438            branches,
5439            else_expr,
5440        } => ExprKind::Case {
5441            operand: operand.as_deref().map(|e| Box::new(recurse(e))),
5442            branches: branches
5443                .iter()
5444                .map(|branch| crate::ast::expr::CaseWhen {
5445                    when: recurse(&branch.when),
5446                    then: recurse(&branch.then),
5447                })
5448                .collect(),
5449            else_expr: else_expr.as_deref().map(|e| Box::new(recurse(e))),
5450        },
5451        ExprKind::Cast { expr, target_type } => ExprKind::Cast {
5452            expr: Box::new(recurse(expr)),
5453            target_type: target_type.clone(),
5454        },
5455        ExprKind::TryCast { expr, target_type } => ExprKind::TryCast {
5456            expr: Box::new(recurse(expr)),
5457            target_type: target_type.clone(),
5458        },
5459        ExprKind::Between {
5460            expr,
5461            low,
5462            high,
5463            negated,
5464        } => ExprKind::Between {
5465            expr: Box::new(recurse(expr)),
5466            low: Box::new(recurse(low)),
5467            high: Box::new(recurse(high)),
5468            negated: *negated,
5469        },
5470        ExprKind::Like {
5471            expr,
5472            pattern,
5473            escape,
5474            negated,
5475            kind,
5476        } => ExprKind::Like {
5477            expr: Box::new(recurse(expr)),
5478            pattern: Box::new(recurse(pattern)),
5479            escape: escape.as_deref().map(|e| Box::new(recurse(e))),
5480            negated: *negated,
5481            kind: *kind,
5482        },
5483        ExprKind::InList {
5484            expr,
5485            list,
5486            negated,
5487        } => ExprKind::InList {
5488            expr: Box::new(recurse(expr)),
5489            list: list.iter().map(recurse).collect(),
5490            negated: *negated,
5491        },
5492        ExprKind::IsNull { expr, negated } => ExprKind::IsNull {
5493            expr: Box::new(recurse(expr)),
5494            negated: *negated,
5495        },
5496        ExprKind::TruthPredicate {
5497            expr,
5498            value,
5499            negated,
5500        } => ExprKind::TruthPredicate {
5501            expr: Box::new(recurse(expr)),
5502            value: *value,
5503            negated: *negated,
5504        },
5505        ExprKind::IsDistinctFrom {
5506            left,
5507            right,
5508            negated,
5509        } => ExprKind::IsDistinctFrom {
5510            left: Box::new(recurse(left)),
5511            right: Box::new(recurse(right)),
5512            negated: *negated,
5513        },
5514        ExprKind::Row { items } => ExprKind::Row {
5515            items: items.iter().map(recurse).collect(),
5516        },
5517        // Qualified column refs, literals, and subquery-bearing expressions are
5518        // left untouched (see the subquery note above).
5519        ExprKind::ColumnRef { .. }
5520        | ExprKind::Literal { .. }
5521        | ExprKind::VectorLiteral { .. }
5522        | ExprKind::ScalarSubquery { .. }
5523        | ExprKind::InSubquery { .. }
5524        | ExprKind::Exists { .. }
5525        | ExprKind::Quantified { .. } => return expr.clone(),
5526    };
5527
5528    crate::ast::expr::Expr {
5529        kind,
5530        span: expr.span,
5531    }
5532}
5533
5534fn build_offset_window_function(
5535    name: &str,
5536    args: &[TypedExpr],
5537) -> Result<OffsetWindowFunction, PlannerError> {
5538    let value = args.first().cloned().ok_or_else(|| {
5539        PlannerError::invalid_expression(format!(
5540            "{}() window function expects 1 to 3 arguments",
5541            name.to_ascii_uppercase()
5542        ))
5543    })?;
5544    if args.len() > 3 {
5545        return Err(PlannerError::invalid_expression(format!(
5546            "{}() window function expects 1 to 3 arguments",
5547            name.to_ascii_uppercase()
5548        )));
5549    }
5550    Ok(OffsetWindowFunction {
5551        value,
5552        offset: args.get(1).cloned(),
5553        default: args.get(2).cloned(),
5554    })
5555}
5556
5557fn expr_contains_aggregate(expr: &crate::ast::expr::Expr) -> bool {
5558    use crate::ast::expr::ExprKind;
5559
5560    match &expr.kind {
5561        ExprKind::FunctionCall {
5562            name,
5563            args,
5564            order_by,
5565            within_group,
5566            filter,
5567            over,
5568            ..
5569        } => {
5570            if over.is_none() && is_aggregate_function(name) {
5571                return true;
5572            }
5573            args.iter().any(expr_contains_aggregate)
5574                || order_by
5575                    .iter()
5576                    .any(|sort| expr_contains_aggregate(&sort.expr))
5577                || within_group
5578                    .iter()
5579                    .any(|sort| expr_contains_aggregate(&sort.expr))
5580                || filter.as_deref().is_some_and(expr_contains_aggregate)
5581                || over.as_ref().is_some_and(|window| {
5582                    window.partition_by.iter().any(expr_contains_aggregate)
5583                        || window
5584                            .order_by
5585                            .iter()
5586                            .any(|sort| expr_contains_aggregate(&sort.expr))
5587                })
5588        }
5589        ExprKind::BinaryOp { left, right, .. } => {
5590            expr_contains_aggregate(left) || expr_contains_aggregate(right)
5591        }
5592        ExprKind::UnaryOp { operand, .. } => expr_contains_aggregate(operand),
5593        ExprKind::TruthPredicate { expr, .. } => expr_contains_aggregate(expr),
5594        ExprKind::IsDistinctFrom { left, right, .. } => {
5595            expr_contains_aggregate(left) || expr_contains_aggregate(right)
5596        }
5597        ExprKind::Row { items } => items.iter().any(expr_contains_aggregate),
5598        ExprKind::Case {
5599            operand,
5600            branches,
5601            else_expr,
5602        } => {
5603            operand.as_deref().is_some_and(expr_contains_aggregate)
5604                || branches.iter().any(|branch| {
5605                    expr_contains_aggregate(&branch.when) || expr_contains_aggregate(&branch.then)
5606                })
5607                || else_expr.as_deref().is_some_and(expr_contains_aggregate)
5608        }
5609        ExprKind::Cast { expr, .. } | ExprKind::TryCast { expr, .. } => {
5610            expr_contains_aggregate(expr)
5611        }
5612        ExprKind::Between {
5613            expr, low, high, ..
5614        } => {
5615            expr_contains_aggregate(expr)
5616                || expr_contains_aggregate(low)
5617                || expr_contains_aggregate(high)
5618        }
5619        ExprKind::Like {
5620            expr,
5621            pattern,
5622            escape,
5623            ..
5624        } => {
5625            expr_contains_aggregate(expr)
5626                || expr_contains_aggregate(pattern)
5627                || escape.as_deref().is_some_and(expr_contains_aggregate)
5628        }
5629        ExprKind::InList { expr, list, .. } => {
5630            expr_contains_aggregate(expr) || list.iter().any(expr_contains_aggregate)
5631        }
5632        ExprKind::IsNull { expr, .. } => expr_contains_aggregate(expr),
5633        ExprKind::ScalarSubquery { .. }
5634        | ExprKind::InSubquery { .. }
5635        | ExprKind::Exists { .. }
5636        | ExprKind::Quantified { .. }
5637        | ExprKind::Literal { .. }
5638        | ExprKind::VectorLiteral { .. }
5639        | ExprKind::ColumnRef { .. } => false,
5640    }
5641}
5642
5643fn typed_expr_contains_aggregate(expr: &TypedExpr) -> bool {
5644    match &expr.kind {
5645        TypedExprKind::FunctionCall {
5646            name,
5647            args,
5648            filter,
5649            order_by,
5650            over,
5651            ..
5652        } => {
5653            if over.is_none() && is_aggregate_function(name) {
5654                return true;
5655            }
5656            args.iter().any(typed_expr_contains_aggregate)
5657                || filter.as_deref().is_some_and(typed_expr_contains_aggregate)
5658                || order_by
5659                    .iter()
5660                    .any(|sort| typed_expr_contains_aggregate(&sort.expr))
5661                || over.as_ref().is_some_and(|window| {
5662                    window
5663                        .partition_by
5664                        .iter()
5665                        .any(typed_expr_contains_aggregate)
5666                        || window
5667                            .order_by
5668                            .iter()
5669                            .any(|sort| typed_expr_contains_aggregate(&sort.expr))
5670                })
5671        }
5672        TypedExprKind::BinaryOp { left, right, .. } => {
5673            typed_expr_contains_aggregate(left) || typed_expr_contains_aggregate(right)
5674        }
5675        TypedExprKind::UnaryOp { operand, .. } => typed_expr_contains_aggregate(operand),
5676        TypedExprKind::Cast { expr, .. } | TypedExprKind::TryCast { expr, .. } => {
5677            typed_expr_contains_aggregate(expr)
5678        }
5679        TypedExprKind::Case {
5680            operand,
5681            branches,
5682            else_expr,
5683        } => {
5684            operand
5685                .as_deref()
5686                .is_some_and(typed_expr_contains_aggregate)
5687                || branches.iter().any(|branch| {
5688                    typed_expr_contains_aggregate(&branch.when)
5689                        || typed_expr_contains_aggregate(&branch.then)
5690                })
5691                || else_expr
5692                    .as_deref()
5693                    .is_some_and(typed_expr_contains_aggregate)
5694        }
5695        TypedExprKind::Between {
5696            expr, low, high, ..
5697        } => {
5698            typed_expr_contains_aggregate(expr)
5699                || typed_expr_contains_aggregate(low)
5700                || typed_expr_contains_aggregate(high)
5701        }
5702        TypedExprKind::Like {
5703            expr,
5704            pattern,
5705            escape,
5706            ..
5707        } => {
5708            typed_expr_contains_aggregate(expr)
5709                || typed_expr_contains_aggregate(pattern)
5710                || escape
5711                    .as_ref()
5712                    .is_some_and(|inner| typed_expr_contains_aggregate(inner))
5713        }
5714        TypedExprKind::InList { expr, list, .. } => {
5715            typed_expr_contains_aggregate(expr) || list.iter().any(typed_expr_contains_aggregate)
5716        }
5717        TypedExprKind::IsNull { expr, .. } => typed_expr_contains_aggregate(expr),
5718        TypedExprKind::InSubquery { expr, .. } => typed_expr_contains_aggregate(expr),
5719        TypedExprKind::Quantified { expr, .. } => typed_expr_contains_aggregate(expr),
5720        TypedExprKind::ScalarSubquery(_) | TypedExprKind::Exists { .. } => false,
5721        _ => false,
5722    }
5723}
5724
5725fn select_contains_window(stmt: &Select) -> bool {
5726    stmt.projection.iter().any(|item| match item {
5727        SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => false,
5728        SelectItem::Expr { expr, .. } => expr_contains_window(expr),
5729    }) || stmt.qualify.as_ref().is_some_and(expr_contains_window)
5730        || stmt
5731            .order_by
5732            .iter()
5733            .any(|order| expr_contains_window(&order.expr))
5734}
5735
5736fn expr_contains_window(expr: &crate::ast::expr::Expr) -> bool {
5737    match &expr.kind {
5738        crate::ast::expr::ExprKind::FunctionCall {
5739            args,
5740            order_by,
5741            within_group,
5742            filter,
5743            over,
5744            ..
5745        } => {
5746            over.is_some()
5747                || args.iter().any(expr_contains_window)
5748                || order_by.iter().any(|sort| expr_contains_window(&sort.expr))
5749                || within_group
5750                    .iter()
5751                    .any(|sort| expr_contains_window(&sort.expr))
5752                || filter.as_deref().is_some_and(expr_contains_window)
5753        }
5754        crate::ast::expr::ExprKind::BinaryOp { left, right, .. } => {
5755            expr_contains_window(left) || expr_contains_window(right)
5756        }
5757        crate::ast::expr::ExprKind::UnaryOp { operand, .. }
5758        | crate::ast::expr::ExprKind::Cast { expr: operand, .. }
5759        | crate::ast::expr::ExprKind::TryCast { expr: operand, .. }
5760        | crate::ast::expr::ExprKind::IsNull { expr: operand, .. } => expr_contains_window(operand),
5761        crate::ast::expr::ExprKind::Between {
5762            expr, low, high, ..
5763        } => expr_contains_window(expr) || expr_contains_window(low) || expr_contains_window(high),
5764        crate::ast::expr::ExprKind::Like {
5765            expr,
5766            pattern,
5767            escape,
5768            ..
5769        } => {
5770            expr_contains_window(expr)
5771                || expr_contains_window(pattern)
5772                || escape.as_deref().is_some_and(expr_contains_window)
5773        }
5774        crate::ast::expr::ExprKind::InList { expr, list, .. } => {
5775            expr_contains_window(expr) || list.iter().any(expr_contains_window)
5776        }
5777        _ => false,
5778    }
5779}
5780
5781fn typed_expr_contains_window(expr: &TypedExpr) -> bool {
5782    match &expr.kind {
5783        TypedExprKind::FunctionCall {
5784            args,
5785            filter,
5786            order_by,
5787            over,
5788            ..
5789        } => {
5790            over.is_some()
5791                || args.iter().any(typed_expr_contains_window)
5792                || filter.as_deref().is_some_and(typed_expr_contains_window)
5793                || order_by
5794                    .iter()
5795                    .any(|sort| typed_expr_contains_window(&sort.expr))
5796        }
5797        TypedExprKind::BinaryOp { left, right, .. } => {
5798            typed_expr_contains_window(left) || typed_expr_contains_window(right)
5799        }
5800        TypedExprKind::UnaryOp { operand, .. }
5801        | TypedExprKind::Cast { expr: operand, .. }
5802        | TypedExprKind::TryCast { expr: operand, .. }
5803        | TypedExprKind::IsNull { expr: operand, .. } => typed_expr_contains_window(operand),
5804        TypedExprKind::Between {
5805            expr, low, high, ..
5806        } => {
5807            typed_expr_contains_window(expr)
5808                || typed_expr_contains_window(low)
5809                || typed_expr_contains_window(high)
5810        }
5811        TypedExprKind::Like {
5812            expr,
5813            pattern,
5814            escape,
5815            ..
5816        } => {
5817            typed_expr_contains_window(expr)
5818                || typed_expr_contains_window(pattern)
5819                || escape.as_deref().is_some_and(typed_expr_contains_window)
5820        }
5821        TypedExprKind::InList { expr, list, .. } => {
5822            typed_expr_contains_window(expr) || list.iter().any(typed_expr_contains_window)
5823        }
5824        _ => false,
5825    }
5826}
5827
5828fn rewrite_projection_for_windows(
5829    projection: &Projection,
5830    window_map: &HashMap<String, usize>,
5831    base_width: usize,
5832    window_names: &[String],
5833) -> Result<Projection, PlannerError> {
5834    match projection {
5835        Projection::All(names) => Ok(Projection::All(names.clone())),
5836        Projection::Columns(columns) => Ok(Projection::Columns(
5837            columns
5838                .iter()
5839                .map(|column| {
5840                    Ok(ProjectedColumn {
5841                        expr: rewrite_expr_for_windows(
5842                            &column.expr,
5843                            window_map,
5844                            base_width,
5845                            window_names,
5846                        )?,
5847                        alias: column.alias.clone(),
5848                    })
5849                })
5850                .collect::<Result<Vec<_>, PlannerError>>()?,
5851        )),
5852    }
5853}
5854
5855fn rewrite_expr_for_windows(
5856    expr: &TypedExpr,
5857    window_map: &HashMap<String, usize>,
5858    base_width: usize,
5859    window_names: &[String],
5860) -> Result<TypedExpr, PlannerError> {
5861    if let Some(index) = window_map.get(&expr_key(expr)) {
5862        return Ok(TypedExpr::column_ref(
5863            "__window__".to_string(),
5864            window_names
5865                .get(*index)
5866                .cloned()
5867                .unwrap_or_else(|| format!("__window_{index}")),
5868            base_width + index,
5869            expr.resolved_type.clone(),
5870            expr.span,
5871        ));
5872    }
5873
5874    let rewrite =
5875        |inner: &TypedExpr| rewrite_expr_for_windows(inner, window_map, base_width, window_names);
5876    let kind = match &expr.kind {
5877        TypedExprKind::FunctionCall {
5878            name,
5879            args,
5880            distinct,
5881            star,
5882            filter,
5883            order_by,
5884            over,
5885        } => {
5886            if over.is_some() {
5887                return Err(PlannerError::invalid_expression(
5888                    "window expression is not part of the window plan".to_string(),
5889                ));
5890            }
5891            TypedExprKind::FunctionCall {
5892                name: name.clone(),
5893                args: args.iter().map(rewrite).collect::<Result<Vec<_>, _>>()?,
5894                distinct: *distinct,
5895                star: *star,
5896                filter: filter.as_deref().map(rewrite).transpose()?.map(Box::new),
5897                order_by: order_by
5898                    .iter()
5899                    .map(|sort| {
5900                        Ok(SortExpr::new(
5901                            rewrite(&sort.expr)?,
5902                            sort.asc,
5903                            sort.nulls_first,
5904                        ))
5905                    })
5906                    .collect::<Result<Vec<_>, PlannerError>>()?,
5907                over: None,
5908            }
5909        }
5910        TypedExprKind::BinaryOp { left, op, right } => TypedExprKind::BinaryOp {
5911            left: Box::new(rewrite(left)?),
5912            op: *op,
5913            right: Box::new(rewrite(right)?),
5914        },
5915        TypedExprKind::UnaryOp { op, operand } => TypedExprKind::UnaryOp {
5916            op: *op,
5917            operand: Box::new(rewrite(operand)?),
5918        },
5919        TypedExprKind::Cast {
5920            expr: inner,
5921            target_type,
5922        } => TypedExprKind::Cast {
5923            expr: Box::new(rewrite(inner)?),
5924            target_type: target_type.clone(),
5925        },
5926        TypedExprKind::TryCast {
5927            expr: inner,
5928            target_type,
5929        } => TypedExprKind::TryCast {
5930            expr: Box::new(rewrite(inner)?),
5931            target_type: target_type.clone(),
5932        },
5933        TypedExprKind::Between {
5934            expr: inner,
5935            low,
5936            high,
5937            negated,
5938        } => TypedExprKind::Between {
5939            expr: Box::new(rewrite(inner)?),
5940            low: Box::new(rewrite(low)?),
5941            high: Box::new(rewrite(high)?),
5942            negated: *negated,
5943        },
5944        TypedExprKind::Like {
5945            expr: inner,
5946            pattern,
5947            escape,
5948            negated,
5949            kind,
5950        } => TypedExprKind::Like {
5951            expr: Box::new(rewrite(inner)?),
5952            pattern: Box::new(rewrite(pattern)?),
5953            escape: escape.as_deref().map(rewrite).transpose()?.map(Box::new),
5954            negated: *negated,
5955            kind: *kind,
5956        },
5957        TypedExprKind::InList {
5958            expr: inner,
5959            list,
5960            negated,
5961        } => TypedExprKind::InList {
5962            expr: Box::new(rewrite(inner)?),
5963            list: list.iter().map(rewrite).collect::<Result<Vec<_>, _>>()?,
5964            negated: *negated,
5965        },
5966        TypedExprKind::IsNull {
5967            expr: inner,
5968            negated,
5969        } => TypedExprKind::IsNull {
5970            expr: Box::new(rewrite(inner)?),
5971            negated: *negated,
5972        },
5973        _ => return Ok(expr.clone()),
5974    };
5975    Ok(TypedExpr {
5976        kind,
5977        resolved_type: expr.resolved_type.clone(),
5978        span: expr.span,
5979    })
5980}
5981
5982/// Rebind an outer ORDER BY expression to the visible projection schema.
5983///
5984/// Projection aliases have already been substituted before type inference, so
5985/// expression identity is enough to map both aliases and repeated expressions
5986/// without making aliases visible to WHERE/GROUP BY/window specifications.
5987fn rewrite_expr_for_projected_output(
5988    expr: &TypedExpr,
5989    projection: &Projection,
5990    output_schema: &[ColumnMetadata],
5991) -> Result<TypedExpr, PlannerError> {
5992    let index = match projection {
5993        Projection::Columns(columns) => columns
5994            .iter()
5995            .position(|column| expr_key(&column.expr) == expr_key(expr)),
5996        Projection::All(_) => match &expr.kind {
5997            TypedExprKind::ColumnRef { column_index, .. }
5998                if *column_index < output_schema.len() =>
5999            {
6000                Some(*column_index)
6001            }
6002            _ => None,
6003        },
6004    };
6005    let Some(index) = index else {
6006        return Err(PlannerError::invalid_expression(
6007            "ORDER BY expression must appear in the SELECT projection for window queries"
6008                .to_string(),
6009        ));
6010    };
6011    let column = output_schema.get(index).ok_or_else(|| {
6012        PlannerError::invalid_expression(
6013            "ORDER BY projection index is outside the output schema".to_string(),
6014        )
6015    })?;
6016    Ok(TypedExpr::column_ref(
6017        "__project__".to_string(),
6018        column.name.clone(),
6019        index,
6020        column.data_type.clone(),
6021        expr.span,
6022    ))
6023}
6024
6025fn map_join_type(join_type: crate::ast::dml::JoinType) -> JoinType {
6026    match join_type {
6027        crate::ast::dml::JoinType::Inner => JoinType::Inner,
6028        crate::ast::dml::JoinType::Left => JoinType::Left,
6029        crate::ast::dml::JoinType::Right => JoinType::Right,
6030        crate::ast::dml::JoinType::Full => JoinType::Full,
6031        crate::ast::dml::JoinType::Cross => JoinType::Cross,
6032    }
6033}
6034
6035struct FoundScopedColumn {
6036    table: String,
6037    index: usize,
6038    ty: ResolvedType,
6039    partner_indices: Vec<usize>,
6040}
6041
6042fn find_scoped_column(
6043    scope: &[ScopedTable],
6044    column: &str,
6045    span: crate::ast::Span,
6046) -> Result<FoundScopedColumn, PlannerError> {
6047    let mut matches = Vec::new();
6048    for table in scope {
6049        if table.hidden_unqualified_columns.contains(column) {
6050            continue;
6051        }
6052        if let Some(local_idx) = table.table.get_column_index(column) {
6053            let meta = &table.table.columns[local_idx];
6054            matches.push(FoundScopedColumn {
6055                table: table.table.name.clone(),
6056                index: table.start_index + local_idx,
6057                ty: meta.data_type.clone(),
6058                partner_indices: table
6059                    .merged_column_partners
6060                    .get(column)
6061                    .cloned()
6062                    .unwrap_or_default(),
6063            });
6064        }
6065    }
6066    match matches.len() {
6067        0 => Err(PlannerError::column_not_found(column, "JOIN input", span)),
6068        1 => Ok(matches.remove(0)),
6069        _ => Err(PlannerError::ambiguous_column(
6070            column,
6071            scope.iter().map(|s| s.table.name.clone()).collect(),
6072            span,
6073        )),
6074    }
6075}
6076
6077fn merged_scoped_column_expr(
6078    found: &FoundScopedColumn,
6079    column: &str,
6080    span: crate::ast::Span,
6081) -> TypedExpr {
6082    let own = TypedExpr::column_ref(
6083        found.table.clone(),
6084        column.to_string(),
6085        found.index,
6086        found.ty.clone(),
6087        span,
6088    );
6089    if found.partner_indices.is_empty() {
6090        return own;
6091    }
6092
6093    let mut args = Vec::with_capacity(found.partner_indices.len() + 1);
6094    args.push(own);
6095    args.extend(found.partner_indices.iter().map(|&index| {
6096        TypedExpr::column_ref(
6097            found.table.clone(),
6098            column.to_string(),
6099            index,
6100            found.ty.clone(),
6101            span,
6102        )
6103    }));
6104    TypedExpr {
6105        kind: TypedExprKind::FunctionCall {
6106            name: "coalesce".to_string(),
6107            args,
6108            distinct: false,
6109            star: false,
6110            filter: None,
6111            order_by: Vec::new(),
6112            over: None,
6113        },
6114        resolved_type: found.ty.clone(),
6115        span,
6116    }
6117}
6118
6119fn projection_schema(
6120    projection: &Projection,
6121    input_schema: &[ColumnMetadata],
6122) -> Vec<ColumnMetadata> {
6123    match projection {
6124        Projection::All(names) => names
6125            .iter()
6126            .enumerate()
6127            .map(|(idx, name)| {
6128                let ty = (names.len() == input_schema.len())
6129                    .then(|| input_schema.get(idx))
6130                    .flatten()
6131                    .or_else(|| input_schema.iter().find(|col| &col.name == name))
6132                    .map(|col| col.data_type.clone())
6133                    .unwrap_or(ResolvedType::Null);
6134                ColumnMetadata::new(name.clone(), ty)
6135            })
6136            .collect(),
6137        Projection::Columns(columns) => columns
6138            .iter()
6139            .enumerate()
6140            .map(|(idx, col)| {
6141                let name = col
6142                    .alias
6143                    .clone()
6144                    .or_else(|| match &col.expr.kind {
6145                        TypedExprKind::ColumnRef { column, .. } => Some(column.clone()),
6146                        // A USING/NATURAL common column is planned as
6147                        // COALESCE(left, right); it still names the merged
6148                        // column, not an anonymous expression.
6149                        TypedExprKind::FunctionCall { name, args, .. }
6150                            if name == "coalesce" && !args.is_empty() =>
6151                        {
6152                            let first_column = match &args[0].kind {
6153                                TypedExprKind::ColumnRef { column, .. } => Some(column),
6154                                _ => None,
6155                            };
6156                            first_column
6157                                .filter(|column| {
6158                                    args.iter().all(|arg| {
6159                                        matches!(
6160                                            &arg.kind,
6161                                            TypedExprKind::ColumnRef { column: other, .. }
6162                                                if other == *column
6163                                        )
6164                                    })
6165                                })
6166                                .cloned()
6167                        }
6168                        _ => None,
6169                    })
6170                    .unwrap_or_else(|| format!("col_{idx}"));
6171                ColumnMetadata::new(name, col.expr.resolved_type.clone())
6172            })
6173            .collect(),
6174    }
6175}
6176
6177fn visible_wildcard_columns(schema: &[ColumnMetadata], scope: &[ScopedTable]) -> Vec<String> {
6178    schema
6179        .iter()
6180        .enumerate()
6181        .filter(|(index, column)| {
6182            !scope.iter().any(|table| {
6183                *index >= table.start_index
6184                    && *index < table.start_index + table.table.columns.len()
6185                    && table.hidden_unqualified_columns.contains(&column.name)
6186            })
6187        })
6188        .map(|(_, column)| column.name.clone())
6189        .collect()
6190}
6191
6192/// Output schema and name scope of a join over `left` and `right`.
6193///
6194/// Shared by the plain and LATERAL join builders so both expose the same
6195/// USING/NATURAL column merging.
6196fn combine_join_shape(
6197    left: &PlannedRelation,
6198    right: &PlannedRelation,
6199    using: Option<&[String]>,
6200) -> (Vec<ColumnMetadata>, Vec<ScopedTable>) {
6201    let mut schema = left.schema.clone();
6202    schema.extend(right.schema.clone());
6203    let mut scope = left.scope.clone();
6204    let mut right_scope = right.scope.clone();
6205    if let Some(columns) = using {
6206        // The right-hand copy of a common column stops being an unqualified
6207        // candidate, and the surviving left-hand column records where its
6208        // partner lives so that an unqualified reference can merge the two.
6209        for column in columns {
6210            let right_index = right_scope.iter().find_map(|table| {
6211                table
6212                    .table
6213                    .get_column_index(column)
6214                    .map(|index| table.start_index + index)
6215            });
6216            let Some(right_index) = right_index else {
6217                continue;
6218            };
6219            for table in &mut scope {
6220                if table.table.get_column_index(column).is_some() {
6221                    table.merge_column_with(column, right_index);
6222                }
6223            }
6224        }
6225        for table in &mut right_scope {
6226            table.hide_unqualified_columns(columns);
6227        }
6228    }
6229    scope.extend(right_scope);
6230    (schema, scope)
6231}
6232
6233/// Whether this FROM item is evaluated once per row of everything to its left.
6234///
6235/// An explicit `LATERAL` marks a derived table; a table function is implicitly
6236/// lateral because its arguments may reference the preceding items (D2).
6237fn from_item_is_lateral(item: &FromItem) -> bool {
6238    match item {
6239        FromItem::Derived { lateral, .. } => *lateral,
6240        FromItem::Function { .. } => true,
6241        FromItem::Table { .. } | FromItem::Join { .. } => false,
6242    }
6243}
6244
6245/// Scope a LATERAL item sees, addressed against the outer row the executor
6246/// builds for it: the left join row followed by the enclosing outer row.
6247///
6248/// `base` is the output offset the left relation was planned at, so its scope
6249/// is rebased to 0; the enclosing scope shifts past the left row. Neither side
6250/// changes `scope_level` here, because planning the lateral relation applies
6251/// [`offset_scope`] once and that is the single level it is nested by.
6252fn lateral_outer_scope(
6253    left_scope: &[ScopedTable],
6254    base: usize,
6255    left_width: usize,
6256    outer_scope: &[ScopedTable],
6257) -> Vec<ScopedTable> {
6258    debug_assert!(
6259        left_scope.iter().all(|table| table.start_index >= base),
6260        "a FROM item's left sibling scope must start at the join's own base"
6261    );
6262    left_scope
6263        .iter()
6264        .cloned()
6265        .map(|mut table| {
6266            table.start_index -= base;
6267            table
6268        })
6269        .chain(outer_scope.iter().cloned().map(|mut table| {
6270            table.start_index += left_width;
6271            table
6272        }))
6273        .collect()
6274}
6275
6276/// Apply a relation alias column-name list to `schema` in place.
6277///
6278/// Exact arity is required for every relation kind, and a repeated name is
6279/// rejected (issue #151, D8).
6280fn apply_alias_columns(
6281    alias: &str,
6282    columns: &[String],
6283    schema: &mut [ColumnMetadata],
6284    span: crate::ast::Span,
6285) -> Result<(), PlannerError> {
6286    if columns.is_empty() {
6287        return Ok(());
6288    }
6289    if columns.len() != schema.len() {
6290        return Err(PlannerError::table_alias_column_count_mismatch(
6291            alias,
6292            columns.len(),
6293            schema.len(),
6294            span,
6295        ));
6296    }
6297    let mut names = HashSet::new();
6298    for name in columns {
6299        if !names.insert(name) {
6300            return Err(PlannerError::invalid_expression(format!(
6301                "relation alias '{alias}' declares column '{name}' more than once"
6302            )));
6303        }
6304    }
6305    for (column, name) in schema.iter_mut().zip(columns) {
6306        column.name.clone_from(name);
6307    }
6308    Ok(())
6309}
6310
6311fn offset_scope(scope: &[ScopedTable], offset: usize) -> Vec<ScopedTable> {
6312    scope
6313        .iter()
6314        .cloned()
6315        .map(|mut table| {
6316            table.start_index += offset;
6317            table.scope_level += 1;
6318            table
6319        })
6320        .collect()
6321}
6322
6323fn natural_join_columns(
6324    left_schema: &[ColumnMetadata],
6325    right_schema: &[ColumnMetadata],
6326) -> Vec<String> {
6327    // Pairing every left column against every right column is quadratic in the
6328    // join width, so the right side is hashed once. Iteration stays over the
6329    // left schema because the common columns keep the left table's order.
6330    let right_names = right_schema
6331        .iter()
6332        .map(|column| column.name.as_str())
6333        .collect::<HashSet<_>>();
6334    left_schema
6335        .iter()
6336        .filter(|left| right_names.contains(left.name.as_str()))
6337        .map(|column| column.name.clone())
6338        .collect()
6339}
6340
6341fn install_base_projection(plan: &mut LogicalPlan, projection: &Projection) {
6342    match plan {
6343        LogicalPlan::Scan {
6344            projection: scan_projection,
6345            ..
6346        } => *scan_projection = projection.clone(),
6347        LogicalPlan::Filter { input, .. } => install_base_projection(input, projection),
6348        _ => {}
6349    }
6350}
6351
6352fn is_aggregate_function(name: &str) -> bool {
6353    matches!(
6354        name.to_ascii_lowercase().as_str(),
6355        "count"
6356            | "sum"
6357            | "total"
6358            | "avg"
6359            | "min"
6360            | "max"
6361            | "group_concat"
6362            | "string_agg"
6363            | "json_group_array"
6364            | "array_agg"
6365            | "json_group_object"
6366            | "jsonb_agg"
6367            | "jsonb_object_agg"
6368            | "percentile_disc"
6369            | "percentile_cont"
6370    ) || type_checker::is_portable_aggregate_name(&name.to_ascii_lowercase())
6371}
6372
6373fn expr_key(expr: &TypedExpr) -> String {
6374    format!("{:?}", expr.kind)
6375}
6376
6377/// Structural signature for DISTINCT ON key matching (D2).
6378///
6379/// `expr_key` embeds the source spans of nested sub-expressions, so the same
6380/// compound expression written once in the ON list and once in ORDER BY would
6381/// never compare equal. This signature erases every rendered
6382/// `span: Span { .. }` segment first. The eraser only rewrites segments that
6383/// match the exact derived-Debug shape (digits and fixed punctuation), so a
6384/// string literal that happens to contain the marker text is left untouched
6385/// and still compares consistently on both sides.
6386fn distinct_on_expr_signature(expr: &TypedExpr) -> String {
6387    const MARKER: &str = "span: Span { start: Location { line: ";
6388    let rendered = format!("{:?}", expr.kind);
6389    let mut result = String::with_capacity(rendered.len());
6390    let mut rest = rendered.as_str();
6391    while let Some(position) = rest.find(MARKER) {
6392        let after = &rest[position + MARKER.len()..];
6393        match debug_span_tail_length(after) {
6394            Some(consumed) => {
6395                result.push_str(&rest[..position]);
6396                result.push_str("span: _");
6397                rest = &after[consumed..];
6398            }
6399            None => {
6400                let keep = position + MARKER.len();
6401                result.push_str(&rest[..keep]);
6402                rest = &rest[keep..];
6403            }
6404        }
6405    }
6406    result.push_str(rest);
6407    result
6408}
6409
6410/// Length of `<digits>, column: <digits> }, end: Location { line: <digits>,
6411/// column: <digits> } }` at the start of `input`, or `None` when the text does
6412/// not match that exact derived-Debug shape.
6413fn debug_span_tail_length(input: &str) -> Option<usize> {
6414    fn digits(input: &str, offset: &mut usize) -> bool {
6415        let start = *offset;
6416        while input
6417            .as_bytes()
6418            .get(*offset)
6419            .is_some_and(u8::is_ascii_digit)
6420        {
6421            *offset += 1;
6422        }
6423        *offset > start
6424    }
6425    fn literal(input: &str, offset: &mut usize, expected: &str) -> bool {
6426        if input[*offset..].starts_with(expected) {
6427            *offset += expected.len();
6428            true
6429        } else {
6430            false
6431        }
6432    }
6433    let mut offset = 0;
6434    (digits(input, &mut offset)
6435        && literal(input, &mut offset, ", column: ")
6436        && digits(input, &mut offset)
6437        && literal(input, &mut offset, " }, end: Location { line: ")
6438        && digits(input, &mut offset)
6439        && literal(input, &mut offset, ", column: ")
6440        && digits(input, &mut offset)
6441        && literal(input, &mut offset, " } }"))
6442    .then_some(offset)
6443}
6444
6445/// Verify the DISTINCT ON / ORDER BY prefix contract (D2) and synthesize the
6446/// complete deterministic sort specification for [`LogicalPlan::DistinctOn`].
6447///
6448/// Returns `(key_count, order_by)` where the leading `key_count` entries cover
6449/// every deduplicated ON key: the user's matching ORDER BY prefix (any
6450/// permutation, keeping the user's direction), then any keys the user ORDER BY
6451/// did not reach as implicit ASC NULLS LAST (D3). The user's non-key tail
6452/// follows, and every input column is appended in schema order as an ASC NULLS
6453/// LAST tie-breaker (D4) so the surviving row of each key group never depends
6454/// on the physical input order.
6455///
6456/// Invariant relied on by `FETCH ... WITH TIES` (D13): the leading
6457/// `user_order_by.len()` entries of the returned specification are exactly the
6458/// user's ORDER BY, in the user's order. Implicit ON keys are only synthesized
6459/// when the user ORDER BY has no non-key tail (a tail plus an unreached key is
6460/// a D2 error), so the two groups can never interleave.
6461fn build_distinct_on_sort_spec(
6462    key_exprs: Vec<TypedExpr>,
6463    user_order_by: Vec<SortExpr>,
6464    base_schema: &[ColumnMetadata],
6465    fallback_span: crate::ast::Span,
6466) -> Result<(usize, Vec<SortExpr>), PlannerError> {
6467    let key_signatures: Vec<String> = key_exprs.iter().map(distinct_on_expr_signature).collect();
6468    let mut consumed = vec![false; key_exprs.len()];
6469    let mut prefix: Vec<SortExpr> = Vec::new();
6470    let mut tail: Vec<SortExpr> = Vec::new();
6471    let mut prefix_ended = false;
6472    for sort in user_order_by {
6473        let signature = distinct_on_expr_signature(&sort.expr);
6474        if let Some(index) = key_signatures
6475            .iter()
6476            .position(|candidate| candidate == &signature)
6477        {
6478            if prefix_ended {
6479                // D2: an ON key reappears after a non-key ORDER BY item
6480                // already ended the prefix (PostgreSQL 42P10).
6481                return Err(PlannerError::distinct_on_order_by_mismatch(sort.expr.span));
6482            }
6483            consumed[index] = true;
6484            prefix.push(sort);
6485        } else {
6486            prefix_ended = true;
6487            tail.push(sort);
6488        }
6489    }
6490    let mut implicit: Vec<SortExpr> = Vec::new();
6491    for (index, key) in key_exprs.into_iter().enumerate() {
6492        if consumed[index] {
6493            continue;
6494        }
6495        if prefix_ended {
6496            // D2: with non-key tail items present, an ON key the prefix never
6497            // reached leaves the deduplication order ambiguous.
6498            return Err(PlannerError::distinct_on_order_by_mismatch(key.span));
6499        }
6500        implicit.push(SortExpr::new(key, true, false));
6501    }
6502    let key_count = prefix.len() + implicit.len();
6503    let mut order_by = prefix;
6504    order_by.append(&mut implicit);
6505    order_by.append(&mut tail);
6506    let mut seen_columns: HashSet<usize> = order_by
6507        .iter()
6508        .filter_map(|sort| match &sort.expr.kind {
6509            TypedExprKind::ColumnRef { column_index, .. } => Some(*column_index),
6510            _ => None,
6511        })
6512        .collect();
6513    for (index, column) in base_schema.iter().enumerate() {
6514        if seen_columns.insert(index) {
6515            order_by.push(SortExpr::new(
6516                TypedExpr::column_ref(
6517                    String::new(),
6518                    column.name.clone(),
6519                    index,
6520                    column.data_type.clone(),
6521                    fallback_span,
6522                ),
6523                true,
6524                false,
6525            ));
6526        }
6527    }
6528    Ok((key_count, order_by))
6529}
6530
6531/// Ordering changes the result only for these aggregates (issue #148, D3).
6532fn is_order_sensitive_aggregate(name: &str) -> bool {
6533    matches!(
6534        name.to_ascii_lowercase().as_str(),
6535        "group_concat"
6536            | "string_agg"
6537            | "percentile_disc"
6538            | "percentile_cont"
6539            | "mode"
6540            | "first"
6541            | "last"
6542    )
6543}
6544
6545fn sort_exprs_key(order_by: &[SortExpr]) -> Option<String> {
6546    if order_by.is_empty() {
6547        return None;
6548    }
6549    Some(
6550        order_by
6551            .iter()
6552            .map(|sort| format!("{}|{}|{}", expr_key(&sort.expr), sort.asc, sort.nulls_first))
6553            .collect::<Vec<_>>()
6554            .join(","),
6555    )
6556}
6557
6558#[allow(clippy::too_many_arguments)]
6559fn aggregate_signature(
6560    name: &str,
6561    distinct: bool,
6562    star: bool,
6563    arg: Option<&TypedExpr>,
6564    separator: Option<&String>,
6565    _expr: &TypedExpr,
6566    filter: Option<&TypedExpr>,
6567    order_by: &[SortExpr],
6568) -> AggregateSignature {
6569    AggregateSignature {
6570        name: canonical_aggregate_name(name),
6571        distinct,
6572        star,
6573        arg_key: arg.map(expr_key),
6574        extra_arg_keys: Vec::new(),
6575        separator: separator.cloned(),
6576        filter_key: filter.map(expr_key),
6577        order_key: sort_exprs_key(order_by),
6578    }
6579}
6580
6581fn canonical_aggregate_name(name: &str) -> String {
6582    match name.to_ascii_lowercase().as_str() {
6583        "variance" | "var_samp" => "var_samp".into(),
6584        "stddev" | "stddev_samp" => "stddev_samp".into(),
6585        "min_by" => "arg_min".into(),
6586        "max_by" => "arg_max".into(),
6587        lower => lower.into(),
6588    }
6589}
6590
6591fn build_group_key_map(group_keys: &[TypedExpr]) -> HashMap<String, usize> {
6592    let mut map = HashMap::new();
6593    for (idx, key) in group_keys.iter().enumerate() {
6594        map.insert(expr_key(key), idx);
6595    }
6596    map
6597}
6598
6599fn build_aggregate_map(aggregates: &[AggregateExpr]) -> HashMap<AggregateSignature, usize> {
6600    let mut map = HashMap::new();
6601    for (idx, agg) in aggregates.iter().enumerate() {
6602        let (name, separator, star, arg) = match &agg.function {
6603            AggregateFunction::Count => (
6604                "count".to_string(),
6605                None,
6606                agg.arg.is_none(),
6607                agg.arg.as_ref(),
6608            ),
6609            AggregateFunction::Sum => ("sum".to_string(), None, false, agg.arg.as_ref()),
6610            AggregateFunction::Total => ("total".to_string(), None, false, agg.arg.as_ref()),
6611            AggregateFunction::Avg => ("avg".to_string(), None, false, agg.arg.as_ref()),
6612            AggregateFunction::Min => ("min".to_string(), None, false, agg.arg.as_ref()),
6613            AggregateFunction::Max => ("max".to_string(), None, false, agg.arg.as_ref()),
6614            AggregateFunction::GroupConcat { separator } => (
6615                "group_concat".to_string(),
6616                separator.clone(),
6617                false,
6618                agg.arg.as_ref(),
6619            ),
6620            AggregateFunction::StringAgg { separator } => (
6621                "string_agg".to_string(),
6622                separator.clone(),
6623                false,
6624                agg.arg.as_ref(),
6625            ),
6626            AggregateFunction::ArrayAgg => ("array_agg".to_string(), None, false, agg.arg.as_ref()),
6627            AggregateFunction::JsonGroupArray => (
6628                "json_group_array".to_string(),
6629                None,
6630                false,
6631                agg.arg.as_ref(),
6632            ),
6633            AggregateFunction::JsonGroupObject => (
6634                "json_group_object".to_string(),
6635                None,
6636                false,
6637                agg.arg.as_ref(),
6638            ),
6639            AggregateFunction::JsonbAgg => ("jsonb_agg".to_string(), None, false, agg.arg.as_ref()),
6640            AggregateFunction::JsonbObjectAgg => (
6641                "jsonb_object_agg".to_string(),
6642                None,
6643                false,
6644                agg.arg.as_ref(),
6645            ),
6646            AggregateFunction::PercentileDisc { fraction } => (
6647                "percentile_disc".to_string(),
6648                Some(format!("{fraction:?}")),
6649                false,
6650                None,
6651            ),
6652            AggregateFunction::PercentileCont { fraction } => (
6653                "percentile_cont".to_string(),
6654                Some(format!("{fraction:?}")),
6655                false,
6656                None,
6657            ),
6658            AggregateFunction::QuantileCont { fraction } => (
6659                "quantile_cont".to_string(),
6660                Some(format!("{fraction:?}")),
6661                false,
6662                agg.arg.as_ref(),
6663            ),
6664            AggregateFunction::Variance { sample } => (
6665                if *sample { "var_samp" } else { "var_pop" }.to_string(),
6666                None,
6667                false,
6668                agg.arg.as_ref(),
6669            ),
6670            AggregateFunction::Stddev { sample } => (
6671                if *sample { "stddev_samp" } else { "stddev_pop" }.to_string(),
6672                None,
6673                false,
6674                agg.arg.as_ref(),
6675            ),
6676            AggregateFunction::Covariance { sample } => (
6677                if *sample { "covar_samp" } else { "covar_pop" }.to_string(),
6678                None,
6679                false,
6680                agg.arg.as_ref(),
6681            ),
6682            AggregateFunction::Corr => ("corr".into(), None, false, agg.arg.as_ref()),
6683            AggregateFunction::Median => ("median".into(), None, false, agg.arg.as_ref()),
6684            AggregateFunction::Mode => (
6685                "mode".into(),
6686                None,
6687                false,
6688                agg.order_by.is_empty().then_some(()).and(agg.arg.as_ref()),
6689            ),
6690            AggregateFunction::RegrCount => ("regr_count".into(), None, false, agg.arg.as_ref()),
6691            AggregateFunction::RegrAvgX => ("regr_avgx".into(), None, false, agg.arg.as_ref()),
6692            AggregateFunction::RegrAvgY => ("regr_avgy".into(), None, false, agg.arg.as_ref()),
6693            AggregateFunction::RegrSxx => ("regr_sxx".into(), None, false, agg.arg.as_ref()),
6694            AggregateFunction::RegrSyy => ("regr_syy".into(), None, false, agg.arg.as_ref()),
6695            AggregateFunction::RegrSxy => ("regr_sxy".into(), None, false, agg.arg.as_ref()),
6696            AggregateFunction::RegrSlope => ("regr_slope".into(), None, false, agg.arg.as_ref()),
6697            AggregateFunction::RegrIntercept => {
6698                ("regr_intercept".into(), None, false, agg.arg.as_ref())
6699            }
6700            AggregateFunction::RegrR2 => ("regr_r2".into(), None, false, agg.arg.as_ref()),
6701            AggregateFunction::AnyValue => ("any_value".into(), None, false, agg.arg.as_ref()),
6702            AggregateFunction::First => ("first".into(), None, false, agg.arg.as_ref()),
6703            AggregateFunction::Last => ("last".into(), None, false, agg.arg.as_ref()),
6704            AggregateFunction::ArgMin => ("arg_min".into(), None, false, agg.arg.as_ref()),
6705            AggregateFunction::ArgMax => ("arg_max".into(), None, false, agg.arg.as_ref()),
6706            AggregateFunction::BitAnd => ("bit_and".into(), None, false, agg.arg.as_ref()),
6707            AggregateFunction::BitOr => ("bit_or".into(), None, false, agg.arg.as_ref()),
6708            AggregateFunction::BitXor => ("bit_xor".into(), None, false, agg.arg.as_ref()),
6709            AggregateFunction::BoolAnd => ("bool_and".into(), None, false, agg.arg.as_ref()),
6710            AggregateFunction::BoolOr => ("bool_or".into(), None, false, agg.arg.as_ref()),
6711        };
6712        let signature = AggregateSignature {
6713            name,
6714            distinct: agg.distinct,
6715            star,
6716            arg_key: arg.map(expr_key),
6717            extra_arg_keys: agg.extra_args.iter().map(expr_key).collect(),
6718            separator,
6719            filter_key: agg.filter.as_ref().map(expr_key),
6720            order_key: sort_exprs_key(&agg.order_by),
6721        };
6722        map.insert(signature, idx);
6723    }
6724    map
6725}
6726
6727/// Hidden aggregate output column carrying the grouping-set mask (issue #149).
6728pub(crate) const GROUPING_ID_COLUMN: &str = "__grouping_id";
6729/// PostgreSQL-compatible bound on expanded grouping sets (D6).
6730const MAX_GROUPING_SETS: usize = 4096;
6731/// CUBE with more than 12 columns always exceeds `MAX_GROUPING_SETS`.
6732const MAX_CUBE_COLUMNS: usize = 12;
6733/// The grouping-id mask is a BIGINT, so 63 keys/arguments at most (D4).
6734const MAX_GROUPING_KEYS: usize = 63;
6735
6736/// GROUP BY expansion result (issue #149).
6737struct ExpandedGroupBy {
6738    group_keys: Vec<TypedExpr>,
6739    grouping_sets: Option<Vec<u64>>,
6740}
6741
6742fn grouping_full_mask(key_count: usize) -> u64 {
6743    if key_count == 0 {
6744        0
6745    } else {
6746        (1u64 << key_count) - 1
6747    }
6748}
6749
6750fn is_grouping_function(name: &str) -> bool {
6751    name.eq_ignore_ascii_case("grouping") || name.eq_ignore_ascii_case("grouping_id")
6752}
6753
6754/// Context for lowering GROUPING/GROUPING_ID onto `__grouping_id` (D4/D5).
6755struct GroupingRewrite {
6756    /// Group-key expression identity -> union key position.
6757    key_index: HashMap<String, usize>,
6758    key_count: usize,
6759    /// Output position of `__grouping_id` (after keys and aggregates).
6760    gid_index: usize,
6761    /// Whether the plan actually carries grouping sets; a plain GROUP BY
6762    /// still accepts GROUPING but every call folds to constant 0.
6763    sets_present: bool,
6764}
6765
6766impl GroupingRewrite {
6767    fn new(
6768        group_keys: &[TypedExpr],
6769        aggregates: &[AggregateExpr],
6770        grouping_sets: &Option<Vec<u64>>,
6771    ) -> Self {
6772        let key_index = group_keys
6773            .iter()
6774            .enumerate()
6775            .map(|(index, key)| (expr_key(key), index))
6776            .collect();
6777        Self {
6778            key_index,
6779            key_count: group_keys.len(),
6780            gid_index: group_keys.len() + aggregates.len(),
6781            sets_present: grouping_sets.is_some(),
6782        }
6783    }
6784}
6785
6786/// AST-level detection of GROUPING/GROUPING_ID calls (D5 placement rules).
6787fn expr_contains_grouping(expr: &crate::ast::expr::Expr) -> bool {
6788    use crate::ast::expr::ExprKind;
6789
6790    match &expr.kind {
6791        ExprKind::FunctionCall {
6792            name,
6793            args,
6794            order_by,
6795            within_group,
6796            filter,
6797            over,
6798            ..
6799        } => {
6800            is_grouping_function(name)
6801                || args.iter().any(expr_contains_grouping)
6802                || order_by
6803                    .iter()
6804                    .any(|sort| expr_contains_grouping(&sort.expr))
6805                || within_group
6806                    .iter()
6807                    .any(|sort| expr_contains_grouping(&sort.expr))
6808                || filter.as_deref().is_some_and(expr_contains_grouping)
6809                || over.as_ref().is_some_and(|window| {
6810                    window.partition_by.iter().any(expr_contains_grouping)
6811                        || window
6812                            .order_by
6813                            .iter()
6814                            .any(|sort| expr_contains_grouping(&sort.expr))
6815                })
6816        }
6817        ExprKind::BinaryOp { left, right, .. } => {
6818            expr_contains_grouping(left) || expr_contains_grouping(right)
6819        }
6820        ExprKind::UnaryOp { operand, .. } => expr_contains_grouping(operand),
6821        ExprKind::TruthPredicate { expr, .. } => expr_contains_grouping(expr),
6822        ExprKind::IsDistinctFrom { left, right, .. } => {
6823            expr_contains_grouping(left) || expr_contains_grouping(right)
6824        }
6825        ExprKind::Row { items } => items.iter().any(expr_contains_grouping),
6826        ExprKind::Case {
6827            operand,
6828            branches,
6829            else_expr,
6830        } => {
6831            operand.as_deref().is_some_and(expr_contains_grouping)
6832                || branches.iter().any(|branch| {
6833                    expr_contains_grouping(&branch.when) || expr_contains_grouping(&branch.then)
6834                })
6835                || else_expr.as_deref().is_some_and(expr_contains_grouping)
6836        }
6837        ExprKind::Cast { expr, .. } | ExprKind::TryCast { expr, .. } => {
6838            expr_contains_grouping(expr)
6839        }
6840        ExprKind::Between {
6841            expr, low, high, ..
6842        } => {
6843            expr_contains_grouping(expr)
6844                || expr_contains_grouping(low)
6845                || expr_contains_grouping(high)
6846        }
6847        ExprKind::Like {
6848            expr,
6849            pattern,
6850            escape,
6851            ..
6852        } => {
6853            expr_contains_grouping(expr)
6854                || expr_contains_grouping(pattern)
6855                || escape.as_deref().is_some_and(expr_contains_grouping)
6856        }
6857        ExprKind::InList { expr, list, .. } => {
6858            expr_contains_grouping(expr) || list.iter().any(expr_contains_grouping)
6859        }
6860        ExprKind::IsNull { expr, .. } => expr_contains_grouping(expr),
6861        ExprKind::ScalarSubquery { .. }
6862        | ExprKind::InSubquery { .. }
6863        | ExprKind::Exists { .. }
6864        | ExprKind::Quantified { .. }
6865        | ExprKind::Literal { .. }
6866        | ExprKind::VectorLiteral { .. }
6867        | ExprKind::ColumnRef { .. } => false,
6868    }
6869}
6870
6871fn typed_expr_contains_grouping(expr: &TypedExpr) -> bool {
6872    match &expr.kind {
6873        TypedExprKind::FunctionCall {
6874            name,
6875            args,
6876            filter,
6877            order_by,
6878            over,
6879            ..
6880        } => {
6881            is_grouping_function(name)
6882                || args.iter().any(typed_expr_contains_grouping)
6883                || filter.as_deref().is_some_and(typed_expr_contains_grouping)
6884                || order_by
6885                    .iter()
6886                    .any(|sort| typed_expr_contains_grouping(&sort.expr))
6887                || over.as_ref().is_some_and(|window| {
6888                    window.partition_by.iter().any(typed_expr_contains_grouping)
6889                        || window
6890                            .order_by
6891                            .iter()
6892                            .any(|sort| typed_expr_contains_grouping(&sort.expr))
6893                })
6894        }
6895        TypedExprKind::BinaryOp { left, right, .. } => {
6896            typed_expr_contains_grouping(left) || typed_expr_contains_grouping(right)
6897        }
6898        TypedExprKind::UnaryOp { operand, .. } => typed_expr_contains_grouping(operand),
6899        TypedExprKind::Cast { expr, .. } | TypedExprKind::TryCast { expr, .. } => {
6900            typed_expr_contains_grouping(expr)
6901        }
6902        TypedExprKind::Case {
6903            operand,
6904            branches,
6905            else_expr,
6906        } => {
6907            operand.as_deref().is_some_and(typed_expr_contains_grouping)
6908                || branches.iter().any(|branch| {
6909                    typed_expr_contains_grouping(&branch.when)
6910                        || typed_expr_contains_grouping(&branch.then)
6911                })
6912                || else_expr
6913                    .as_deref()
6914                    .is_some_and(typed_expr_contains_grouping)
6915        }
6916        TypedExprKind::Between {
6917            expr, low, high, ..
6918        } => {
6919            typed_expr_contains_grouping(expr)
6920                || typed_expr_contains_grouping(low)
6921                || typed_expr_contains_grouping(high)
6922        }
6923        TypedExprKind::Like {
6924            expr,
6925            pattern,
6926            escape,
6927            ..
6928        } => {
6929            typed_expr_contains_grouping(expr)
6930                || typed_expr_contains_grouping(pattern)
6931                || escape
6932                    .as_ref()
6933                    .is_some_and(|inner| typed_expr_contains_grouping(inner))
6934        }
6935        TypedExprKind::InList { expr, list, .. } => {
6936            typed_expr_contains_grouping(expr) || list.iter().any(typed_expr_contains_grouping)
6937        }
6938        TypedExprKind::IsNull { expr, .. } => typed_expr_contains_grouping(expr),
6939        TypedExprKind::InSubquery { expr, .. } => typed_expr_contains_grouping(expr),
6940        TypedExprKind::Quantified { expr, .. } => typed_expr_contains_grouping(expr),
6941        TypedExprKind::Literal(_)
6942        | TypedExprKind::VectorLiteral(_)
6943        | TypedExprKind::ColumnRef { .. }
6944        | TypedExprKind::ScalarSubquery(_)
6945        | TypedExprKind::Exists { .. } => false,
6946    }
6947}
6948
6949fn bigint_literal(value: u64, span: crate::ast::Span) -> TypedExpr {
6950    TypedExpr {
6951        kind: TypedExprKind::Literal(Literal::Number(value.to_string())),
6952        resolved_type: ResolvedType::BigInt,
6953        span,
6954    }
6955}
6956
6957fn bigint_binary_op(
6958    left: TypedExpr,
6959    op: crate::ast::expr::BinaryOp,
6960    right: TypedExpr,
6961    span: crate::ast::Span,
6962) -> TypedExpr {
6963    TypedExpr {
6964        kind: TypedExprKind::BinaryOp {
6965            left: Box::new(left),
6966            op,
6967            right: Box::new(right),
6968        },
6969        resolved_type: ResolvedType::BigInt,
6970        span,
6971    }
6972}
6973
6974/// Lower a `GROUPING(e1, ..., en)` call to integer arithmetic over the
6975/// hidden `__grouping_id` output column (D4).
6976///
6977/// Argument `j` (0-based, leftmost = most significant result bit) whose key
6978/// occupies union position `i` contributes
6979/// `((__grouping_id / 2^(K-1-i)) % 2) * 2^(n-1-j)`; the divisor is a power
6980/// of two, so no division by zero is possible at runtime.
6981fn lower_grouping_call(
6982    args: &[TypedExpr],
6983    context: &GroupingRewrite,
6984    span: crate::ast::Span,
6985) -> Result<TypedExpr, PlannerError> {
6986    use crate::ast::expr::BinaryOp as AstBinaryOp;
6987
6988    if args.is_empty() {
6989        return Err(PlannerError::invalid_expression(
6990            "GROUPING requires at least one argument".to_string(),
6991        ));
6992    }
6993    if args.len() > MAX_GROUPING_KEYS {
6994        return Err(PlannerError::invalid_expression(format!(
6995            "GROUPING accepts at most {MAX_GROUPING_KEYS} arguments"
6996        )));
6997    }
6998    let mut key_positions = Vec::with_capacity(args.len());
6999    for arg in args {
7000        let Some(&position) = context.key_index.get(&expr_key(arg)) else {
7001            return Err(PlannerError::invalid_expression(
7002                "arguments to GROUPING must be grouping expressions of the query".to_string(),
7003            ));
7004        };
7005        key_positions.push(position);
7006    }
7007
7008    if !context.sets_present {
7009        // Plain GROUP BY has exactly one grouping set: every key is present.
7010        return Ok(bigint_literal(0, span));
7011    }
7012
7013    let argument_count = key_positions.len();
7014    let mut sum: Option<TypedExpr> = None;
7015    for (argument, key_position) in key_positions.into_iter().enumerate() {
7016        let gid_ref = TypedExpr::column_ref(
7017            "__agg__".to_string(),
7018            GROUPING_ID_COLUMN.to_string(),
7019            context.gid_index,
7020            ResolvedType::BigInt,
7021            span,
7022        );
7023        let excluded_shift = (context.key_count - 1 - key_position) as u32;
7024        let bit = bigint_binary_op(
7025            bigint_binary_op(
7026                gid_ref,
7027                AstBinaryOp::Div,
7028                bigint_literal(1u64 << excluded_shift, span),
7029                span,
7030            ),
7031            AstBinaryOp::Mod,
7032            bigint_literal(2, span),
7033            span,
7034        );
7035        let weight = 1u64 << (argument_count - 1 - argument);
7036        let term = if weight == 1 {
7037            bit
7038        } else {
7039            bigint_binary_op(bit, AstBinaryOp::Mul, bigint_literal(weight, span), span)
7040        };
7041        sum = Some(match sum {
7042            None => term,
7043            Some(current) => bigint_binary_op(current, AstBinaryOp::Add, term, span),
7044        });
7045    }
7046    Ok(sum.expect("GROUPING argument list is non-empty"))
7047}
7048
7049/// Pre-pass over aggregate-context expressions: replace GROUPING calls and
7050/// validate their placement before `rewrite_expr_with_maps` runs (D5).
7051///
7052/// Aggregate calls are returned unchanged (their signature must keep matching
7053/// the collected plan aggregates), but GROUPING inside their arguments is a
7054/// planning error because aggregate arguments evaluate against input rows.
7055fn rewrite_grouping_calls(
7056    expr: &TypedExpr,
7057    context: &GroupingRewrite,
7058) -> Result<TypedExpr, PlannerError> {
7059    let rebuild = |inner: &TypedExpr| rewrite_grouping_calls(inner, context);
7060    let rebuild_box = |inner: &TypedExpr| -> Result<Box<TypedExpr>, PlannerError> {
7061        Ok(Box::new(rewrite_grouping_calls(inner, context)?))
7062    };
7063    let kind = match &expr.kind {
7064        TypedExprKind::FunctionCall {
7065            name,
7066            args,
7067            distinct,
7068            star,
7069            filter,
7070            order_by,
7071            over,
7072        } => {
7073            if is_grouping_function(name) {
7074                if over.is_some() {
7075                    return Err(PlannerError::invalid_expression(
7076                        "GROUPING cannot be used as a window function".to_string(),
7077                    ));
7078                }
7079                return lower_grouping_call(args, context, expr.span);
7080            }
7081            if over.is_none() && is_aggregate_function(name) {
7082                if args.iter().any(typed_expr_contains_grouping)
7083                    || filter.as_deref().is_some_and(typed_expr_contains_grouping)
7084                    || order_by
7085                        .iter()
7086                        .any(|sort| typed_expr_contains_grouping(&sort.expr))
7087                {
7088                    return Err(PlannerError::invalid_expression(
7089                        "GROUPING cannot appear inside aggregate function arguments".to_string(),
7090                    ));
7091                }
7092                return Ok(expr.clone());
7093            }
7094            TypedExprKind::FunctionCall {
7095                name: name.clone(),
7096                args: args.iter().map(rebuild).collect::<Result<Vec<_>, _>>()?,
7097                distinct: *distinct,
7098                star: *star,
7099                filter: filter.as_deref().map(rebuild_box).transpose()?,
7100                order_by: order_by
7101                    .iter()
7102                    .map(|sort| {
7103                        Ok(SortExpr::new(
7104                            rebuild(&sort.expr)?,
7105                            sort.asc,
7106                            sort.nulls_first,
7107                        ))
7108                    })
7109                    .collect::<Result<Vec<_>, PlannerError>>()?,
7110                over: over
7111                    .as_ref()
7112                    .map(|window| {
7113                        Ok(typed_expr::TypedWindowSpec {
7114                            partition_by: window
7115                                .partition_by
7116                                .iter()
7117                                .map(rebuild)
7118                                .collect::<Result<Vec<_>, _>>()?,
7119                            order_by: window
7120                                .order_by
7121                                .iter()
7122                                .map(|sort| {
7123                                    Ok(SortExpr::new(
7124                                        rebuild(&sort.expr)?,
7125                                        sort.asc,
7126                                        sort.nulls_first,
7127                                    ))
7128                                })
7129                                .collect::<Result<Vec<_>, PlannerError>>()?,
7130                            frame: window.frame.clone(),
7131                        })
7132                    })
7133                    .transpose()
7134                    .map_err(|error: PlannerError| error)?,
7135            }
7136        }
7137        TypedExprKind::BinaryOp { left, op, right } => TypedExprKind::BinaryOp {
7138            left: rebuild_box(left)?,
7139            op: *op,
7140            right: rebuild_box(right)?,
7141        },
7142        TypedExprKind::UnaryOp { op, operand } => TypedExprKind::UnaryOp {
7143            op: *op,
7144            operand: rebuild_box(operand)?,
7145        },
7146        TypedExprKind::Case {
7147            operand,
7148            branches,
7149            else_expr,
7150        } => TypedExprKind::Case {
7151            operand: operand.as_deref().map(rebuild_box).transpose()?,
7152            branches: branches
7153                .iter()
7154                .map(|branch| {
7155                    Ok(TypedCaseWhen {
7156                        when: rebuild(&branch.when)?,
7157                        then: rebuild(&branch.then)?,
7158                    })
7159                })
7160                .collect::<Result<Vec<_>, PlannerError>>()?,
7161            else_expr: else_expr.as_deref().map(rebuild_box).transpose()?,
7162        },
7163        TypedExprKind::Cast {
7164            expr: inner,
7165            target_type,
7166        } => TypedExprKind::Cast {
7167            expr: rebuild_box(inner)?,
7168            target_type: target_type.clone(),
7169        },
7170        TypedExprKind::TryCast {
7171            expr: inner,
7172            target_type,
7173        } => TypedExprKind::TryCast {
7174            expr: rebuild_box(inner)?,
7175            target_type: target_type.clone(),
7176        },
7177        TypedExprKind::Between {
7178            expr: inner,
7179            low,
7180            high,
7181            negated,
7182        } => TypedExprKind::Between {
7183            expr: rebuild_box(inner)?,
7184            low: rebuild_box(low)?,
7185            high: rebuild_box(high)?,
7186            negated: *negated,
7187        },
7188        TypedExprKind::Like {
7189            expr: inner,
7190            pattern,
7191            escape,
7192            negated,
7193            kind,
7194        } => TypedExprKind::Like {
7195            expr: rebuild_box(inner)?,
7196            pattern: rebuild_box(pattern)?,
7197            escape: escape.as_deref().map(rebuild_box).transpose()?,
7198            negated: *negated,
7199            kind: *kind,
7200        },
7201        TypedExprKind::InList {
7202            expr: inner,
7203            list,
7204            negated,
7205        } => TypedExprKind::InList {
7206            expr: rebuild_box(inner)?,
7207            list: list.iter().map(rebuild).collect::<Result<Vec<_>, _>>()?,
7208            negated: *negated,
7209        },
7210        TypedExprKind::IsNull {
7211            expr: inner,
7212            negated,
7213        } => TypedExprKind::IsNull {
7214            expr: rebuild_box(inner)?,
7215            negated: *negated,
7216        },
7217        TypedExprKind::InSubquery {
7218            expr: inner,
7219            subquery,
7220            negated,
7221        } => TypedExprKind::InSubquery {
7222            expr: rebuild_box(inner)?,
7223            subquery: subquery.clone(),
7224            negated: *negated,
7225        },
7226        TypedExprKind::Quantified {
7227            expr: inner,
7228            op,
7229            quantifier,
7230            subquery,
7231        } => TypedExprKind::Quantified {
7232            expr: rebuild_box(inner)?,
7233            op: *op,
7234            quantifier: *quantifier,
7235            subquery: subquery.clone(),
7236        },
7237        TypedExprKind::Literal(_)
7238        | TypedExprKind::VectorLiteral(_)
7239        | TypedExprKind::ColumnRef { .. }
7240        | TypedExprKind::ScalarSubquery(_)
7241        | TypedExprKind::Exists { .. } => return Ok(expr.clone()),
7242    };
7243    Ok(TypedExpr {
7244        kind,
7245        resolved_type: expr.resolved_type.clone(),
7246        span: expr.span,
7247    })
7248}
7249
7250fn build_aggregate_schema(
7251    group_keys: &[TypedExpr],
7252    aggregates: &[AggregateExpr],
7253) -> Vec<ColumnMetadata> {
7254    let mut schema = Vec::new();
7255    for (idx, key) in group_keys.iter().enumerate() {
7256        let name = match &key.kind {
7257            TypedExprKind::ColumnRef { column, .. } => column.clone(),
7258            _ => format!("group_{idx}"),
7259        };
7260        schema.push(ColumnMetadata::new(name, key.resolved_type.clone()));
7261    }
7262    for (idx, agg) in aggregates.iter().enumerate() {
7263        let name = match &agg.function {
7264            AggregateFunction::Count => format!("count_{idx}"),
7265            AggregateFunction::Sum => format!("sum_{idx}"),
7266            AggregateFunction::Total => format!("total_{idx}"),
7267            AggregateFunction::Avg => format!("avg_{idx}"),
7268            AggregateFunction::Min => format!("min_{idx}"),
7269            AggregateFunction::Max => format!("max_{idx}"),
7270            AggregateFunction::GroupConcat { .. } => format!("group_concat_{idx}"),
7271            AggregateFunction::StringAgg { .. } => format!("string_agg_{idx}"),
7272            AggregateFunction::JsonGroupArray => format!("json_group_array_{idx}"),
7273            AggregateFunction::JsonGroupObject => format!("json_group_object_{idx}"),
7274            AggregateFunction::JsonbAgg => format!("jsonb_agg_{idx}"),
7275            AggregateFunction::JsonbObjectAgg => format!("jsonb_object_agg_{idx}"),
7276            AggregateFunction::PercentileDisc { .. } => format!("percentile_disc_{idx}"),
7277            _ => format!("aggregate_{idx}"),
7278        };
7279        schema.push(ColumnMetadata::new(name, agg.result_type.clone()));
7280    }
7281    schema
7282}
7283
7284fn rewrite_expr_with_maps(
7285    expr: &TypedExpr,
7286    group_key_map: &HashMap<String, usize>,
7287    aggregate_map: &HashMap<AggregateSignature, usize>,
7288    output_names: &[String],
7289) -> Result<TypedExpr, PlannerError> {
7290    let group_key_count = output_names.len().saturating_sub(aggregate_map.len());
7291    let key = expr_key(expr);
7292    if let Some(idx) = group_key_map.get(&key) {
7293        return Ok(make_output_column_ref(
7294            *idx,
7295            output_names,
7296            expr.resolved_type.clone(),
7297            expr.span,
7298        ));
7299    }
7300
7301    match &expr.kind {
7302        TypedExprKind::FunctionCall {
7303            name,
7304            args,
7305            distinct,
7306            star,
7307            filter,
7308            order_by,
7309            over: None,
7310        } if is_aggregate_function(name) => {
7311            let lower = name.to_ascii_lowercase();
7312            let is_percentile = matches!(lower.as_str(), "percentile_disc" | "percentile_cont");
7313            let separator = if name.eq_ignore_ascii_case("group_concat") && args.len() == 2 {
7314                if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
7315                    Some(value.clone())
7316                } else {
7317                    return Err(PlannerError::invalid_expression(
7318                        "GROUP_CONCAT separator must be a string literal".to_string(),
7319                    ));
7320                }
7321            } else if name.eq_ignore_ascii_case("string_agg") && args.len() == 2 {
7322                if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
7323                    Some(value.clone())
7324                } else {
7325                    return Err(PlannerError::invalid_expression(
7326                        "STRING_AGG separator must be a string literal".to_string(),
7327                    ));
7328                }
7329            } else if is_percentile && args.len() == 1 {
7330                Some(format!(
7331                    "{:?}",
7332                    type_checker::percentile_fraction_named(&lower, &args[0])?
7333                ))
7334            } else if lower == "quantile_cont" && args.len() == 2 {
7335                Some(format!(
7336                    "{:?}",
7337                    type_checker::percentile_fraction_named(&lower, &args[1])?
7338                ))
7339            } else {
7340                None
7341            };
7342            let signature = AggregateSignature {
7343                name: canonical_aggregate_name(name),
7344                distinct: *distinct,
7345                star: *star,
7346                arg_key: if is_percentile {
7347                    None
7348                } else {
7349                    args.first().map(expr_key)
7350                },
7351                extra_arg_keys: if matches!(
7352                    lower.as_str(),
7353                    "group_concat"
7354                        | "string_agg"
7355                        | "percentile_disc"
7356                        | "percentile_cont"
7357                        | "quantile_cont"
7358                ) {
7359                    Vec::new()
7360                } else {
7361                    args.iter().skip(1).map(expr_key).collect()
7362                },
7363                separator,
7364                filter_key: filter.as_deref().map(expr_key),
7365                order_key: if is_order_sensitive_aggregate(name) {
7366                    sort_exprs_key(order_by)
7367                } else {
7368                    None
7369                },
7370            };
7371            let idx = aggregate_map.get(&signature).ok_or_else(|| {
7372                PlannerError::invalid_expression(
7373                    "aggregate in expression is not part of plan".to_string(),
7374                )
7375            })?;
7376            let output_index = group_key_count + idx;
7377            Ok(make_output_column_ref(
7378                output_index,
7379                output_names,
7380                expr.resolved_type.clone(),
7381                expr.span,
7382            ))
7383        }
7384        TypedExprKind::FunctionCall {
7385            name,
7386            args,
7387            distinct,
7388            star,
7389            filter,
7390            order_by,
7391            over,
7392        } => {
7393            if over.is_none() && (*distinct || *star) {
7394                return Err(PlannerError::invalid_expression(
7395                    "DISTINCT/STAR modifiers are only supported for aggregates".to_string(),
7396                ));
7397            }
7398            if filter.is_some() || !order_by.is_empty() {
7399                // Non-aggregate calls never carry these clauses (rejected by
7400                // the type checker), so reaching here means the aggregate
7401                // above did not match the plan.
7402                return Err(PlannerError::invalid_expression(
7403                    "aggregate in expression is not part of plan".to_string(),
7404                ));
7405            }
7406            let mut rewritten_args = Vec::with_capacity(args.len());
7407            for arg in args {
7408                rewritten_args.push(rewrite_expr_with_maps(
7409                    arg,
7410                    group_key_map,
7411                    aggregate_map,
7412                    output_names,
7413                )?);
7414            }
7415            let over = over
7416                .as_ref()
7417                .map(|window| {
7418                    let partition_by = window
7419                        .partition_by
7420                        .iter()
7421                        .map(|expr| {
7422                            rewrite_expr_with_maps(expr, group_key_map, aggregate_map, output_names)
7423                        })
7424                        .collect::<Result<Vec<_>, PlannerError>>()?;
7425                    let order_by = window
7426                        .order_by
7427                        .iter()
7428                        .map(|sort| {
7429                            Ok(SortExpr::new(
7430                                rewrite_expr_with_maps(
7431                                    &sort.expr,
7432                                    group_key_map,
7433                                    aggregate_map,
7434                                    output_names,
7435                                )?,
7436                                sort.asc,
7437                                sort.nulls_first,
7438                            ))
7439                        })
7440                        .collect::<Result<Vec<_>, PlannerError>>()?;
7441                    Ok(crate::planner::typed_expr::TypedWindowSpec {
7442                        partition_by,
7443                        order_by,
7444                        frame: window.frame.clone(),
7445                    })
7446                })
7447                .transpose()?;
7448            Ok(TypedExpr {
7449                kind: TypedExprKind::FunctionCall {
7450                    name: name.clone(),
7451                    args: rewritten_args,
7452                    distinct: *distinct,
7453                    star: *star,
7454                    filter: None,
7455                    order_by: Vec::new(),
7456                    over,
7457                },
7458                resolved_type: expr.resolved_type.clone(),
7459                span: expr.span,
7460            })
7461        }
7462        TypedExprKind::BinaryOp { left, op, right } => {
7463            let left = rewrite_expr_with_maps(left, group_key_map, aggregate_map, output_names)?;
7464            let right = rewrite_expr_with_maps(right, group_key_map, aggregate_map, output_names)?;
7465            Ok(TypedExpr {
7466                kind: TypedExprKind::BinaryOp {
7467                    left: Box::new(left),
7468                    op: *op,
7469                    right: Box::new(right),
7470                },
7471                resolved_type: expr.resolved_type.clone(),
7472                span: expr.span,
7473            })
7474        }
7475        TypedExprKind::UnaryOp { op, operand } => {
7476            let operand =
7477                rewrite_expr_with_maps(operand, group_key_map, aggregate_map, output_names)?;
7478            Ok(TypedExpr {
7479                kind: TypedExprKind::UnaryOp {
7480                    op: *op,
7481                    operand: Box::new(operand),
7482                },
7483                resolved_type: expr.resolved_type.clone(),
7484                span: expr.span,
7485            })
7486        }
7487        TypedExprKind::Case {
7488            operand,
7489            branches,
7490            else_expr,
7491        } => {
7492            let operand = operand
7493                .as_deref()
7494                .map(|operand| {
7495                    rewrite_expr_with_maps(operand, group_key_map, aggregate_map, output_names)
7496                        .map(Box::new)
7497                })
7498                .transpose()?;
7499            let mut rewritten_branches = Vec::with_capacity(branches.len());
7500            for branch in branches {
7501                rewritten_branches.push(TypedCaseWhen {
7502                    when: rewrite_expr_with_maps(
7503                        &branch.when,
7504                        group_key_map,
7505                        aggregate_map,
7506                        output_names,
7507                    )?,
7508                    then: rewrite_expr_with_maps(
7509                        &branch.then,
7510                        group_key_map,
7511                        aggregate_map,
7512                        output_names,
7513                    )?,
7514                });
7515            }
7516            let else_expr = else_expr
7517                .as_deref()
7518                .map(|else_expr| {
7519                    rewrite_expr_with_maps(else_expr, group_key_map, aggregate_map, output_names)
7520                        .map(Box::new)
7521                })
7522                .transpose()?;
7523            Ok(TypedExpr {
7524                kind: TypedExprKind::Case {
7525                    operand,
7526                    branches: rewritten_branches,
7527                    else_expr,
7528                },
7529                resolved_type: expr.resolved_type.clone(),
7530                span: expr.span,
7531            })
7532        }
7533        TypedExprKind::Between {
7534            expr: inner,
7535            low,
7536            high,
7537            negated,
7538        } => {
7539            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
7540            let low = rewrite_expr_with_maps(low, group_key_map, aggregate_map, output_names)?;
7541            let high = rewrite_expr_with_maps(high, group_key_map, aggregate_map, output_names)?;
7542            Ok(TypedExpr {
7543                kind: TypedExprKind::Between {
7544                    expr: Box::new(inner),
7545                    low: Box::new(low),
7546                    high: Box::new(high),
7547                    negated: *negated,
7548                },
7549                resolved_type: expr.resolved_type.clone(),
7550                span: expr.span,
7551            })
7552        }
7553        TypedExprKind::Like {
7554            expr: inner,
7555            pattern,
7556            escape,
7557            negated,
7558            kind,
7559        } => {
7560            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
7561            let pattern =
7562                rewrite_expr_with_maps(pattern, group_key_map, aggregate_map, output_names)?;
7563            let escape = if let Some(esc) = escape {
7564                Some(Box::new(rewrite_expr_with_maps(
7565                    esc,
7566                    group_key_map,
7567                    aggregate_map,
7568                    output_names,
7569                )?))
7570            } else {
7571                None
7572            };
7573            Ok(TypedExpr {
7574                kind: TypedExprKind::Like {
7575                    expr: Box::new(inner),
7576                    pattern: Box::new(pattern),
7577                    escape,
7578                    negated: *negated,
7579                    kind: *kind,
7580                },
7581                resolved_type: expr.resolved_type.clone(),
7582                span: expr.span,
7583            })
7584        }
7585        TypedExprKind::InList {
7586            expr: inner,
7587            list,
7588            negated,
7589        } => {
7590            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
7591            let mut rewritten_list = Vec::with_capacity(list.len());
7592            for item in list {
7593                rewritten_list.push(rewrite_expr_with_maps(
7594                    item,
7595                    group_key_map,
7596                    aggregate_map,
7597                    output_names,
7598                )?);
7599            }
7600            Ok(TypedExpr {
7601                kind: TypedExprKind::InList {
7602                    expr: Box::new(inner),
7603                    list: rewritten_list,
7604                    negated: *negated,
7605                },
7606                resolved_type: expr.resolved_type.clone(),
7607                span: expr.span,
7608            })
7609        }
7610        TypedExprKind::IsNull {
7611            expr: inner,
7612            negated,
7613        } => {
7614            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
7615            Ok(TypedExpr {
7616                kind: TypedExprKind::IsNull {
7617                    expr: Box::new(inner),
7618                    negated: *negated,
7619                },
7620                resolved_type: expr.resolved_type.clone(),
7621                span: expr.span,
7622            })
7623        }
7624        TypedExprKind::Literal(_) | TypedExprKind::VectorLiteral(_) => Ok(expr.clone()),
7625        // References the GROUPING pre-pass already resolved onto the
7626        // aggregate output (the hidden __grouping_id column) pass through.
7627        TypedExprKind::ColumnRef { table, .. } if table == "__agg__" => Ok(expr.clone()),
7628        TypedExprKind::ColumnRef { .. } => Err(PlannerError::invalid_expression(
7629            "column reference must appear in GROUP BY or be aggregated".to_string(),
7630        )),
7631        TypedExprKind::Cast {
7632            expr: inner,
7633            target_type,
7634        } => {
7635            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
7636            Ok(TypedExpr {
7637                kind: TypedExprKind::Cast {
7638                    expr: Box::new(inner),
7639                    target_type: target_type.clone(),
7640                },
7641                resolved_type: expr.resolved_type.clone(),
7642                span: expr.span,
7643            })
7644        }
7645        TypedExprKind::TryCast {
7646            expr: inner,
7647            target_type,
7648        } => {
7649            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
7650            Ok(TypedExpr {
7651                kind: TypedExprKind::TryCast {
7652                    expr: Box::new(inner),
7653                    target_type: target_type.clone(),
7654                },
7655                resolved_type: expr.resolved_type.clone(),
7656                span: expr.span,
7657            })
7658        }
7659        TypedExprKind::ScalarSubquery(_)
7660        | TypedExprKind::InSubquery { .. }
7661        | TypedExprKind::Exists { .. }
7662        | TypedExprKind::Quantified { .. } => Ok(expr.clone()),
7663    }
7664}
7665
7666fn make_output_column_ref(
7667    index: usize,
7668    output_names: &[String],
7669    resolved_type: ResolvedType,
7670    span: crate::ast::Span,
7671) -> TypedExpr {
7672    let name = output_names
7673        .get(index)
7674        .cloned()
7675        .unwrap_or_else(|| format!("col_{index}"));
7676    TypedExpr::column_ref("__agg__".to_string(), name, index, resolved_type, span)
7677}