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;
17pub mod type_checker;
18pub mod typed_expr;
19pub mod types;
20
21#[cfg(test)]
22mod planner_tests;
23
24pub use aggregate_expr::{AggregateExpr, AggregateFunction};
25pub use error::PlannerError;
26pub use knn_optimizer::{KnnPattern, SortDirection, detect_knn_pattern};
27pub use logical_plan::{JoinType, LogicalPlan, SetOperator, WindowExpr, WindowFunction};
28pub use name_resolver::{NameResolver, ResolvedColumn};
29pub use type_checker::{ScopedTable, TypeChecker};
30pub use typed_expr::{
31    ProjectedColumn, Projection, SortExpr, TypedAssignment, TypedCaseWhen, TypedExpr, TypedExprKind,
32};
33pub use types::ResolvedType;
34
35use crate::ast::ddl::{
36    ColumnConstraint, ColumnDef, CreateIndex, CreateTable, DropIndex, DropTable,
37};
38use crate::ast::dml::{
39    Delete, FromItem, Insert, InsertSource, LITERAL_TABLE, OrderByExpr, Select, SelectItem,
40    SetOperator as AstSetOperator, Update,
41};
42use crate::ast::expr::Literal;
43use crate::ast::{PragmaValue, Spanned, Statement, StatementKind};
44use crate::catalog::{Catalog, ColumnMetadata, IndexMetadata, TableMetadata};
45use crate::{AlopexDialect, DataSourceFormat, Parser, SqlError, TableType};
46use std::collections::{HashMap, HashSet};
47
48#[derive(Clone)]
49struct PlannedRelation {
50    plan: LogicalPlan,
51    schema: Vec<ColumnMetadata>,
52    scope: Vec<ScopedTable>,
53}
54
55type CtePlans = HashMap<String, PlannedRelation>;
56
57/// Planning output used by server-side routing analysis.
58///
59/// This is intentionally owned by `alopex-sql` and contains no
60/// `alopex-cluster` types. Cluster routing layers can translate this DTO into
61/// their own routing model without making SQL depend on cluster metadata.
62#[derive(Debug, Clone)]
63pub struct PlannedStatement {
64    /// Logical plan produced by the regular SQL planner.
65    pub plan: LogicalPlan,
66    /// SQL-owned routing input derived during planning.
67    pub routing_input: RoutingInput,
68}
69
70impl PlannedStatement {
71    /// Statement kind associated with this plan.
72    pub fn statement_kind(&self) -> &StatementKind {
73        &self.routing_input.statement_kind
74    }
75
76    /// Table references extracted for routing analysis.
77    pub fn table_references(&self) -> &[TableReference] {
78        &self.routing_input.table_references
79    }
80
81    /// Planning diagnostics available for routing layers to attach to their
82    /// own decision diagnostics.
83    pub fn diagnostics(&self) -> &[PlanningDiagnostic] {
84        &self.routing_input.diagnostics
85    }
86}
87
88/// SQL-owned input for routing decision composition.
89#[derive(Debug, Clone)]
90pub struct RoutingInput {
91    /// Original statement kind. Consumers should match on variants rather than
92    /// reparsing SQL.
93    pub statement_kind: StatementKind,
94    /// Conservative table references extracted from the planned statement.
95    pub table_references: Vec<TableReference>,
96    /// Diagnostics produced while preparing routing input.
97    pub diagnostics: Vec<PlanningDiagnostic>,
98}
99
100/// A table reference visible at the SQL planning boundary.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct TableReference {
103    /// Table name as resolved by the current planner/catalog view.
104    pub table_name: String,
105    /// Access class requested by the statement.
106    pub access: TableReferenceAccess,
107    /// Extraction source for diagnostics and future extractor expansion.
108    pub source: TableReferenceSource,
109}
110
111impl TableReference {
112    pub fn new(
113        table_name: impl Into<String>,
114        access: TableReferenceAccess,
115        source: TableReferenceSource,
116    ) -> Self {
117        Self {
118            table_name: table_name.into(),
119            access,
120            source,
121        }
122    }
123}
124
125/// Access class for a table reference.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum TableReferenceAccess {
128    /// Read-only scan/reference.
129    Read,
130    /// Data mutation against an existing table.
131    Write,
132    /// Table creation.
133    Create,
134    /// Table drop/removal.
135    Drop,
136    /// Metadata operation related to a table, such as CREATE INDEX.
137    Metadata,
138}
139
140/// Where a table reference was extracted from.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum TableReferenceSource {
143    /// The existing `LogicalPlan::table_name()` single-table helper.
144    TopLevelPlanTableName,
145    /// A physical table scan in a logical plan tree.
146    LogicalPlanScan,
147    /// A DML target table.
148    LogicalPlanMutationTarget,
149    /// A DDL target table.
150    LogicalPlanDdlTarget,
151    /// A table referenced by index metadata.
152    LogicalPlanIndexTarget,
153    /// A table reached through a typed subquery expression.
154    TypedExprSubquery,
155}
156
157/// Severity for planning diagnostics.
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum PlanningDiagnosticSeverity {
160    Info,
161    Warning,
162}
163
164/// SQL planning diagnostic attachment point for routing layers.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct PlanningDiagnostic {
167    /// Stable machine-readable diagnostic code.
168    pub code: &'static str,
169    /// Diagnostic severity.
170    pub severity: PlanningDiagnosticSeverity,
171    /// Human-readable context.
172    pub message: String,
173}
174
175impl PlanningDiagnostic {
176    pub fn info(code: &'static str, message: impl Into<String>) -> Self {
177        Self {
178            code,
179            severity: PlanningDiagnosticSeverity::Info,
180            message: message.into(),
181        }
182    }
183
184    pub fn warning(code: &'static str, message: impl Into<String>) -> Self {
185        Self {
186            code,
187            severity: PlanningDiagnosticSeverity::Warning,
188            message: message.into(),
189        }
190    }
191}
192
193/// Parse and plan SQL without executing it, returning SQL-owned routing input.
194pub fn plan_sql_for_routing<C: Catalog + ?Sized>(
195    catalog: &C,
196    sql: &str,
197) -> Result<Vec<PlannedStatement>, SqlError> {
198    let statements = Parser::parse_sql(&AlopexDialect, sql).map_err(SqlError::from)?;
199    statements
200        .iter()
201        .map(|statement| plan_statement_for_routing(catalog, statement).map_err(SqlError::from))
202        .collect()
203}
204
205/// Plan a parsed statement without executing it, returning SQL-owned routing input.
206pub fn plan_statement_for_routing<C: Catalog + ?Sized>(
207    catalog: &C,
208    statement: &Statement,
209) -> Result<PlannedStatement, PlannerError> {
210    let planner = Planner::new(catalog);
211    let plan = planner.plan(statement)?;
212    let routing_input = routing_input_for_plan(statement, &plan)?;
213    Ok(PlannedStatement {
214        plan,
215        routing_input,
216    })
217}
218
219fn routing_input_for_plan(
220    statement: &Statement,
221    plan: &LogicalPlan,
222) -> Result<RoutingInput, PlannerError> {
223    let mut diagnostics = Vec::new();
224    let extractor = TableReferenceExtractor::new();
225    let table_references = extractor.extract_from_logical_plan(
226        plan,
227        table_reference_access(statement)?,
228        &mut diagnostics,
229    );
230
231    Ok(RoutingInput {
232        statement_kind: statement.kind.clone(),
233        table_references,
234        diagnostics,
235    })
236}
237
238/// Extracts physical table references from SQL-owned planner structures.
239#[derive(Debug, Default, Clone, Copy)]
240pub struct TableReferenceExtractor;
241
242impl TableReferenceExtractor {
243    pub fn new() -> Self {
244        Self
245    }
246
247    /// Extract references from a logical plan tree. `root_access` is applied to
248    /// the top-level statement target; nested typed subqueries are read-only.
249    pub fn extract_from_logical_plan(
250        &self,
251        plan: &LogicalPlan,
252        root_access: TableReferenceAccess,
253        diagnostics: &mut Vec<PlanningDiagnostic>,
254    ) -> Vec<TableReference> {
255        let mut references = Vec::new();
256        self.extract_plan(
257            plan,
258            root_access,
259            TableReferenceSource::LogicalPlanScan,
260            diagnostics,
261            &mut references,
262        );
263        if references.is_empty() {
264            diagnostics.push(PlanningDiagnostic::info(
265                "ALOPEX-PLAN-ROUTE-001",
266                "statement has no physical table reference",
267            ));
268        }
269        references
270    }
271
272    /// Extract references from a typed subquery plan embedded in an expression.
273    pub fn extract_from_subquery_context(
274        &self,
275        plan: &LogicalPlan,
276        diagnostics: &mut Vec<PlanningDiagnostic>,
277    ) -> Vec<TableReference> {
278        let mut references = Vec::new();
279        self.extract_plan(
280            plan,
281            TableReferenceAccess::Read,
282            TableReferenceSource::TypedExprSubquery,
283            diagnostics,
284            &mut references,
285        );
286        references
287    }
288
289    fn extract_plan(
290        &self,
291        plan: &LogicalPlan,
292        root_access: TableReferenceAccess,
293        scan_source: TableReferenceSource,
294        diagnostics: &mut Vec<PlanningDiagnostic>,
295        references: &mut Vec<TableReference>,
296    ) {
297        match plan {
298            LogicalPlan::Scan { table, projection } => {
299                if table != LITERAL_TABLE {
300                    push_table_reference(
301                        references,
302                        table,
303                        TableReferenceAccess::Read,
304                        scan_source,
305                    );
306                }
307                self.extract_projection(projection, diagnostics, references);
308            }
309            LogicalPlan::Filter { input, predicate } => {
310                self.extract_plan(input, root_access, scan_source, diagnostics, references);
311                self.extract_typed_expr(predicate, diagnostics, references);
312            }
313            LogicalPlan::Project { input, projection } => {
314                self.extract_plan(input, root_access, scan_source, diagnostics, references);
315                self.extract_projection(projection, diagnostics, references);
316            }
317            LogicalPlan::Join {
318                left,
319                right,
320                condition,
321                ..
322            } => {
323                self.extract_plan(
324                    left,
325                    TableReferenceAccess::Read,
326                    scan_source,
327                    diagnostics,
328                    references,
329                );
330                self.extract_plan(
331                    right,
332                    TableReferenceAccess::Read,
333                    scan_source,
334                    diagnostics,
335                    references,
336                );
337                if let Some(condition) = condition {
338                    self.extract_typed_expr(condition, diagnostics, references);
339                }
340            }
341            LogicalPlan::Aggregate {
342                input,
343                group_keys,
344                aggregates,
345                having,
346                projection,
347            } => {
348                self.extract_plan(input, root_access, scan_source, diagnostics, references);
349                for expr in group_keys {
350                    self.extract_typed_expr(expr, diagnostics, references);
351                }
352                for aggregate in aggregates {
353                    if let Some(arg) = &aggregate.arg {
354                        self.extract_typed_expr(arg, diagnostics, references);
355                    }
356                }
357                if let Some(having) = having {
358                    self.extract_typed_expr(having, diagnostics, references);
359                }
360                self.extract_projection(projection, diagnostics, references);
361            }
362            LogicalPlan::Window { input, windows } => {
363                self.extract_plan(input, root_access, scan_source, diagnostics, references);
364                for window in windows {
365                    for expr in &window.partition_by {
366                        self.extract_typed_expr(expr, diagnostics, references);
367                    }
368                    for sort_expr in &window.order_by {
369                        self.extract_typed_expr(&sort_expr.expr, diagnostics, references);
370                    }
371                    if let WindowFunction::Aggregate(aggregate) = &window.function
372                        && let Some(arg) = &aggregate.arg
373                    {
374                        self.extract_typed_expr(arg, diagnostics, references);
375                    }
376                }
377            }
378            LogicalPlan::SetOperation { left, right, .. } => {
379                self.extract_plan(left, root_access, scan_source, diagnostics, references);
380                self.extract_plan(right, root_access, scan_source, diagnostics, references);
381            }
382            LogicalPlan::Sort { input, order_by } => {
383                self.extract_plan(input, root_access, scan_source, diagnostics, references);
384                for sort_expr in order_by {
385                    self.extract_typed_expr(&sort_expr.expr, diagnostics, references);
386                }
387            }
388            LogicalPlan::Limit { input, .. } => {
389                self.extract_plan(input, root_access, scan_source, diagnostics, references);
390            }
391            LogicalPlan::Insert { table, values, .. } => {
392                push_table_reference(
393                    references,
394                    table,
395                    root_access,
396                    TableReferenceSource::LogicalPlanMutationTarget,
397                );
398                for row in values {
399                    for value in row {
400                        self.extract_typed_expr(value, diagnostics, references);
401                    }
402                }
403            }
404            LogicalPlan::InsertSelect { table, source, .. } => {
405                push_table_reference(
406                    references,
407                    table,
408                    root_access,
409                    TableReferenceSource::LogicalPlanMutationTarget,
410                );
411                self.extract_plan(
412                    source,
413                    TableReferenceAccess::Read,
414                    scan_source,
415                    diagnostics,
416                    references,
417                );
418            }
419            LogicalPlan::Update {
420                table,
421                assignments,
422                filter,
423            } => {
424                push_table_reference(
425                    references,
426                    table,
427                    root_access,
428                    TableReferenceSource::LogicalPlanMutationTarget,
429                );
430                for assignment in assignments {
431                    self.extract_typed_expr(&assignment.value, diagnostics, references);
432                }
433                if let Some(filter) = filter {
434                    self.extract_typed_expr(filter, diagnostics, references);
435                }
436            }
437            LogicalPlan::Delete { table, filter } => {
438                push_table_reference(
439                    references,
440                    table,
441                    root_access,
442                    TableReferenceSource::LogicalPlanMutationTarget,
443                );
444                if let Some(filter) = filter {
445                    self.extract_typed_expr(filter, diagnostics, references);
446                }
447            }
448            LogicalPlan::CreateTable { table, .. } => push_table_reference(
449                references,
450                &table.name,
451                root_access,
452                TableReferenceSource::LogicalPlanDdlTarget,
453            ),
454            LogicalPlan::DropTable { name, .. } => push_table_reference(
455                references,
456                name,
457                root_access,
458                TableReferenceSource::LogicalPlanDdlTarget,
459            ),
460            LogicalPlan::CreateIndex { index, .. } => push_table_reference(
461                references,
462                &index.table,
463                root_access,
464                TableReferenceSource::LogicalPlanIndexTarget,
465            ),
466            LogicalPlan::DropIndex { name, .. } => diagnostics.push(PlanningDiagnostic::warning(
467                "ALOPEX-PLAN-ROUTE-003",
468                format!(
469                    "DROP INDEX {name} does not expose a target table in the current logical plan"
470                ),
471            )),
472            LogicalPlan::Pragma { .. } => {}
473        }
474    }
475
476    fn extract_projection(
477        &self,
478        projection: &Projection,
479        diagnostics: &mut Vec<PlanningDiagnostic>,
480        references: &mut Vec<TableReference>,
481    ) {
482        if let Projection::Columns(columns) = projection {
483            for column in columns {
484                self.extract_typed_expr(&column.expr, diagnostics, references);
485            }
486        }
487    }
488
489    fn extract_typed_expr(
490        &self,
491        expr: &TypedExpr,
492        diagnostics: &mut Vec<PlanningDiagnostic>,
493        references: &mut Vec<TableReference>,
494    ) {
495        match &expr.kind {
496            TypedExprKind::Literal(_)
497            | TypedExprKind::ColumnRef { .. }
498            | TypedExprKind::VectorLiteral(_) => {}
499            TypedExprKind::BinaryOp { left, right, .. } => {
500                self.extract_typed_expr(left, diagnostics, references);
501                self.extract_typed_expr(right, diagnostics, references);
502            }
503            TypedExprKind::UnaryOp { operand, .. }
504            | TypedExprKind::Cast { expr: operand, .. }
505            | TypedExprKind::IsNull { expr: operand, .. } => {
506                self.extract_typed_expr(operand, diagnostics, references);
507            }
508            TypedExprKind::Case {
509                operand,
510                branches,
511                else_expr,
512            } => {
513                if let Some(operand) = operand {
514                    self.extract_typed_expr(operand, diagnostics, references);
515                }
516                for branch in branches {
517                    self.extract_typed_expr(&branch.when, diagnostics, references);
518                    self.extract_typed_expr(&branch.then, diagnostics, references);
519                }
520                if let Some(else_expr) = else_expr {
521                    self.extract_typed_expr(else_expr, diagnostics, references);
522                }
523            }
524            TypedExprKind::FunctionCall { args, .. } => {
525                for arg in args {
526                    self.extract_typed_expr(arg, diagnostics, references);
527                }
528            }
529            TypedExprKind::Between {
530                expr, low, high, ..
531            } => {
532                self.extract_typed_expr(expr, diagnostics, references);
533                self.extract_typed_expr(low, diagnostics, references);
534                self.extract_typed_expr(high, diagnostics, references);
535            }
536            TypedExprKind::Like {
537                expr,
538                pattern,
539                escape,
540                ..
541            } => {
542                self.extract_typed_expr(expr, diagnostics, references);
543                self.extract_typed_expr(pattern, diagnostics, references);
544                if let Some(escape) = escape {
545                    self.extract_typed_expr(escape, diagnostics, references);
546                }
547            }
548            TypedExprKind::InList { expr, list, .. } => {
549                self.extract_typed_expr(expr, diagnostics, references);
550                for item in list {
551                    self.extract_typed_expr(item, diagnostics, references);
552                }
553            }
554            TypedExprKind::ScalarSubquery(subquery) => self.extract_plan(
555                subquery,
556                TableReferenceAccess::Read,
557                TableReferenceSource::TypedExprSubquery,
558                diagnostics,
559                references,
560            ),
561            TypedExprKind::InSubquery { expr, subquery, .. } => {
562                self.extract_typed_expr(expr, diagnostics, references);
563                self.extract_plan(
564                    subquery,
565                    TableReferenceAccess::Read,
566                    TableReferenceSource::TypedExprSubquery,
567                    diagnostics,
568                    references,
569                );
570            }
571            TypedExprKind::Exists { subquery, .. } => self.extract_plan(
572                subquery,
573                TableReferenceAccess::Read,
574                TableReferenceSource::TypedExprSubquery,
575                diagnostics,
576                references,
577            ),
578            TypedExprKind::Quantified { expr, subquery, .. } => {
579                self.extract_typed_expr(expr, diagnostics, references);
580                self.extract_plan(
581                    subquery,
582                    TableReferenceAccess::Read,
583                    TableReferenceSource::TypedExprSubquery,
584                    diagnostics,
585                    references,
586                );
587            }
588        }
589    }
590}
591
592fn push_table_reference(
593    references: &mut Vec<TableReference>,
594    table_name: &str,
595    access: TableReferenceAccess,
596    source: TableReferenceSource,
597) {
598    if !references.iter().any(|reference| {
599        reference.table_name == table_name
600            && reference.access == access
601            && reference.source == source
602    }) {
603        references.push(TableReference::new(table_name, access, source));
604    }
605}
606
607#[derive(Debug)]
608enum GenericHostStatement<'a> {
609    CreateTable(&'a CreateTable),
610    DropTable(&'a DropTable),
611    CreateIndex(&'a CreateIndex),
612    DropIndex(&'a DropIndex),
613    Pragma {
614        name: &'a str,
615        value: &'a Option<PragmaValue>,
616    },
617    Select(&'a Select),
618    Insert(&'a Insert),
619    Update(&'a Update),
620    Delete(&'a Delete),
621    Unsupported,
622}
623
624fn classify_generic_host_statement(statement_kind: &StatementKind) -> GenericHostStatement<'_> {
625    // The fallback is intentionally unreachable for the current enum. It
626    // becomes the safe route before a future statement-specific host is added.
627    #[allow(unreachable_patterns)]
628    match statement_kind {
629        StatementKind::CreateTable(statement) => GenericHostStatement::CreateTable(statement),
630        StatementKind::DropTable(statement) => GenericHostStatement::DropTable(statement),
631        StatementKind::CreateIndex(statement) => GenericHostStatement::CreateIndex(statement),
632        StatementKind::DropIndex(statement) => GenericHostStatement::DropIndex(statement),
633        StatementKind::Pragma { name, value } => GenericHostStatement::Pragma { name, value },
634        StatementKind::Select(statement) => GenericHostStatement::Select(statement),
635        StatementKind::Insert(statement) => GenericHostStatement::Insert(statement),
636        StatementKind::Update(statement) => GenericHostStatement::Update(statement),
637        StatementKind::Delete(statement) => GenericHostStatement::Delete(statement),
638        _ => GenericHostStatement::Unsupported,
639    }
640}
641
642fn unsupported_generic_statement(statement: &Statement) -> PlannerError {
643    PlannerError::unsupported_feature(
644        "statement kind for the generic SQL planner",
645        "a statement-specific planner",
646        statement.span,
647    )
648}
649
650fn table_reference_access(statement: &Statement) -> Result<TableReferenceAccess, PlannerError> {
651    table_reference_access_for_classified(
652        statement,
653        classify_generic_host_statement(&statement.kind),
654    )
655}
656
657fn table_reference_access_for_classified(
658    statement: &Statement,
659    classified: GenericHostStatement<'_>,
660) -> Result<TableReferenceAccess, PlannerError> {
661    match classified {
662        GenericHostStatement::Select(_) => Ok(TableReferenceAccess::Read),
663        GenericHostStatement::Insert(_)
664        | GenericHostStatement::Update(_)
665        | GenericHostStatement::Delete(_) => Ok(TableReferenceAccess::Write),
666        GenericHostStatement::CreateTable(_) => Ok(TableReferenceAccess::Create),
667        GenericHostStatement::DropTable(_) => Ok(TableReferenceAccess::Drop),
668        GenericHostStatement::CreateIndex(_)
669        | GenericHostStatement::DropIndex(_)
670        | GenericHostStatement::Pragma { .. } => Ok(TableReferenceAccess::Metadata),
671        GenericHostStatement::Unsupported => Err(unsupported_generic_statement(statement)),
672    }
673}
674
675/// The SQL query planner.
676///
677/// The planner converts AST statements into logical plans. It performs:
678/// - Name resolution: Validates table and column references
679/// - Type checking: Infers and validates expression types
680/// - Plan construction: Builds the logical plan tree
681///
682/// # Design Notes
683///
684/// - The planner uses an immutable reference to the catalog (`&C`)
685/// - DDL statements produce plans but don't modify the catalog
686/// - The executor is responsible for applying catalog changes
687///
688/// # Examples
689///
690/// ```
691/// use alopex_sql::catalog::MemoryCatalog;
692/// use alopex_sql::planner::Planner;
693///
694/// let catalog = MemoryCatalog::new();
695/// let planner = Planner::new(&catalog);
696///
697/// // Parse and plan a statement
698/// // let stmt = parser.parse("SELECT * FROM users")?;
699/// // let plan = planner.plan(&stmt)?;
700/// ```
701pub struct Planner<'a, C: Catalog + ?Sized> {
702    catalog: &'a C,
703    name_resolver: NameResolver<'a, C>,
704    type_checker: TypeChecker<'a, C>,
705}
706
707impl<'a, C: Catalog + ?Sized> Planner<'a, C> {
708    /// Create a new planner with the given catalog.
709    pub fn new(catalog: &'a C) -> Self {
710        Self {
711            catalog,
712            name_resolver: NameResolver::new(catalog),
713            type_checker: TypeChecker::new(catalog),
714        }
715    }
716
717    /// Plan a SQL statement.
718    ///
719    /// This is the main entry point for converting an AST statement into a logical plan.
720    ///
721    /// # Errors
722    ///
723    /// Returns a `PlannerError` if:
724    /// - Referenced tables or columns don't exist
725    /// - Type checking fails
726    /// - DDL validation fails (e.g., table already exists for CREATE TABLE)
727    pub fn plan(&self, stmt: &Statement) -> Result<LogicalPlan, PlannerError> {
728        self.plan_classified_statement(stmt, classify_generic_host_statement(&stmt.kind))
729    }
730
731    fn plan_classified_statement(
732        &self,
733        stmt: &Statement,
734        classified: GenericHostStatement<'_>,
735    ) -> Result<LogicalPlan, PlannerError> {
736        match classified {
737            // DDL statements
738            GenericHostStatement::CreateTable(statement) => self.plan_create_table(statement),
739            GenericHostStatement::DropTable(statement) => self.plan_drop_table(statement),
740            GenericHostStatement::CreateIndex(statement) => self.plan_create_index(statement),
741            GenericHostStatement::DropIndex(statement) => self.plan_drop_index(statement),
742            GenericHostStatement::Pragma { name, value } => self.plan_pragma(name, value),
743
744            // DML statements
745            GenericHostStatement::Select(statement) => self.plan_select(statement),
746            GenericHostStatement::Insert(statement) => self.plan_insert(statement),
747            GenericHostStatement::Update(statement) => self.plan_update(statement),
748            GenericHostStatement::Delete(statement) => self.plan_delete(statement),
749            GenericHostStatement::Unsupported => Err(unsupported_generic_statement(stmt)),
750        }
751    }
752
753    fn plan_pragma(
754        &self,
755        raw_name: &str,
756        value: &Option<PragmaValue>,
757    ) -> Result<LogicalPlan, PlannerError> {
758        let name = raw_name.to_ascii_lowercase();
759        if !matches!(name.as_str(), "cache_size" | "memory_limit" | "io_stats") {
760            return Err(PlannerError::InvalidPragma {
761                name,
762                reason: "supported names are cache_size, memory_limit, and io_stats".to_string(),
763            });
764        }
765        match name.as_str() {
766            "cache_size" => match value {
767                Some(PragmaValue::Int(v)) if *v > 0 => {}
768                Some(PragmaValue::Int(_)) => {
769                    return Err(PlannerError::InvalidPragma {
770                        name,
771                        reason: "cache_size must be a positive page count".to_string(),
772                    });
773                }
774                Some(PragmaValue::Text(_)) => {
775                    return Err(PlannerError::InvalidPragma {
776                        name,
777                        reason: "cache_size requires an integer page count".to_string(),
778                    });
779                }
780                None => {}
781            },
782            "memory_limit" => {
783                if let Some(PragmaValue::Int(v)) = value
784                    && *v < 0
785                {
786                    return Err(PlannerError::InvalidPragma {
787                        name,
788                        reason: "memory_limit cannot be negative".to_string(),
789                    });
790                }
791            }
792            "io_stats" => {
793                if value.is_some() {
794                    return Err(PlannerError::InvalidPragma {
795                        name,
796                        reason: "io_stats does not accept a value".to_string(),
797                    });
798                }
799            }
800            _ => unreachable!(),
801        }
802        Ok(LogicalPlan::Pragma {
803            name,
804            value: value.clone(),
805        })
806    }
807
808    // ============================================================
809    // DDL Planning Methods (Task 16)
810    // ============================================================
811
812    /// Plan a CREATE TABLE statement.
813    ///
814    /// Validates that the table doesn't already exist (unless IF NOT EXISTS is specified),
815    /// and converts the AST column definitions to catalog metadata.
816    fn plan_create_table(&self, stmt: &CreateTable) -> Result<LogicalPlan, PlannerError> {
817        // Check if table already exists
818        if !stmt.if_not_exists && self.catalog.table_exists(&stmt.name) {
819            return Err(PlannerError::table_already_exists(&stmt.name));
820        }
821
822        // Convert column definitions to metadata
823        let columns: Vec<ColumnMetadata> = stmt
824            .columns
825            .iter()
826            .map(|col| self.convert_column_def(col))
827            .collect();
828
829        // Collect primary key from table constraints
830        let primary_key = Self::extract_primary_key(stmt);
831
832        // Build table metadata
833        // Note: table_id defaults to 0 as placeholder; Executor assigns the actual ID
834        let mut table = TableMetadata::new(stmt.name.clone(), columns);
835        if let Some(pk) = primary_key {
836            table = table.with_primary_key(pk);
837        }
838        table.catalog_name = "default".to_string();
839        table.namespace_name = "default".to_string();
840        table.table_type = TableType::Managed;
841        table.data_source_format = DataSourceFormat::Alopex;
842        table.properties = HashMap::new();
843
844        Ok(LogicalPlan::CreateTable {
845            table,
846            if_not_exists: stmt.if_not_exists,
847            with_options: stmt
848                .with_options
849                .iter()
850                .map(|opt| (opt.key.clone(), opt.value.clone()))
851                .collect(),
852        })
853    }
854
855    /// Convert an AST column definition to catalog column metadata.
856    fn convert_column_def(&self, col: &ColumnDef) -> ColumnMetadata {
857        let data_type = ResolvedType::from_ast(&col.data_type);
858        let mut meta = ColumnMetadata::new(col.name.clone(), data_type);
859
860        // Process constraints
861        for constraint in &col.constraints {
862            meta = Self::apply_column_constraint(meta, constraint);
863        }
864
865        meta
866    }
867
868    /// Apply a column constraint to column metadata.
869    fn apply_column_constraint(
870        mut meta: ColumnMetadata,
871        constraint: &ColumnConstraint,
872    ) -> ColumnMetadata {
873        match constraint {
874            ColumnConstraint::NotNull { .. } => {
875                meta.not_null = true;
876            }
877            ColumnConstraint::PrimaryKey { .. } => {
878                meta.primary_key = true;
879                meta.not_null = true; // PRIMARY KEY implies NOT NULL
880            }
881            ColumnConstraint::Unique { .. } => {
882                meta.unique = true;
883            }
884            ColumnConstraint::Default { value: expr, .. } => {
885                meta.default = Some(expr.clone());
886            }
887        }
888        meta
889    }
890
891    /// Extract primary key columns from table constraints.
892    fn extract_primary_key(stmt: &CreateTable) -> Option<Vec<String>> {
893        use crate::ast::ddl::TableConstraint;
894
895        // First check table-level constraints
896        // Note: Currently only PrimaryKey variant exists; when more variants are added,
897        // this should iterate to find the first PrimaryKey constraint
898        if let Some(TableConstraint::PrimaryKey { columns, .. }) = stmt.constraints.first() {
899            return Some(columns.clone());
900        }
901
902        // Then check column-level PRIMARY KEY constraints
903        let pk_columns: Vec<String> = stmt
904            .columns
905            .iter()
906            .filter(|col| col.constraints.iter().any(Self::is_primary_key_constraint))
907            .map(|col| col.name.clone())
908            .collect();
909
910        if pk_columns.is_empty() {
911            None
912        } else {
913            Some(pk_columns)
914        }
915    }
916
917    /// Check if a column constraint is a PRIMARY KEY constraint.
918    fn is_primary_key_constraint(constraint: &ColumnConstraint) -> bool {
919        matches!(constraint, ColumnConstraint::PrimaryKey { .. })
920    }
921
922    /// Plan a DROP TABLE statement.
923    ///
924    /// Validates that the table exists (unless IF EXISTS is specified).
925    fn plan_drop_table(&self, stmt: &DropTable) -> Result<LogicalPlan, PlannerError> {
926        // Check if table exists
927        if !stmt.if_exists && !self.table_exists_in_default(&stmt.name) {
928            return Err(PlannerError::TableNotFound {
929                name: stmt.name.clone(),
930                line: stmt.span.start.line,
931                column: stmt.span.start.column,
932            });
933        }
934
935        Ok(LogicalPlan::DropTable {
936            name: stmt.name.clone(),
937            if_exists: stmt.if_exists,
938        })
939    }
940
941    fn table_exists_in_default(&self, name: &str) -> bool {
942        match self.catalog.get_table(name) {
943            Some(table) => table.catalog_name == "default" && table.namespace_name == "default",
944            None => false,
945        }
946    }
947
948    /// Plan a CREATE INDEX statement.
949    ///
950    /// Validates that:
951    /// - The index doesn't already exist (unless IF NOT EXISTS is specified)
952    /// - The target table exists
953    /// - The target column exists in the table
954    fn plan_create_index(&self, stmt: &CreateIndex) -> Result<LogicalPlan, PlannerError> {
955        // Check if index already exists
956        if !stmt.if_not_exists && self.catalog.index_exists(&stmt.name) {
957            return Err(PlannerError::index_already_exists(&stmt.name));
958        }
959
960        // Validate table exists
961        let table = self.name_resolver.resolve_table(&stmt.table, stmt.span)?;
962
963        // Validate column exists
964        self.name_resolver
965            .resolve_column(table, &stmt.column, stmt.span)?;
966
967        // Build index metadata
968        // Note: index_id is set to 0 as placeholder; Executor assigns the actual ID
969        // Note: column_indices will be resolved by Executor when table schema is available
970        let mut index = IndexMetadata::new(
971            0,
972            stmt.name.clone(),
973            stmt.table.clone(),
974            vec![stmt.column.clone()],
975        );
976
977        if let Some(method) = stmt.method {
978            index = index.with_method(method);
979        }
980
981        let options: Vec<(String, String)> = stmt
982            .options
983            .iter()
984            .map(|opt| (opt.key.clone(), opt.value.clone()))
985            .collect();
986        if !options.is_empty() {
987            index = index.with_options(options);
988        }
989
990        Ok(LogicalPlan::CreateIndex {
991            index,
992            if_not_exists: stmt.if_not_exists,
993        })
994    }
995
996    /// Plan a DROP INDEX statement.
997    ///
998    /// Validates that the index exists (unless IF EXISTS is specified).
999    fn plan_drop_index(&self, stmt: &DropIndex) -> Result<LogicalPlan, PlannerError> {
1000        // Check if index exists
1001        if !stmt.if_exists && !self.index_exists_in_default(&stmt.name) {
1002            return Err(PlannerError::index_not_found(&stmt.name));
1003        }
1004
1005        Ok(LogicalPlan::DropIndex {
1006            name: stmt.name.clone(),
1007            if_exists: stmt.if_exists,
1008        })
1009    }
1010
1011    fn index_exists_in_default(&self, name: &str) -> bool {
1012        match self.catalog.get_index(name) {
1013            Some(index) => index.catalog_name == "default" && index.namespace_name == "default",
1014            None => false,
1015        }
1016    }
1017
1018    // ============================================================
1019    // DML Planning Methods (Task 17 & 18)
1020    // ============================================================
1021
1022    /// Plan a SELECT statement.
1023    ///
1024    /// Builds a logical plan tree: Scan -> Filter -> Sort -> Limit
1025    /// Each layer is optional and only added if the corresponding clause is present.
1026    fn plan_select(&self, stmt: &Select) -> Result<LogicalPlan, PlannerError> {
1027        self.plan_select_relation(stmt, &[], &CtePlans::new())
1028            .map(|relation| relation.plan)
1029    }
1030
1031    fn plan_ctes(
1032        &self,
1033        stmt: &Select,
1034        enclosing_ctes: &CtePlans,
1035    ) -> Result<CtePlans, PlannerError> {
1036        let Some(with) = &stmt.with else {
1037            return Ok(enclosing_ctes.clone());
1038        };
1039        if with.recursive {
1040            return Err(PlannerError::unsupported_feature(
1041                "recursive common table expressions",
1042                "a future version",
1043                with.span,
1044            ));
1045        }
1046
1047        let mut plans = enclosing_ctes.clone();
1048        let mut local_names = HashSet::new();
1049        for cte in &with.ctes {
1050            if !local_names.insert(cte.name.clone()) {
1051                return Err(PlannerError::invalid_expression(format!(
1052                    "common table expression '{}' is defined more than once",
1053                    cte.name
1054                )));
1055            }
1056            let StatementKind::Select(select) = &cte.query.kind else {
1057                return Err(PlannerError::unsupported_feature(
1058                    "non-SELECT common table expression",
1059                    "a future version",
1060                    cte.span,
1061                ));
1062            };
1063            let relation = self.plan_select_relation(select, &[], &plans)?;
1064            plans.insert(cte.name.clone(), relation);
1065        }
1066        Ok(plans)
1067    }
1068
1069    fn plan_select_relation(
1070        &self,
1071        stmt: &Select,
1072        outer_scope: &[ScopedTable],
1073        enclosing_ctes: &CtePlans,
1074    ) -> Result<PlannedRelation, PlannerError> {
1075        let ctes = self.plan_ctes(stmt, enclosing_ctes)?;
1076        if !stmt.set_operations.is_empty() {
1077            let mut left_select = stmt.clone();
1078            left_select.with = None;
1079            left_select.set_operations.clear();
1080            left_select.order_by.clear();
1081            left_select.limit = None;
1082            left_select.offset = None;
1083            let mut relation = self.plan_select_relation(&left_select, outer_scope, &ctes)?;
1084
1085            for operation in &stmt.set_operations {
1086                let right = self.plan_select_relation(&operation.right, outer_scope, &ctes)?;
1087                if relation.schema.len() != right.schema.len() {
1088                    return Err(PlannerError::set_operation_column_count_mismatch(
1089                        relation.schema.len(),
1090                        right.schema.len(),
1091                        operation.span,
1092                    ));
1093                }
1094                for (left_column, right_column) in relation.schema.iter().zip(&right.schema) {
1095                    if left_column.data_type != right_column.data_type {
1096                        return Err(PlannerError::type_mismatch(
1097                            left_column.data_type.type_name(),
1098                            right_column.data_type.type_name(),
1099                            operation.span,
1100                        ));
1101                    }
1102                }
1103
1104                relation.plan = LogicalPlan::SetOperation {
1105                    left: Box::new(relation.plan),
1106                    right: Box::new(right.plan),
1107                    operator: match operation.operator {
1108                        AstSetOperator::Union => SetOperator::Union,
1109                        AstSetOperator::Intersect => SetOperator::Intersect,
1110                        AstSetOperator::Except => SetOperator::Except,
1111                    },
1112                    all: operation.all,
1113                };
1114            }
1115
1116            relation.scope = vec![ScopedTable::new(
1117                TableMetadata::new(LITERAL_TABLE, relation.schema.clone()),
1118                0,
1119            )];
1120            if !stmt.order_by.is_empty() {
1121                // 集合演算の ORDER BY は結果列(左辺の出力列名)を参照する。
1122                // 射影別名は既に relation.schema へ反映済みなので、別名置換の
1123                // マップは空でよい。
1124                let order_by = self.build_sort_exprs_with_scope(
1125                    &stmt.order_by,
1126                    &relation.scope,
1127                    &HashMap::new(),
1128                    &ctes,
1129                )?;
1130                relation.plan = LogicalPlan::Sort {
1131                    input: Box::new(relation.plan),
1132                    order_by,
1133                };
1134            }
1135            if stmt.limit.is_some() || stmt.offset.is_some() {
1136                relation.plan = LogicalPlan::Limit {
1137                    input: Box::new(relation.plan),
1138                    limit: self.extract_limit_value(&stmt.limit, stmt.span)?,
1139                    offset: self.extract_limit_value(&stmt.offset, stmt.span)?,
1140                };
1141            }
1142            return Ok(relation);
1143        }
1144
1145        let mut relation = self.plan_from_items(&stmt.from, stmt.span, outer_scope, &ctes)?;
1146        let expr_scope = relation
1147            .scope
1148            .iter()
1149            .cloned()
1150            .chain(offset_scope(outer_scope, relation.schema.len()))
1151            .collect::<Vec<_>>();
1152
1153        let has_group_by = stmt
1154            .group_by
1155            .as_ref()
1156            .is_some_and(|items| !items.is_empty());
1157        let has_aggregate = self.select_contains_aggregate(stmt);
1158        let has_window = select_contains_window(stmt);
1159        let distinct_only =
1160            stmt.distinct && !has_group_by && !has_aggregate && stmt.having.is_none();
1161
1162        if has_window && (has_group_by || has_aggregate || stmt.having.is_some() || stmt.distinct) {
1163            return Err(PlannerError::unsupported_feature(
1164                "window functions combined with GROUP BY, ordinary aggregates, HAVING, or DISTINCT",
1165                "future",
1166                stmt.span,
1167            ));
1168        }
1169
1170        // SELECT-list aliases are visible to HAVING and ORDER BY only. `expr_scope`
1171        // above stays alias-free so that WHERE and GROUP BY keep resolving against
1172        // the FROM-derived base relations, as the SQL standard requires.
1173        let projection_aliases = collect_projection_aliases(&stmt.projection);
1174
1175        let final_projection = self.build_projection_with_scope(
1176            &stmt.projection,
1177            &relation.schema,
1178            &expr_scope,
1179            &ctes,
1180        )?;
1181        if !has_window {
1182            install_base_projection(&mut relation.plan, &final_projection);
1183        }
1184        let needs_project_boundary = !matches!(relation.plan, LogicalPlan::Scan { .. });
1185        let base_schema = relation.schema.clone();
1186        let mut plan = relation.plan;
1187
1188        // 3. Add Filter if WHERE clause is present
1189        if let Some(ref selection) = stmt.selection {
1190            let predicate = self.infer_expr_with_scope(selection, &expr_scope, &ctes)?;
1191
1192            // Verify predicate returns Boolean
1193            if predicate.resolved_type != ResolvedType::Boolean {
1194                return Err(PlannerError::type_mismatch(
1195                    "Boolean",
1196                    predicate.resolved_type.to_string(),
1197                    selection.span,
1198                ));
1199            }
1200
1201            plan = LogicalPlan::Filter {
1202                input: Box::new(plan),
1203                predicate,
1204            };
1205        }
1206
1207        if has_window {
1208            let mut windows = Vec::new();
1209            let mut window_map = HashMap::new();
1210            if let Projection::Columns(columns) = &final_projection {
1211                for column in columns {
1212                    self.collect_windows_from_typed_expr(
1213                        &column.expr,
1214                        &mut windows,
1215                        &mut window_map,
1216                    )?;
1217                }
1218            }
1219
1220            let mut outer_order_by = Vec::new();
1221            for order_expr in &stmt.order_by {
1222                let sort_source =
1223                    substitute_projection_aliases(&order_expr.expr, &projection_aliases);
1224                let typed = self.infer_expr_with_scope(&sort_source, &expr_scope, &ctes)?;
1225                self.collect_windows_from_typed_expr(&typed, &mut windows, &mut window_map)?;
1226                outer_order_by.push(SortExpr::new(
1227                    typed,
1228                    order_expr.asc.unwrap_or(true),
1229                    order_expr.nulls_first.unwrap_or(false),
1230                ));
1231            }
1232
1233            let window_names = (0..windows.len())
1234                .map(|idx| format!("__window_{idx}"))
1235                .collect::<Vec<_>>();
1236            let mut window_schema = base_schema;
1237            window_schema.extend(windows.iter().enumerate().map(|(idx, window)| {
1238                ColumnMetadata::new(window_names[idx].clone(), window.result_type.clone())
1239            }));
1240
1241            let projection = rewrite_projection_for_windows(
1242                &final_projection,
1243                &window_map,
1244                relation.schema.len(),
1245                &window_names,
1246            )?;
1247            let order_by = outer_order_by
1248                .into_iter()
1249                .map(|sort| {
1250                    Ok(SortExpr::new(
1251                        rewrite_expr_for_windows(
1252                            &sort.expr,
1253                            &window_map,
1254                            relation.schema.len(),
1255                            &window_names,
1256                        )?,
1257                        sort.asc,
1258                        sort.nulls_first,
1259                    ))
1260                })
1261                .collect::<Result<Vec<_>, PlannerError>>()?;
1262
1263            plan = LogicalPlan::Window {
1264                input: Box::new(plan),
1265                windows,
1266            };
1267            if !order_by.is_empty() {
1268                plan = LogicalPlan::Sort {
1269                    input: Box::new(plan),
1270                    order_by,
1271                };
1272            }
1273            if stmt.limit.is_some() || stmt.offset.is_some() {
1274                plan = LogicalPlan::Limit {
1275                    input: Box::new(plan),
1276                    limit: self.extract_limit_value(&stmt.limit, stmt.span)?,
1277                    offset: self.extract_limit_value(&stmt.offset, stmt.span)?,
1278                };
1279            }
1280            let output_schema = projection_schema(&projection, &window_schema);
1281            plan = LogicalPlan::Project {
1282                input: Box::new(plan),
1283                projection,
1284            };
1285            return Ok(PlannedRelation {
1286                plan,
1287                schema: output_schema.clone(),
1288                scope: vec![ScopedTable::new(
1289                    TableMetadata::new(LITERAL_TABLE, output_schema),
1290                    0,
1291                )],
1292            });
1293        }
1294
1295        if has_group_by || has_aggregate || stmt.having.is_some() || stmt.distinct {
1296            if !has_group_by && !has_aggregate && stmt.having.is_some() {
1297                return Err(PlannerError::invalid_expression(
1298                    "HAVING requires GROUP BY or aggregate functions".to_string(),
1299                ));
1300            }
1301
1302            let (group_keys, projected) = if distinct_only {
1303                let projected = self.build_projected_columns_for_distinct_with_scope(
1304                    &stmt.projection,
1305                    &relation.schema,
1306                    &expr_scope,
1307                    &ctes,
1308                )?;
1309                let group_keys = projected.iter().map(|col| col.expr.clone()).collect();
1310                (group_keys, projected)
1311            } else {
1312                let group_keys = self.build_group_keys_with_scope(stmt, &expr_scope, &ctes)?;
1313                let projected = self.build_projected_columns_for_aggregate_with_scope(
1314                    &stmt.projection,
1315                    &expr_scope,
1316                    &ctes,
1317                )?;
1318                (group_keys, projected)
1319            };
1320            let mut aggregates = Vec::new();
1321            let mut agg_map = HashMap::new();
1322
1323            for col in &projected {
1324                self.collect_aggregates_from_typed_expr(&col.expr, &mut aggregates, &mut agg_map)?;
1325            }
1326
1327            let having_typed = if let Some(having) = &stmt.having {
1328                let having = substitute_projection_aliases(having, &projection_aliases);
1329                let typed = self.infer_expr_with_scope(&having, &expr_scope, &ctes)?;
1330                if typed.resolved_type != ResolvedType::Boolean {
1331                    return Err(PlannerError::type_mismatch(
1332                        "Boolean",
1333                        typed.resolved_type.type_name().to_string(),
1334                        typed.span,
1335                    ));
1336                }
1337                self.collect_aggregates_from_typed_expr(&typed, &mut aggregates, &mut agg_map)?;
1338                Some(typed)
1339            } else {
1340                None
1341            };
1342
1343            let mut order_by = Vec::new();
1344            if !stmt.order_by.is_empty() {
1345                for order_expr in &stmt.order_by {
1346                    let sort_source =
1347                        substitute_projection_aliases(&order_expr.expr, &projection_aliases);
1348                    let typed = self.infer_expr_with_scope(&sort_source, &expr_scope, &ctes)?;
1349                    self.collect_aggregates_from_typed_expr(&typed, &mut aggregates, &mut agg_map)?;
1350                    let asc = order_expr.asc.unwrap_or(true);
1351                    let nulls_first = order_expr.nulls_first.unwrap_or(false);
1352                    order_by.push(SortExpr::new(typed, asc, nulls_first));
1353                }
1354            }
1355
1356            if let Some(ref having) = having_typed {
1357                self.type_checker
1358                    .validate_having_expr(having, &group_keys, &aggregates)?;
1359            }
1360
1361            let output_schema = build_aggregate_schema(&group_keys, &aggregates);
1362            let output_names: Vec<String> = output_schema.iter().map(|c| c.name.clone()).collect();
1363
1364            let projection = self.build_aggregate_projection(
1365                projected,
1366                &group_keys,
1367                &aggregates,
1368                &output_names,
1369            )?;
1370
1371            let having = if let Some(having) = having_typed {
1372                Some(self.rewrite_expr_for_aggregate(
1373                    &having,
1374                    &group_keys,
1375                    &aggregates,
1376                    &output_names,
1377                )?)
1378            } else {
1379                None
1380            };
1381
1382            let order_by = order_by
1383                .into_iter()
1384                .map(|expr| {
1385                    let rewritten = self.rewrite_expr_for_aggregate(
1386                        &expr.expr,
1387                        &group_keys,
1388                        &aggregates,
1389                        &output_names,
1390                    )?;
1391                    Ok(SortExpr::new(rewritten, expr.asc, expr.nulls_first))
1392                })
1393                .collect::<Result<Vec<_>, PlannerError>>()?;
1394
1395            let schema = projection_schema(&projection, &output_schema);
1396            plan = LogicalPlan::Aggregate {
1397                input: Box::new(plan),
1398                group_keys,
1399                aggregates,
1400                having,
1401                projection,
1402            };
1403
1404            if !order_by.is_empty() {
1405                plan = LogicalPlan::Sort {
1406                    input: Box::new(plan),
1407                    order_by,
1408                };
1409            }
1410
1411            if stmt.limit.is_some() || stmt.offset.is_some() {
1412                let limit = self.extract_limit_value(&stmt.limit, stmt.span)?;
1413                let offset = self.extract_limit_value(&stmt.offset, stmt.span)?;
1414                plan = LogicalPlan::Limit {
1415                    input: Box::new(plan),
1416                    limit,
1417                    offset,
1418                };
1419            }
1420
1421            return Ok(PlannedRelation {
1422                plan,
1423                schema: schema.clone(),
1424                scope: vec![ScopedTable::new(
1425                    TableMetadata::new(LITERAL_TABLE, schema),
1426                    0,
1427                )],
1428            });
1429        }
1430
1431        // Non-aggregate path: ORDER BY + LIMIT/OFFSET
1432        if !stmt.order_by.is_empty() {
1433            let order_by = self.build_sort_exprs_with_scope(
1434                &stmt.order_by,
1435                &expr_scope,
1436                &projection_aliases,
1437                &ctes,
1438            )?;
1439            plan = LogicalPlan::Sort {
1440                input: Box::new(plan),
1441                order_by,
1442            };
1443        }
1444
1445        if stmt.limit.is_some() || stmt.offset.is_some() {
1446            let limit = self.extract_limit_value(&stmt.limit, stmt.span)?;
1447            let offset = self.extract_limit_value(&stmt.offset, stmt.span)?;
1448            plan = LogicalPlan::Limit {
1449                input: Box::new(plan),
1450                limit,
1451                offset,
1452            };
1453        }
1454
1455        let output_schema = projection_schema(&final_projection, &relation.schema);
1456        if needs_project_boundary {
1457            plan = LogicalPlan::Project {
1458                input: Box::new(plan),
1459                projection: final_projection,
1460            };
1461        }
1462        Ok(PlannedRelation {
1463            plan,
1464            schema: output_schema.clone(),
1465            scope: vec![ScopedTable::new(
1466                TableMetadata::new(LITERAL_TABLE, output_schema),
1467                0,
1468            )],
1469        })
1470    }
1471
1472    /// Build the projection for a SELECT statement.
1473    ///
1474    /// Handles wildcard expansion and expression type checking.
1475    fn plan_from_items(
1476        &self,
1477        items: &[FromItem],
1478        select_span: crate::ast::Span,
1479        outer_scope: &[ScopedTable],
1480        ctes: &CtePlans,
1481    ) -> Result<PlannedRelation, PlannerError> {
1482        match items {
1483            [] => {
1484                let schema = Vec::new();
1485                Ok(PlannedRelation {
1486                    plan: LogicalPlan::Scan {
1487                        table: LITERAL_TABLE.to_string(),
1488                        projection: Projection::All(Vec::new()),
1489                    },
1490                    schema: schema.clone(),
1491                    scope: vec![ScopedTable::new(
1492                        TableMetadata::new(LITERAL_TABLE, schema),
1493                        0,
1494                    )],
1495                })
1496            }
1497            [single] => self.plan_from_item(single, 0, outer_scope, ctes),
1498            [first, rest @ ..] => {
1499                let mut relation = self.plan_from_item(first, 0, outer_scope, ctes)?;
1500                for item in rest {
1501                    let right =
1502                        self.plan_from_item(item, relation.schema.len(), outer_scope, ctes)?;
1503                    relation = self.combine_join_relation(
1504                        relation,
1505                        right,
1506                        JoinType::Cross,
1507                        None,
1508                        None,
1509                        select_span,
1510                    )?;
1511                }
1512                Ok(relation)
1513            }
1514        }
1515    }
1516
1517    fn plan_from_item(
1518        &self,
1519        item: &FromItem,
1520        start_index: usize,
1521        outer_scope: &[ScopedTable],
1522        ctes: &CtePlans,
1523    ) -> Result<PlannedRelation, PlannerError> {
1524        match item {
1525            FromItem::Table { name, alias, span } => {
1526                if let Some(cte) = ctes.get(name) {
1527                    let mut relation = cte.clone();
1528                    relation.plan = LogicalPlan::Project {
1529                        input: Box::new(relation.plan),
1530                        projection: Projection::All(
1531                            relation.schema.iter().map(|col| col.name.clone()).collect(),
1532                        ),
1533                    };
1534                    relation.scope = vec![ScopedTable::new(
1535                        TableMetadata::new(
1536                            alias.clone().unwrap_or_else(|| name.clone()),
1537                            relation.schema.clone(),
1538                        ),
1539                        start_index,
1540                    )];
1541                    return Ok(relation);
1542                }
1543                let table = self.name_resolver.resolve_table(name, *span)?.clone();
1544                let mut scope_table = table.clone();
1545                if let Some(alias) = alias {
1546                    scope_table.name = alias.clone();
1547                }
1548                let schema = table.columns.clone();
1549                Ok(PlannedRelation {
1550                    plan: LogicalPlan::Scan {
1551                        table: name.clone(),
1552                        projection: Projection::All(
1553                            schema.iter().map(|col| col.name.clone()).collect(),
1554                        ),
1555                    },
1556                    schema,
1557                    scope: vec![ScopedTable::new(scope_table, start_index)],
1558                })
1559            }
1560            FromItem::Join {
1561                left,
1562                right,
1563                join_type,
1564                condition,
1565                using,
1566                natural,
1567                span,
1568            } => {
1569                let left_relation = self.plan_from_item(left, start_index, outer_scope, ctes)?;
1570                let right_relation = self.plan_from_item(
1571                    right,
1572                    start_index + left_relation.schema.len(),
1573                    outer_scope,
1574                    ctes,
1575                )?;
1576                let expr_scope = left_relation
1577                    .scope
1578                    .iter()
1579                    .cloned()
1580                    .chain(right_relation.scope.iter().cloned())
1581                    .chain(offset_scope(
1582                        outer_scope,
1583                        left_relation.schema.len() + right_relation.schema.len(),
1584                    ))
1585                    .collect::<Vec<_>>();
1586                let using = if *natural {
1587                    Some(natural_join_columns(
1588                        &left_relation.schema,
1589                        &right_relation.schema,
1590                    ))
1591                } else {
1592                    using.clone()
1593                };
1594                let typed_condition = if let Some(expr) = condition {
1595                    let typed = self.infer_expr_with_scope(expr, &expr_scope, ctes)?;
1596                    if typed.resolved_type != ResolvedType::Boolean {
1597                        return Err(PlannerError::type_mismatch(
1598                            "Boolean",
1599                            typed.resolved_type.to_string(),
1600                            expr.span,
1601                        ));
1602                    }
1603                    Some(typed)
1604                } else {
1605                    self.build_using_condition(
1606                        using.as_deref(),
1607                        &left_relation,
1608                        &right_relation,
1609                        *span,
1610                    )?
1611                };
1612                self.combine_join_relation(
1613                    left_relation,
1614                    right_relation,
1615                    map_join_type(*join_type),
1616                    typed_condition,
1617                    using,
1618                    *span,
1619                )
1620            }
1621            FromItem::Derived {
1622                subquery,
1623                alias,
1624                span,
1625            } => {
1626                let crate::ast::StatementKind::Select(select) = &subquery.kind else {
1627                    return Err(PlannerError::unsupported_feature(
1628                        "non-SELECT derived table",
1629                        "v0.6.0-subquery Phase 6",
1630                        *span,
1631                    ));
1632                };
1633                // A derived table is evaluated independently of the query it
1634                // sits in, so nothing from the enclosing scopes is visible
1635                // inside it. Only LATERAL lifts that restriction, and Alopex
1636                // does not accept LATERAL yet. Passing `outer_scope` through
1637                // here would resolve an outer name into a correlated reference
1638                // the user never wrote, so the scope stops at this boundary.
1639                let mut relation = self.plan_select_relation(select, &[], ctes)?;
1640                let alias = alias.clone().ok_or_else(|| {
1641                    PlannerError::invalid_expression("derived table requires an alias".to_string())
1642                })?;
1643                relation.plan = LogicalPlan::Project {
1644                    input: Box::new(relation.plan),
1645                    projection: Projection::All(
1646                        relation.schema.iter().map(|col| col.name.clone()).collect(),
1647                    ),
1648                };
1649                relation.scope = vec![ScopedTable::new(
1650                    TableMetadata::new(alias, relation.schema.clone()),
1651                    start_index,
1652                )];
1653                Ok(relation)
1654            }
1655        }
1656    }
1657
1658    fn combine_join_relation(
1659        &self,
1660        left: PlannedRelation,
1661        right: PlannedRelation,
1662        join_type: JoinType,
1663        condition: Option<TypedExpr>,
1664        using: Option<Vec<String>>,
1665        _span: crate::ast::Span,
1666    ) -> Result<PlannedRelation, PlannerError> {
1667        let mut schema = left.schema.clone();
1668        schema.extend(right.schema.clone());
1669        let mut scope = left.scope.clone();
1670        let mut right_scope = right.scope.clone();
1671        if let Some(columns) = &using {
1672            // The right-hand copy of a common column stops being an unqualified
1673            // candidate, and the surviving left-hand column records where its
1674            // partner lives so that an unqualified reference can merge the two.
1675            for column in columns {
1676                let right_index = right_scope.iter().find_map(|table| {
1677                    table
1678                        .table
1679                        .get_column_index(column)
1680                        .map(|index| table.start_index + index)
1681                });
1682                let Some(right_index) = right_index else {
1683                    continue;
1684                };
1685                for table in &mut scope {
1686                    if table.table.get_column_index(column).is_some() {
1687                        table.merge_column_with(column, right_index);
1688                    }
1689                }
1690            }
1691            for table in &mut right_scope {
1692                table.hide_unqualified_columns(columns);
1693            }
1694        }
1695        scope.extend(right_scope);
1696        Ok(PlannedRelation {
1697            plan: LogicalPlan::Join {
1698                left: Box::new(left.plan),
1699                right: Box::new(right.plan),
1700                join_type,
1701                condition,
1702                using,
1703            },
1704            schema,
1705            scope,
1706        })
1707    }
1708
1709    fn build_using_condition(
1710        &self,
1711        using: Option<&[String]>,
1712        left: &PlannedRelation,
1713        right: &PlannedRelation,
1714        span: crate::ast::Span,
1715    ) -> Result<Option<TypedExpr>, PlannerError> {
1716        let Some(columns) = using else {
1717            return Ok(None);
1718        };
1719        let mut condition = None;
1720        for column in columns {
1721            let left_col = find_scoped_column(&left.scope, column, span)?;
1722            let right_col = find_scoped_column(&right.scope, column, span)?;
1723            let left_expr = merged_scoped_column_expr(&left_col, column, span);
1724            let right_expr = merged_scoped_column_expr(&right_col, column, span);
1725            self.type_checker
1726                .check_comparison_op(&left_col.ty, &right_col.ty, span)?;
1727            let eq = TypedExpr::binary_op(
1728                left_expr,
1729                crate::ast::expr::BinaryOp::Eq,
1730                right_expr,
1731                ResolvedType::Boolean,
1732                span,
1733            );
1734            condition = Some(match condition {
1735                Some(prev) => TypedExpr::binary_op(
1736                    prev,
1737                    crate::ast::expr::BinaryOp::And,
1738                    eq,
1739                    ResolvedType::Boolean,
1740                    span,
1741                ),
1742                None => eq,
1743            });
1744        }
1745        Ok(condition)
1746    }
1747
1748    fn infer_expr_with_scope(
1749        &self,
1750        expr: &crate::ast::expr::Expr,
1751        scope: &[ScopedTable],
1752        ctes: &CtePlans,
1753    ) -> Result<TypedExpr, PlannerError> {
1754        self.type_checker
1755            .infer_type_with_scope(expr, scope, &|stmt, outer_scope| {
1756                let crate::ast::StatementKind::Select(select) = &stmt.kind else {
1757                    return Err(PlannerError::unsupported_feature(
1758                        "non-SELECT subquery",
1759                        "v0.6.0-subquery Phase 6",
1760                        stmt.span(),
1761                    ));
1762                };
1763                let relation = self.plan_select_relation(select, outer_scope, ctes)?;
1764                Ok((relation.plan, relation.schema))
1765            })
1766    }
1767
1768    #[allow(dead_code)]
1769    fn build_projection(
1770        &self,
1771        items: &[SelectItem],
1772        table: &TableMetadata,
1773    ) -> Result<Projection, PlannerError> {
1774        // Check for wildcard - if present, expand it
1775        if items.len() == 1 && matches!(&items[0], SelectItem::Wildcard { .. }) {
1776            let columns = self.name_resolver.expand_wildcard(table);
1777            return Ok(Projection::All(columns));
1778        }
1779
1780        // Process each select item
1781        let mut projected_columns = Vec::new();
1782        for item in items {
1783            match item {
1784                SelectItem::Wildcard { span } => {
1785                    // Wildcard mixed with other items - expand inline
1786                    for col in &table.columns {
1787                        let column_index = table.get_column_index(&col.name).unwrap();
1788                        let typed_expr = TypedExpr::column_ref(
1789                            table.name.clone(),
1790                            col.name.clone(),
1791                            column_index,
1792                            col.data_type.clone(),
1793                            *span,
1794                        );
1795                        projected_columns.push(ProjectedColumn::new(typed_expr));
1796                    }
1797                }
1798                SelectItem::QualifiedWildcard {
1799                    table: qualifier,
1800                    span,
1801                } => {
1802                    if qualifier != &table.name {
1803                        return Err(PlannerError::invalid_expression(format!(
1804                            "table '{qualifier}' is not available for wildcard projection"
1805                        )));
1806                    }
1807                    for col in &table.columns {
1808                        let column_index = table.get_column_index(&col.name).unwrap();
1809                        projected_columns.push(ProjectedColumn::new(TypedExpr::column_ref(
1810                            table.name.clone(),
1811                            col.name.clone(),
1812                            column_index,
1813                            col.data_type.clone(),
1814                            *span,
1815                        )));
1816                    }
1817                }
1818                SelectItem::Expr { expr, alias, .. } => {
1819                    let typed_expr = self.type_checker.infer_type(expr, table)?;
1820                    let projected = if let Some(alias) = alias {
1821                        ProjectedColumn::with_alias(typed_expr, alias.clone())
1822                    } else {
1823                        ProjectedColumn::new(typed_expr)
1824                    };
1825                    projected_columns.push(projected);
1826                }
1827            }
1828        }
1829
1830        Ok(Projection::Columns(projected_columns))
1831    }
1832
1833    fn build_projection_with_scope(
1834        &self,
1835        items: &[SelectItem],
1836        schema: &[ColumnMetadata],
1837        scope: &[ScopedTable],
1838        ctes: &CtePlans,
1839    ) -> Result<Projection, PlannerError> {
1840        if items.len() == 1 && matches!(&items[0], SelectItem::Wildcard { .. }) {
1841            return Ok(Projection::All(visible_wildcard_columns(schema, scope)));
1842        }
1843
1844        let mut projected_columns = Vec::new();
1845        for item in items {
1846            match item {
1847                SelectItem::Wildcard { span } => {
1848                    for scoped in scope {
1849                        for (local_idx, col) in scoped.table.columns.iter().enumerate() {
1850                            projected_columns.push(ProjectedColumn::new(TypedExpr::column_ref(
1851                                scoped.table.name.clone(),
1852                                col.name.clone(),
1853                                scoped.start_index + local_idx,
1854                                col.data_type.clone(),
1855                                *span,
1856                            )));
1857                        }
1858                    }
1859                }
1860                SelectItem::QualifiedWildcard { table, span } => {
1861                    let scoped = scope
1862                        .iter()
1863                        .filter(|scoped| scoped.table.name == *table)
1864                        .collect::<Vec<_>>();
1865                    match scoped.as_slice() {
1866                        [] => {
1867                            return Err(PlannerError::invalid_expression(format!(
1868                                "table '{table}' is not available for wildcard projection"
1869                            )));
1870                        }
1871                        [scoped] => {
1872                            for (local_idx, col) in scoped.table.columns.iter().enumerate() {
1873                                projected_columns.push(ProjectedColumn::new(
1874                                    TypedExpr::column_ref(
1875                                        scoped.table.name.clone(),
1876                                        col.name.clone(),
1877                                        scoped.start_index + local_idx,
1878                                        col.data_type.clone(),
1879                                        *span,
1880                                    ),
1881                                ));
1882                            }
1883                        }
1884                        _ => {
1885                            return Err(PlannerError::ambiguous_column(
1886                                table,
1887                                scoped
1888                                    .iter()
1889                                    .map(|scoped| scoped.table.name.clone())
1890                                    .collect(),
1891                                *span,
1892                            ));
1893                        }
1894                    }
1895                }
1896                SelectItem::Expr { expr, alias, .. } => {
1897                    let typed_expr = self.infer_expr_with_scope(expr, scope, ctes)?;
1898                    let projected = if let Some(alias) = alias {
1899                        ProjectedColumn::with_alias(typed_expr, alias.clone())
1900                    } else {
1901                        ProjectedColumn::new(typed_expr)
1902                    };
1903                    projected_columns.push(projected);
1904                }
1905            }
1906        }
1907
1908        Ok(Projection::Columns(projected_columns))
1909    }
1910
1911    /// Build sort expressions from ORDER BY clause.
1912    #[allow(dead_code)]
1913    fn build_sort_exprs(
1914        &self,
1915        order_by: &[OrderByExpr],
1916        table: &TableMetadata,
1917    ) -> Result<Vec<SortExpr>, PlannerError> {
1918        let mut sort_exprs = Vec::new();
1919
1920        for order_expr in order_by {
1921            let typed_expr = self.type_checker.infer_type(&order_expr.expr, table)?;
1922
1923            // Determine sort direction (default: ASC)
1924            let asc = order_expr.asc.unwrap_or(true);
1925
1926            // Determine NULLS ordering (default: NULLS LAST for both ASC and DESC)
1927            let nulls_first = order_expr.nulls_first.unwrap_or(false);
1928
1929            sort_exprs.push(SortExpr::new(typed_expr, asc, nulls_first));
1930        }
1931
1932        Ok(sort_exprs)
1933    }
1934
1935    fn build_sort_exprs_with_scope(
1936        &self,
1937        order_by: &[OrderByExpr],
1938        scope: &[ScopedTable],
1939        projection_aliases: &HashMap<String, crate::ast::expr::Expr>,
1940        ctes: &CtePlans,
1941    ) -> Result<Vec<SortExpr>, PlannerError> {
1942        let mut sort_exprs = Vec::new();
1943        for order_expr in order_by {
1944            let sort_source = substitute_projection_aliases(&order_expr.expr, projection_aliases);
1945            let typed_expr = self.infer_expr_with_scope(&sort_source, scope, ctes)?;
1946            let asc = order_expr.asc.unwrap_or(true);
1947            let nulls_first = order_expr.nulls_first.unwrap_or(false);
1948            sort_exprs.push(SortExpr::new(typed_expr, asc, nulls_first));
1949        }
1950        Ok(sort_exprs)
1951    }
1952
1953    fn select_contains_aggregate(&self, stmt: &Select) -> bool {
1954        stmt.projection.iter().any(|item| match item {
1955            SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => false,
1956            SelectItem::Expr { expr, .. } => expr_contains_aggregate(expr),
1957        }) || stmt
1958            .group_by
1959            .as_ref()
1960            .map(|items| items.iter().any(expr_contains_aggregate))
1961            .unwrap_or(false)
1962            || stmt
1963                .having
1964                .as_ref()
1965                .map(expr_contains_aggregate)
1966                .unwrap_or(false)
1967            || stmt
1968                .order_by
1969                .iter()
1970                .any(|order| expr_contains_aggregate(&order.expr))
1971    }
1972
1973    #[allow(dead_code)]
1974    fn build_group_keys(
1975        &self,
1976        stmt: &Select,
1977        table: &TableMetadata,
1978    ) -> Result<Vec<TypedExpr>, PlannerError> {
1979        let mut keys = Vec::new();
1980        if let Some(items) = &stmt.group_by {
1981            for expr in items {
1982                let typed = self.type_checker.infer_type(expr, table)?;
1983                if typed_expr_contains_aggregate(&typed) {
1984                    return Err(PlannerError::invalid_expression(
1985                        "GROUP BY cannot contain aggregate functions".to_string(),
1986                    ));
1987                }
1988                if !matches!(typed.kind, TypedExprKind::ColumnRef { .. }) {
1989                    return Err(PlannerError::invalid_expression(
1990                        "GROUP BY expressions must be column references".to_string(),
1991                    ));
1992                }
1993                keys.push(typed);
1994            }
1995        }
1996        Ok(keys)
1997    }
1998
1999    fn build_group_keys_with_scope(
2000        &self,
2001        stmt: &Select,
2002        scope: &[ScopedTable],
2003        ctes: &CtePlans,
2004    ) -> Result<Vec<TypedExpr>, PlannerError> {
2005        let mut keys = Vec::new();
2006        if let Some(items) = &stmt.group_by {
2007            for expr in items {
2008                let typed = self.infer_expr_with_scope(expr, scope, ctes)?;
2009                if typed_expr_contains_aggregate(&typed) {
2010                    return Err(PlannerError::invalid_expression(
2011                        "GROUP BY cannot contain aggregate functions".to_string(),
2012                    ));
2013                }
2014                if !matches!(typed.kind, TypedExprKind::ColumnRef { .. }) {
2015                    return Err(PlannerError::invalid_expression(
2016                        "GROUP BY expressions must be column references".to_string(),
2017                    ));
2018                }
2019                keys.push(typed);
2020            }
2021        }
2022        Ok(keys)
2023    }
2024
2025    #[allow(dead_code)]
2026    fn build_projected_columns_for_aggregate(
2027        &self,
2028        items: &[SelectItem],
2029        table: &TableMetadata,
2030    ) -> Result<Vec<ProjectedColumn>, PlannerError> {
2031        let mut projected = Vec::new();
2032        for item in items {
2033            match item {
2034                SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => {
2035                    return Err(PlannerError::invalid_expression(
2036                        "wildcard projection not supported with GROUP BY/aggregate".to_string(),
2037                    ));
2038                }
2039                SelectItem::Expr { expr, alias, .. } => {
2040                    let typed = self.type_checker.infer_type(expr, table)?;
2041                    projected.push(ProjectedColumn {
2042                        expr: typed,
2043                        alias: alias.clone(),
2044                    });
2045                }
2046            }
2047        }
2048        Ok(projected)
2049    }
2050
2051    fn build_projected_columns_for_aggregate_with_scope(
2052        &self,
2053        items: &[SelectItem],
2054        scope: &[ScopedTable],
2055        ctes: &CtePlans,
2056    ) -> Result<Vec<ProjectedColumn>, PlannerError> {
2057        let mut projected = Vec::new();
2058        for item in items {
2059            match item {
2060                SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => {
2061                    return Err(PlannerError::invalid_expression(
2062                        "wildcard projection not supported with GROUP BY/aggregate".to_string(),
2063                    ));
2064                }
2065                SelectItem::Expr { expr, alias, .. } => {
2066                    let typed = self.infer_expr_with_scope(expr, scope, ctes)?;
2067                    projected.push(ProjectedColumn {
2068                        expr: typed,
2069                        alias: alias.clone(),
2070                    });
2071                }
2072            }
2073        }
2074        Ok(projected)
2075    }
2076
2077    #[allow(dead_code)]
2078    fn build_projected_columns_for_distinct(
2079        &self,
2080        items: &[SelectItem],
2081        table: &TableMetadata,
2082    ) -> Result<Vec<ProjectedColumn>, PlannerError> {
2083        let projection = self.build_projection(items, table)?;
2084        match projection {
2085            Projection::All(columns) => {
2086                let mut projected = Vec::with_capacity(columns.len());
2087                for column in columns {
2088                    let column_index = table.get_column_index(&column).ok_or_else(|| {
2089                        PlannerError::invalid_expression(format!(
2090                            "column '{column}' not found for DISTINCT projection"
2091                        ))
2092                    })?;
2093                    let column_meta = table.get_column(&column).ok_or_else(|| {
2094                        PlannerError::invalid_expression(format!(
2095                            "column '{column}' not found for DISTINCT projection"
2096                        ))
2097                    })?;
2098                    let typed_expr = TypedExpr::column_ref(
2099                        table.name.clone(),
2100                        column.clone(),
2101                        column_index,
2102                        column_meta.data_type.clone(),
2103                        crate::ast::Span::default(),
2104                    );
2105                    projected.push(ProjectedColumn::new(typed_expr));
2106                }
2107                Ok(projected)
2108            }
2109            Projection::Columns(columns) => Ok(columns),
2110        }
2111    }
2112
2113    fn build_projected_columns_for_distinct_with_scope(
2114        &self,
2115        items: &[SelectItem],
2116        schema: &[ColumnMetadata],
2117        scope: &[ScopedTable],
2118        ctes: &CtePlans,
2119    ) -> Result<Vec<ProjectedColumn>, PlannerError> {
2120        let projection = self.build_projection_with_scope(items, schema, scope, ctes)?;
2121        match projection {
2122            Projection::All(columns) => {
2123                let mut projected = Vec::with_capacity(columns.len());
2124                for (idx, column) in columns.into_iter().enumerate() {
2125                    let column_meta = schema.get(idx).ok_or_else(|| {
2126                        PlannerError::invalid_expression(format!(
2127                            "column '{column}' not found for DISTINCT projection"
2128                        ))
2129                    })?;
2130                    projected.push(ProjectedColumn::new(TypedExpr::column_ref(
2131                        LITERAL_TABLE.to_string(),
2132                        column,
2133                        idx,
2134                        column_meta.data_type.clone(),
2135                        crate::ast::Span::default(),
2136                    )));
2137                }
2138                Ok(projected)
2139            }
2140            Projection::Columns(columns) => Ok(columns),
2141        }
2142    }
2143
2144    fn collect_aggregates_from_typed_expr(
2145        &self,
2146        expr: &TypedExpr,
2147        aggregates: &mut Vec<AggregateExpr>,
2148        aggregate_map: &mut HashMap<AggregateSignature, usize>,
2149    ) -> Result<(), PlannerError> {
2150        match &expr.kind {
2151            TypedExprKind::FunctionCall {
2152                name,
2153                args,
2154                distinct,
2155                star,
2156                over: None,
2157            } if is_aggregate_function(name) => {
2158                for arg in args {
2159                    if typed_expr_contains_aggregate(arg) {
2160                        return Err(PlannerError::invalid_expression(
2161                            "nested aggregate functions are not supported".to_string(),
2162                        ));
2163                    }
2164                }
2165                let (agg, signature) =
2166                    self.build_aggregate_expr_from_typed(expr, name, args, *distinct, *star)?;
2167                aggregate_map.entry(signature).or_insert_with(|| {
2168                    aggregates.push(agg);
2169                    aggregates.len() - 1
2170                });
2171                Ok(())
2172            }
2173            TypedExprKind::BinaryOp { left, right, .. } => {
2174                self.collect_aggregates_from_typed_expr(left, aggregates, aggregate_map)?;
2175                self.collect_aggregates_from_typed_expr(right, aggregates, aggregate_map)?;
2176                Ok(())
2177            }
2178            TypedExprKind::UnaryOp { operand, .. } => {
2179                self.collect_aggregates_from_typed_expr(operand, aggregates, aggregate_map)
2180            }
2181            TypedExprKind::Cast { expr, .. } => {
2182                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)
2183            }
2184            TypedExprKind::Case {
2185                operand,
2186                branches,
2187                else_expr,
2188            } => {
2189                if let Some(operand) = operand {
2190                    self.collect_aggregates_from_typed_expr(operand, aggregates, aggregate_map)?;
2191                }
2192                for branch in branches {
2193                    self.collect_aggregates_from_typed_expr(
2194                        &branch.when,
2195                        aggregates,
2196                        aggregate_map,
2197                    )?;
2198                    self.collect_aggregates_from_typed_expr(
2199                        &branch.then,
2200                        aggregates,
2201                        aggregate_map,
2202                    )?;
2203                }
2204                if let Some(else_expr) = else_expr {
2205                    self.collect_aggregates_from_typed_expr(else_expr, aggregates, aggregate_map)?;
2206                }
2207                Ok(())
2208            }
2209            TypedExprKind::FunctionCall { args, .. } => {
2210                for arg in args {
2211                    self.collect_aggregates_from_typed_expr(arg, aggregates, aggregate_map)?;
2212                }
2213                Ok(())
2214            }
2215            TypedExprKind::Between {
2216                expr, low, high, ..
2217            } => {
2218                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)?;
2219                self.collect_aggregates_from_typed_expr(low, aggregates, aggregate_map)?;
2220                self.collect_aggregates_from_typed_expr(high, aggregates, aggregate_map)?;
2221                Ok(())
2222            }
2223            TypedExprKind::Like {
2224                expr,
2225                pattern,
2226                escape,
2227                ..
2228            } => {
2229                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)?;
2230                self.collect_aggregates_from_typed_expr(pattern, aggregates, aggregate_map)?;
2231                if let Some(esc) = escape {
2232                    self.collect_aggregates_from_typed_expr(esc, aggregates, aggregate_map)?;
2233                }
2234                Ok(())
2235            }
2236            TypedExprKind::InList { expr, list, .. } => {
2237                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)?;
2238                for item in list {
2239                    self.collect_aggregates_from_typed_expr(item, aggregates, aggregate_map)?;
2240                }
2241                Ok(())
2242            }
2243            TypedExprKind::IsNull { expr, .. } => {
2244                self.collect_aggregates_from_typed_expr(expr, aggregates, aggregate_map)
2245            }
2246            _ => Ok(()),
2247        }
2248    }
2249
2250    fn collect_windows_from_typed_expr(
2251        &self,
2252        expr: &TypedExpr,
2253        windows: &mut Vec<WindowExpr>,
2254        window_map: &mut HashMap<String, usize>,
2255    ) -> Result<(), PlannerError> {
2256        match &expr.kind {
2257            TypedExprKind::FunctionCall {
2258                name,
2259                args,
2260                distinct,
2261                star,
2262                over: Some(over),
2263            } => {
2264                if args.iter().any(typed_expr_contains_window)
2265                    || over.partition_by.iter().any(typed_expr_contains_window)
2266                    || over
2267                        .order_by
2268                        .iter()
2269                        .any(|sort| typed_expr_contains_window(&sort.expr))
2270                {
2271                    return Err(PlannerError::invalid_expression(
2272                        "nested window functions are not supported".to_string(),
2273                    ));
2274                }
2275
2276                let key = expr_key(expr);
2277                if window_map.contains_key(&key) {
2278                    return Ok(());
2279                }
2280                let function = match name.to_ascii_lowercase().as_str() {
2281                    "row_number" => WindowFunction::RowNumber,
2282                    "rank" => WindowFunction::Rank,
2283                    "dense_rank" => WindowFunction::DenseRank,
2284                    "sum" | "count" | "avg" | "min" | "max" => {
2285                        let (aggregate, _) = self
2286                            .build_aggregate_expr_from_typed(expr, name, args, *distinct, *star)?;
2287                        WindowFunction::Aggregate(aggregate)
2288                    }
2289                    "lag" | "lead" => {
2290                        return Err(PlannerError::unsupported_feature(
2291                            format!("{} window function", name.to_ascii_uppercase()),
2292                            "future",
2293                            expr.span,
2294                        ));
2295                    }
2296                    _ => {
2297                        return Err(PlannerError::unsupported_feature(
2298                            format!("function '{}' with OVER", name),
2299                            "future",
2300                            expr.span,
2301                        ));
2302                    }
2303                };
2304                let index = windows.len();
2305                windows.push(WindowExpr {
2306                    function,
2307                    partition_by: over.partition_by.clone(),
2308                    order_by: over.order_by.clone(),
2309                    result_type: expr.resolved_type.clone(),
2310                });
2311                window_map.insert(key, index);
2312                Ok(())
2313            }
2314            TypedExprKind::FunctionCall { args, .. } => {
2315                for arg in args {
2316                    self.collect_windows_from_typed_expr(arg, windows, window_map)?;
2317                }
2318                Ok(())
2319            }
2320            TypedExprKind::BinaryOp { left, right, .. } => {
2321                self.collect_windows_from_typed_expr(left, windows, window_map)?;
2322                self.collect_windows_from_typed_expr(right, windows, window_map)
2323            }
2324            TypedExprKind::UnaryOp { operand, .. } => {
2325                self.collect_windows_from_typed_expr(operand, windows, window_map)
2326            }
2327            TypedExprKind::Cast { expr, .. } | TypedExprKind::IsNull { expr, .. } => {
2328                self.collect_windows_from_typed_expr(expr, windows, window_map)
2329            }
2330            TypedExprKind::Between {
2331                expr, low, high, ..
2332            } => {
2333                self.collect_windows_from_typed_expr(expr, windows, window_map)?;
2334                self.collect_windows_from_typed_expr(low, windows, window_map)?;
2335                self.collect_windows_from_typed_expr(high, windows, window_map)
2336            }
2337            TypedExprKind::Like {
2338                expr,
2339                pattern,
2340                escape,
2341                ..
2342            } => {
2343                self.collect_windows_from_typed_expr(expr, windows, window_map)?;
2344                self.collect_windows_from_typed_expr(pattern, windows, window_map)?;
2345                if let Some(escape) = escape {
2346                    self.collect_windows_from_typed_expr(escape, windows, window_map)?;
2347                }
2348                Ok(())
2349            }
2350            TypedExprKind::InList { expr, list, .. } => {
2351                self.collect_windows_from_typed_expr(expr, windows, window_map)?;
2352                for item in list {
2353                    self.collect_windows_from_typed_expr(item, windows, window_map)?;
2354                }
2355                Ok(())
2356            }
2357            _ => Ok(()),
2358        }
2359    }
2360
2361    fn build_aggregate_expr_from_typed(
2362        &self,
2363        expr: &TypedExpr,
2364        name: &str,
2365        args: &[TypedExpr],
2366        distinct: bool,
2367        star: bool,
2368    ) -> Result<(AggregateExpr, AggregateSignature), PlannerError> {
2369        let lower = name.to_lowercase();
2370        match lower.as_str() {
2371            "count" => {
2372                if star {
2373                    let agg = AggregateExpr::count_star();
2374                    let signature = aggregate_signature(name, distinct, star, None, None, expr);
2375                    return Ok((agg, signature));
2376                }
2377                if args.len() != 1 {
2378                    return Err(PlannerError::type_mismatch(
2379                        "1 argument",
2380                        format!("{} arguments", args.len()),
2381                        expr.span,
2382                    ));
2383                }
2384                let agg = AggregateExpr {
2385                    function: AggregateFunction::Count,
2386                    arg: Some(args[0].clone()),
2387                    distinct,
2388                    result_type: ResolvedType::BigInt,
2389                };
2390                let signature =
2391                    aggregate_signature(name, distinct, star, Some(&args[0]), None, expr);
2392                Ok((agg, signature))
2393            }
2394            "sum" => {
2395                let arg = self.require_single_aggregate_arg(args, expr.span)?;
2396                let agg = AggregateExpr {
2397                    function: AggregateFunction::Sum,
2398                    arg: Some(arg.clone()),
2399                    distinct,
2400                    result_type: crate::planner::aggregate_expr::sum_result_type(
2401                        &arg.resolved_type,
2402                    ),
2403                };
2404                let signature = aggregate_signature(name, distinct, star, Some(arg), None, expr);
2405                Ok((agg, signature))
2406            }
2407            "total" => {
2408                let arg = self.require_single_aggregate_arg(args, expr.span)?;
2409                let agg = AggregateExpr {
2410                    function: AggregateFunction::Total,
2411                    arg: Some(arg.clone()),
2412                    distinct: false,
2413                    result_type: ResolvedType::Double,
2414                };
2415                let signature = aggregate_signature(name, false, star, Some(arg), None, expr);
2416                Ok((agg, signature))
2417            }
2418            "avg" => {
2419                let arg = self.require_single_aggregate_arg(args, expr.span)?;
2420                let agg = AggregateExpr {
2421                    function: AggregateFunction::Avg,
2422                    arg: Some(arg.clone()),
2423                    distinct,
2424                    result_type: ResolvedType::Double,
2425                };
2426                let signature = aggregate_signature(name, distinct, star, Some(arg), None, expr);
2427                Ok((agg, signature))
2428            }
2429            "min" => {
2430                let arg = self.require_single_aggregate_arg(args, expr.span)?;
2431                let agg = AggregateExpr {
2432                    function: AggregateFunction::Min,
2433                    arg: Some(arg.clone()),
2434                    distinct,
2435                    result_type: arg.resolved_type.clone(),
2436                };
2437                let signature = aggregate_signature(name, distinct, star, Some(arg), None, expr);
2438                Ok((agg, signature))
2439            }
2440            "max" => {
2441                let arg = self.require_single_aggregate_arg(args, expr.span)?;
2442                let agg = AggregateExpr {
2443                    function: AggregateFunction::Max,
2444                    arg: Some(arg.clone()),
2445                    distinct,
2446                    result_type: arg.resolved_type.clone(),
2447                };
2448                let signature = aggregate_signature(name, distinct, star, Some(arg), None, expr);
2449                Ok((agg, signature))
2450            }
2451            "group_concat" => {
2452                if args.is_empty() || args.len() > 2 {
2453                    return Err(PlannerError::type_mismatch(
2454                        "1 or 2 arguments",
2455                        format!("{} arguments", args.len()),
2456                        expr.span,
2457                    ));
2458                }
2459                let arg = &args[0];
2460                let mut separator = None;
2461                if args.len() == 2 {
2462                    if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
2463                        separator = Some(value.clone());
2464                    } else {
2465                        return Err(PlannerError::invalid_expression(
2466                            "GROUP_CONCAT separator must be a string literal".to_string(),
2467                        ));
2468                    }
2469                }
2470                let agg = AggregateExpr {
2471                    function: AggregateFunction::GroupConcat { separator },
2472                    arg: Some(arg.clone()),
2473                    distinct,
2474                    result_type: ResolvedType::Text,
2475                };
2476                let signature = aggregate_signature(
2477                    name,
2478                    distinct,
2479                    star,
2480                    Some(arg),
2481                    match &agg.function {
2482                        AggregateFunction::GroupConcat { separator } => separator.as_ref(),
2483                        _ => None,
2484                    },
2485                    expr,
2486                );
2487                Ok((agg, signature))
2488            }
2489            "string_agg" => {
2490                if args.len() != 2 {
2491                    return Err(PlannerError::type_mismatch(
2492                        "2 arguments",
2493                        format!("{} arguments", args.len()),
2494                        expr.span,
2495                    ));
2496                }
2497                let arg = &args[0];
2498                let separator =
2499                    if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
2500                        Some(value.clone())
2501                    } else {
2502                        return Err(PlannerError::invalid_expression(
2503                            "STRING_AGG separator must be a string literal".to_string(),
2504                        ));
2505                    };
2506                let agg = AggregateExpr {
2507                    function: AggregateFunction::StringAgg { separator },
2508                    arg: Some(arg.clone()),
2509                    distinct,
2510                    result_type: ResolvedType::Text,
2511                };
2512                let signature = aggregate_signature(
2513                    name,
2514                    distinct,
2515                    star,
2516                    Some(arg),
2517                    match &agg.function {
2518                        AggregateFunction::StringAgg { separator } => separator.as_ref(),
2519                        _ => None,
2520                    },
2521                    expr,
2522                );
2523                Ok((agg, signature))
2524            }
2525            _ => Err(PlannerError::unsupported_feature(
2526                format!("function '{}'", name),
2527                "future",
2528                expr.span,
2529            )),
2530        }
2531    }
2532
2533    fn require_single_aggregate_arg<'b>(
2534        &self,
2535        args: &'b [TypedExpr],
2536        span: crate::ast::Span,
2537    ) -> Result<&'b TypedExpr, PlannerError> {
2538        if args.len() != 1 {
2539            return Err(PlannerError::type_mismatch(
2540                "1 argument",
2541                format!("{} arguments", args.len()),
2542                span,
2543            ));
2544        }
2545        Ok(&args[0])
2546    }
2547
2548    fn build_aggregate_projection(
2549        &self,
2550        projected: Vec<ProjectedColumn>,
2551        group_keys: &[TypedExpr],
2552        aggregates: &[AggregateExpr],
2553        output_names: &[String],
2554    ) -> Result<Projection, PlannerError> {
2555        let mut columns = Vec::new();
2556        for col in projected {
2557            let rewritten =
2558                self.rewrite_expr_for_aggregate(&col.expr, group_keys, aggregates, output_names)?;
2559            columns.push(ProjectedColumn {
2560                expr: rewritten,
2561                alias: col.alias,
2562            });
2563        }
2564        Ok(Projection::Columns(columns))
2565    }
2566
2567    fn rewrite_expr_for_aggregate(
2568        &self,
2569        expr: &TypedExpr,
2570        group_keys: &[TypedExpr],
2571        aggregates: &[AggregateExpr],
2572        output_names: &[String],
2573    ) -> Result<TypedExpr, PlannerError> {
2574        let group_key_map = build_group_key_map(group_keys);
2575        let aggregate_map = build_aggregate_map(aggregates);
2576
2577        rewrite_expr_with_maps(expr, &group_key_map, &aggregate_map, output_names)
2578    }
2579
2580    /// Extract a numeric value from a LIMIT or OFFSET expression.
2581    ///
2582    /// Currently only supports literal integer values.
2583    fn extract_limit_value(
2584        &self,
2585        expr: &Option<crate::ast::expr::Expr>,
2586        stmt_span: crate::ast::Span,
2587    ) -> Result<Option<u64>, PlannerError> {
2588        match expr {
2589            None => Ok(None),
2590            Some(e) => {
2591                // For now, only support literal integers
2592                if let crate::ast::expr::ExprKind::Literal {
2593                    literal: Literal::Number(s),
2594                } = &e.kind
2595                {
2596                    s.parse::<u64>().map(Some).map_err(|_| {
2597                        PlannerError::type_mismatch("unsigned integer", s.clone(), e.span)
2598                    })
2599                } else {
2600                    Err(PlannerError::unsupported_feature(
2601                        "non-literal LIMIT/OFFSET",
2602                        "v0.3.0+",
2603                        stmt_span,
2604                    ))
2605                }
2606            }
2607        }
2608    }
2609
2610    /// Plan an INSERT statement.
2611    ///
2612    /// Handles column list specification or implicit column ordering.
2613    /// When columns are omitted, uses table definition order from TableMetadata.
2614    fn plan_insert(&self, stmt: &Insert) -> Result<LogicalPlan, PlannerError> {
2615        // Resolve the target table
2616        let table = self.name_resolver.resolve_table(&stmt.table, stmt.span)?;
2617
2618        // Determine the column list
2619        let columns: Vec<String> = if let Some(ref cols) = stmt.columns {
2620            // Explicit column list - validate each column exists
2621            for col in cols {
2622                self.name_resolver.resolve_column(table, col, stmt.span)?;
2623            }
2624            cols.clone()
2625        } else {
2626            // Implicit - use all columns in table definition order
2627            table.column_names().into_iter().map(String::from).collect()
2628        };
2629
2630        match &stmt.source {
2631            InsertSource::Values { values } => {
2632                let mut typed_values: Vec<Vec<TypedExpr>> = Vec::new();
2633
2634                for row in values {
2635                    if row.len() != columns.len() {
2636                        return Err(PlannerError::column_value_count_mismatch(
2637                            columns.len(),
2638                            row.len(),
2639                            stmt.span,
2640                        ));
2641                    }
2642
2643                    typed_values.push(self.type_check_insert_values(row, &columns, table)?);
2644                }
2645
2646                Ok(LogicalPlan::Insert {
2647                    table: table.name.clone(),
2648                    columns,
2649                    values: typed_values,
2650                })
2651            }
2652            InsertSource::Select { select } => {
2653                let source = self.plan_select_relation(select, &[], &CtePlans::new())?;
2654                if source.schema.len() != columns.len() {
2655                    return Err(PlannerError::column_value_count_mismatch(
2656                        columns.len(),
2657                        source.schema.len(),
2658                        stmt.span,
2659                    ));
2660                }
2661
2662                for (source_column, target_column) in source.schema.iter().zip(&columns) {
2663                    let target = table
2664                        .get_column(target_column)
2665                        .expect("validated target column");
2666                    if target.not_null && source_column.data_type == ResolvedType::Null {
2667                        return Err(PlannerError::null_constraint_violation(
2668                            target_column,
2669                            stmt.span,
2670                        ));
2671                    }
2672                    self.validate_resolved_type_assignment(
2673                        &source_column.data_type,
2674                        &target.data_type,
2675                        stmt.span,
2676                    )?;
2677                }
2678
2679                Ok(LogicalPlan::InsertSelect {
2680                    table: table.name.clone(),
2681                    columns,
2682                    source: Box::new(source.plan),
2683                })
2684            }
2685        }
2686    }
2687
2688    /// Type-check INSERT values against column definitions.
2689    fn type_check_insert_values(
2690        &self,
2691        values: &[crate::ast::expr::Expr],
2692        columns: &[String],
2693        table: &TableMetadata,
2694    ) -> Result<Vec<TypedExpr>, PlannerError> {
2695        let mut typed_values = Vec::new();
2696
2697        for (i, value) in values.iter().enumerate() {
2698            let column_name = &columns[i];
2699            let column_meta = table.get_column(column_name).ok_or_else(|| {
2700                PlannerError::column_not_found(column_name, &table.name, value.span)
2701            })?;
2702
2703            // Type-check the value expression
2704            let typed_value = self.type_checker.infer_type(value, table)?;
2705
2706            // Check for NOT NULL constraint violation (except for NULL literal which is allowed if nullable)
2707            if column_meta.not_null
2708                && matches!(&typed_value.kind, TypedExprKind::Literal(Literal::Null))
2709            {
2710                return Err(PlannerError::null_constraint_violation(
2711                    column_name,
2712                    value.span,
2713                ));
2714            }
2715
2716            // Validate type compatibility
2717            self.validate_type_assignment(&typed_value, &column_meta.data_type, value.span)?;
2718
2719            let typed_value =
2720                self.coerce_assignment_value(typed_value, &column_meta.data_type, value.span);
2721
2722            typed_values.push(typed_value);
2723        }
2724
2725        Ok(typed_values)
2726    }
2727
2728    /// Validate that a value type can be assigned to a column type.
2729    fn validate_type_assignment(
2730        &self,
2731        value: &TypedExpr,
2732        target_type: &ResolvedType,
2733        span: crate::ast::Span,
2734    ) -> Result<(), PlannerError> {
2735        self.validate_resolved_type_assignment(&value.resolved_type, target_type, span)
2736    }
2737
2738    fn validate_resolved_type_assignment(
2739        &self,
2740        source_type: &ResolvedType,
2741        target_type: &ResolvedType,
2742        span: crate::ast::Span,
2743    ) -> Result<(), PlannerError> {
2744        // NULL can be assigned to any nullable column
2745        if *source_type == ResolvedType::Null {
2746            return Ok(());
2747        }
2748
2749        // Check for exact match or implicit conversion compatibility
2750        if self.types_compatible(source_type, target_type) {
2751            return Ok(());
2752        }
2753
2754        Err(PlannerError::type_mismatch(
2755            target_type.to_string(),
2756            source_type.to_string(),
2757            span,
2758        ))
2759    }
2760
2761    /// Check if two types are compatible for assignment.
2762    fn types_compatible(&self, source: &ResolvedType, target: &ResolvedType) -> bool {
2763        use ResolvedType::*;
2764
2765        // Same type is always compatible
2766        if source == target {
2767            return true;
2768        }
2769
2770        // Numeric promotions
2771        match (source, target) {
2772            // Integer can be assigned to BigInt, Float, Double
2773            (Integer, BigInt) | (Integer, Float) | (Integer, Double) => true,
2774            // BigInt can be assigned to Float, Double
2775            (BigInt, Float) | (BigInt, Double) => true,
2776            // Float can be assigned to Double
2777            (Float, Double) => true,
2778            // A decimal literal is typed DOUBLE, so a FLOAT column needs this
2779            // narrowing; the value is rounded to f32 at execution time.
2780            (Double, Float) => true,
2781            // TIMESTAMP is stored as microseconds; text and numeric input is
2782            // converted by the assignment expression at execution time.
2783            (Text | Integer | BigInt | Float | Double, Timestamp) => true,
2784            // Vector dimensions must match
2785            (Vector { dimension: d1, .. }, Vector { dimension: d2, .. }) => d1 == d2,
2786            _ => false,
2787        }
2788    }
2789
2790    fn coerce_assignment_value(
2791        &self,
2792        value: TypedExpr,
2793        target_type: &ResolvedType,
2794        span: crate::ast::Span,
2795    ) -> TypedExpr {
2796        if value.resolved_type != *target_type
2797            && value.resolved_type != ResolvedType::Null
2798            && matches!(
2799                target_type,
2800                ResolvedType::Integer
2801                    | ResolvedType::BigInt
2802                    | ResolvedType::Float
2803                    | ResolvedType::Double
2804                    | ResolvedType::Timestamp
2805            )
2806        {
2807            TypedExpr::cast(value, target_type.clone(), span)
2808        } else {
2809            value
2810        }
2811    }
2812
2813    /// Plan an UPDATE statement.
2814    ///
2815    /// Validates assignments and optional WHERE clause.
2816    fn plan_update(&self, stmt: &Update) -> Result<LogicalPlan, PlannerError> {
2817        // Resolve the target table
2818        let table = self.name_resolver.resolve_table(&stmt.table, stmt.span)?;
2819
2820        // Process assignments
2821        let mut typed_assignments = Vec::new();
2822
2823        for assignment in &stmt.assignments {
2824            // Resolve the column
2825            let column_meta =
2826                self.name_resolver
2827                    .resolve_column(table, &assignment.column, assignment.span)?;
2828            let column_index = table.get_column_index(&assignment.column).unwrap();
2829
2830            // Type-check the value expression
2831            let typed_value = self.type_checker.infer_type(&assignment.value, table)?;
2832
2833            // Check NOT NULL constraint
2834            if column_meta.not_null
2835                && matches!(&typed_value.kind, TypedExprKind::Literal(Literal::Null))
2836            {
2837                return Err(PlannerError::null_constraint_violation(
2838                    &assignment.column,
2839                    assignment.value.span,
2840                ));
2841            }
2842
2843            // Validate type compatibility
2844            self.validate_type_assignment(
2845                &typed_value,
2846                &column_meta.data_type,
2847                assignment.value.span,
2848            )?;
2849
2850            let typed_value = self.coerce_assignment_value(
2851                typed_value,
2852                &column_meta.data_type,
2853                assignment.value.span,
2854            );
2855
2856            typed_assignments.push(TypedAssignment::new(
2857                assignment.column.clone(),
2858                column_index,
2859                typed_value,
2860            ));
2861        }
2862
2863        // Process optional WHERE clause
2864        let filter = if let Some(ref selection) = stmt.selection {
2865            let predicate = self.type_checker.infer_type(selection, table)?;
2866
2867            // Verify predicate returns Boolean
2868            if predicate.resolved_type != ResolvedType::Boolean {
2869                return Err(PlannerError::type_mismatch(
2870                    "Boolean",
2871                    predicate.resolved_type.to_string(),
2872                    selection.span,
2873                ));
2874            }
2875
2876            Some(predicate)
2877        } else {
2878            None
2879        };
2880
2881        Ok(LogicalPlan::Update {
2882            table: table.name.clone(),
2883            assignments: typed_assignments,
2884            filter,
2885        })
2886    }
2887
2888    /// Plan a DELETE statement.
2889    ///
2890    /// Validates optional WHERE clause.
2891    fn plan_delete(&self, stmt: &Delete) -> Result<LogicalPlan, PlannerError> {
2892        // Resolve the target table
2893        let table = self.name_resolver.resolve_table(&stmt.table, stmt.span)?;
2894
2895        // Process optional WHERE clause
2896        let filter = if let Some(ref selection) = stmt.selection {
2897            let predicate = self.type_checker.infer_type(selection, table)?;
2898
2899            // Verify predicate returns Boolean
2900            if predicate.resolved_type != ResolvedType::Boolean {
2901                return Err(PlannerError::type_mismatch(
2902                    "Boolean",
2903                    predicate.resolved_type.to_string(),
2904                    selection.span,
2905                ));
2906            }
2907
2908            Some(predicate)
2909        } else {
2910            None
2911        };
2912
2913        Ok(LogicalPlan::Delete {
2914            table: table.name.clone(),
2915            filter,
2916        })
2917    }
2918}
2919
2920#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2921struct AggregateSignature {
2922    name: String,
2923    distinct: bool,
2924    star: bool,
2925    arg_key: Option<String>,
2926    separator: Option<String>,
2927}
2928
2929/// Collect the SELECT-list aliases that ORDER BY / HAVING may reference.
2930///
2931/// Per the SQL standard, aliases introduced by the projection are visible to
2932/// HAVING and ORDER BY (which are logically evaluated after the projection),
2933/// but not to WHERE / GROUP BY. Only `SelectItem::Expr` carries an alias;
2934/// wildcards contribute nothing.
2935///
2936/// When the same alias is declared twice the first declaration wins, which
2937/// keeps the substitution deterministic instead of depending on map ordering.
2938fn collect_projection_aliases(items: &[SelectItem]) -> HashMap<String, crate::ast::expr::Expr> {
2939    let mut aliases = HashMap::new();
2940    for item in items {
2941        if let SelectItem::Expr {
2942            expr,
2943            alias: Some(alias),
2944            ..
2945        } = item
2946        {
2947            aliases.entry(alias.clone()).or_insert_with(|| expr.clone());
2948        }
2949    }
2950    aliases
2951}
2952
2953/// Substitute projection aliases inside an ORDER BY / HAVING expression.
2954///
2955/// An unqualified `ColumnRef` whose name matches a projection alias is replaced
2956/// by the aliased source expression, so everything downstream (type inference,
2957/// aggregate collection, `validate_having_expr`, and the aggregate output
2958/// rewrite) observes the very expression the projection already produced.
2959///
2960/// Substitution rules:
2961/// - Only unqualified references are eligible; `t.total` always means the base
2962///   column `total` of table `t`, never an alias.
2963/// - An alias takes precedence over a base column of the same name, per the
2964///   SQL standard. `order_by_prefers_projection_alias_over_shadowed_base_column`
2965///   pins this behaviour.
2966/// - The substituted expression keeps the *reference* site's span so that any
2967///   resulting diagnostic still points at the ORDER BY / HAVING clause.
2968/// - Subqueries are not descended into: an inner SELECT establishes its own
2969///   projection scope, so the outer alias must not leak inside it.
2970fn substitute_projection_aliases(
2971    expr: &crate::ast::expr::Expr,
2972    aliases: &HashMap<String, crate::ast::expr::Expr>,
2973) -> crate::ast::expr::Expr {
2974    use crate::ast::expr::ExprKind;
2975
2976    if aliases.is_empty() {
2977        return expr.clone();
2978    }
2979
2980    let recurse = |e: &crate::ast::expr::Expr| substitute_projection_aliases(e, aliases);
2981
2982    let kind = match &expr.kind {
2983        ExprKind::ColumnRef {
2984            table: None,
2985            column,
2986        } => match aliases.get(column) {
2987            Some(source) => {
2988                let mut replacement = source.clone();
2989                replacement.span = expr.span;
2990                return replacement;
2991            }
2992            None => return expr.clone(),
2993        },
2994        ExprKind::BinaryOp { left, op, right } => ExprKind::BinaryOp {
2995            left: Box::new(recurse(left)),
2996            op: *op,
2997            right: Box::new(recurse(right)),
2998        },
2999        ExprKind::UnaryOp { op, operand } => ExprKind::UnaryOp {
3000            op: *op,
3001            operand: Box::new(recurse(operand)),
3002        },
3003        ExprKind::FunctionCall {
3004            name,
3005            args,
3006            distinct,
3007            star,
3008            over,
3009        } => ExprKind::FunctionCall {
3010            name: name.clone(),
3011            args: args.iter().map(recurse).collect(),
3012            distinct: *distinct,
3013            star: *star,
3014            over: over.as_ref().map(|window| crate::ast::expr::WindowSpec {
3015                partition_by: window.partition_by.iter().map(recurse).collect(),
3016                order_by: window
3017                    .order_by
3018                    .iter()
3019                    .map(|order| OrderByExpr {
3020                        expr: recurse(&order.expr),
3021                        asc: order.asc,
3022                        nulls_first: order.nulls_first,
3023                        span: order.span,
3024                    })
3025                    .collect(),
3026            }),
3027        },
3028        ExprKind::Case {
3029            operand,
3030            branches,
3031            else_expr,
3032        } => ExprKind::Case {
3033            operand: operand.as_deref().map(|e| Box::new(recurse(e))),
3034            branches: branches
3035                .iter()
3036                .map(|branch| crate::ast::expr::CaseWhen {
3037                    when: recurse(&branch.when),
3038                    then: recurse(&branch.then),
3039                })
3040                .collect(),
3041            else_expr: else_expr.as_deref().map(|e| Box::new(recurse(e))),
3042        },
3043        ExprKind::Cast { expr, target_type } => ExprKind::Cast {
3044            expr: Box::new(recurse(expr)),
3045            target_type: target_type.clone(),
3046        },
3047        ExprKind::Between {
3048            expr,
3049            low,
3050            high,
3051            negated,
3052        } => ExprKind::Between {
3053            expr: Box::new(recurse(expr)),
3054            low: Box::new(recurse(low)),
3055            high: Box::new(recurse(high)),
3056            negated: *negated,
3057        },
3058        ExprKind::Like {
3059            expr,
3060            pattern,
3061            escape,
3062            negated,
3063            kind,
3064        } => ExprKind::Like {
3065            expr: Box::new(recurse(expr)),
3066            pattern: Box::new(recurse(pattern)),
3067            escape: escape.as_deref().map(|e| Box::new(recurse(e))),
3068            negated: *negated,
3069            kind: *kind,
3070        },
3071        ExprKind::InList {
3072            expr,
3073            list,
3074            negated,
3075        } => ExprKind::InList {
3076            expr: Box::new(recurse(expr)),
3077            list: list.iter().map(recurse).collect(),
3078            negated: *negated,
3079        },
3080        ExprKind::IsNull { expr, negated } => ExprKind::IsNull {
3081            expr: Box::new(recurse(expr)),
3082            negated: *negated,
3083        },
3084        // Qualified column refs, literals, and subquery-bearing expressions are
3085        // left untouched (see the subquery note above).
3086        ExprKind::ColumnRef { .. }
3087        | ExprKind::Literal { .. }
3088        | ExprKind::VectorLiteral { .. }
3089        | ExprKind::ScalarSubquery { .. }
3090        | ExprKind::InSubquery { .. }
3091        | ExprKind::Exists { .. }
3092        | ExprKind::Quantified { .. } => return expr.clone(),
3093    };
3094
3095    crate::ast::expr::Expr {
3096        kind,
3097        span: expr.span,
3098    }
3099}
3100
3101fn expr_contains_aggregate(expr: &crate::ast::expr::Expr) -> bool {
3102    use crate::ast::expr::ExprKind;
3103
3104    match &expr.kind {
3105        ExprKind::FunctionCall {
3106            name, args, over, ..
3107        } => {
3108            if over.is_none() && is_aggregate_function(name) {
3109                return true;
3110            }
3111            args.iter().any(expr_contains_aggregate)
3112                || over.as_ref().is_some_and(|window| {
3113                    window.partition_by.iter().any(expr_contains_aggregate)
3114                        || window
3115                            .order_by
3116                            .iter()
3117                            .any(|sort| expr_contains_aggregate(&sort.expr))
3118                })
3119        }
3120        ExprKind::BinaryOp { left, right, .. } => {
3121            expr_contains_aggregate(left) || expr_contains_aggregate(right)
3122        }
3123        ExprKind::UnaryOp { operand, .. } => expr_contains_aggregate(operand),
3124        ExprKind::Case {
3125            operand,
3126            branches,
3127            else_expr,
3128        } => {
3129            operand.as_deref().is_some_and(expr_contains_aggregate)
3130                || branches.iter().any(|branch| {
3131                    expr_contains_aggregate(&branch.when) || expr_contains_aggregate(&branch.then)
3132                })
3133                || else_expr.as_deref().is_some_and(expr_contains_aggregate)
3134        }
3135        ExprKind::Cast { expr, .. } => expr_contains_aggregate(expr),
3136        ExprKind::Between {
3137            expr, low, high, ..
3138        } => {
3139            expr_contains_aggregate(expr)
3140                || expr_contains_aggregate(low)
3141                || expr_contains_aggregate(high)
3142        }
3143        ExprKind::Like {
3144            expr,
3145            pattern,
3146            escape,
3147            ..
3148        } => {
3149            expr_contains_aggregate(expr)
3150                || expr_contains_aggregate(pattern)
3151                || escape.as_deref().is_some_and(expr_contains_aggregate)
3152        }
3153        ExprKind::InList { expr, list, .. } => {
3154            expr_contains_aggregate(expr) || list.iter().any(expr_contains_aggregate)
3155        }
3156        ExprKind::IsNull { expr, .. } => expr_contains_aggregate(expr),
3157        ExprKind::ScalarSubquery { .. }
3158        | ExprKind::InSubquery { .. }
3159        | ExprKind::Exists { .. }
3160        | ExprKind::Quantified { .. }
3161        | ExprKind::Literal { .. }
3162        | ExprKind::VectorLiteral { .. }
3163        | ExprKind::ColumnRef { .. } => false,
3164    }
3165}
3166
3167fn typed_expr_contains_aggregate(expr: &TypedExpr) -> bool {
3168    match &expr.kind {
3169        TypedExprKind::FunctionCall {
3170            name, args, over, ..
3171        } => {
3172            if over.is_none() && is_aggregate_function(name) {
3173                return true;
3174            }
3175            args.iter().any(typed_expr_contains_aggregate)
3176                || over.as_ref().is_some_and(|window| {
3177                    window
3178                        .partition_by
3179                        .iter()
3180                        .any(typed_expr_contains_aggregate)
3181                        || window
3182                            .order_by
3183                            .iter()
3184                            .any(|sort| typed_expr_contains_aggregate(&sort.expr))
3185                })
3186        }
3187        TypedExprKind::BinaryOp { left, right, .. } => {
3188            typed_expr_contains_aggregate(left) || typed_expr_contains_aggregate(right)
3189        }
3190        TypedExprKind::UnaryOp { operand, .. } => typed_expr_contains_aggregate(operand),
3191        TypedExprKind::Cast { expr, .. } => typed_expr_contains_aggregate(expr),
3192        TypedExprKind::Case {
3193            operand,
3194            branches,
3195            else_expr,
3196        } => {
3197            operand
3198                .as_deref()
3199                .is_some_and(typed_expr_contains_aggregate)
3200                || branches.iter().any(|branch| {
3201                    typed_expr_contains_aggregate(&branch.when)
3202                        || typed_expr_contains_aggregate(&branch.then)
3203                })
3204                || else_expr
3205                    .as_deref()
3206                    .is_some_and(typed_expr_contains_aggregate)
3207        }
3208        TypedExprKind::Between {
3209            expr, low, high, ..
3210        } => {
3211            typed_expr_contains_aggregate(expr)
3212                || typed_expr_contains_aggregate(low)
3213                || typed_expr_contains_aggregate(high)
3214        }
3215        TypedExprKind::Like {
3216            expr,
3217            pattern,
3218            escape,
3219            ..
3220        } => {
3221            typed_expr_contains_aggregate(expr)
3222                || typed_expr_contains_aggregate(pattern)
3223                || escape
3224                    .as_ref()
3225                    .is_some_and(|inner| typed_expr_contains_aggregate(inner))
3226        }
3227        TypedExprKind::InList { expr, list, .. } => {
3228            typed_expr_contains_aggregate(expr) || list.iter().any(typed_expr_contains_aggregate)
3229        }
3230        TypedExprKind::IsNull { expr, .. } => typed_expr_contains_aggregate(expr),
3231        TypedExprKind::InSubquery { expr, .. } => typed_expr_contains_aggregate(expr),
3232        TypedExprKind::Quantified { expr, .. } => typed_expr_contains_aggregate(expr),
3233        TypedExprKind::ScalarSubquery(_) | TypedExprKind::Exists { .. } => false,
3234        _ => false,
3235    }
3236}
3237
3238fn select_contains_window(stmt: &Select) -> bool {
3239    stmt.projection.iter().any(|item| match item {
3240        SelectItem::Wildcard { .. } | SelectItem::QualifiedWildcard { .. } => false,
3241        SelectItem::Expr { expr, .. } => expr_contains_window(expr),
3242    }) || stmt
3243        .order_by
3244        .iter()
3245        .any(|order| expr_contains_window(&order.expr))
3246}
3247
3248fn expr_contains_window(expr: &crate::ast::expr::Expr) -> bool {
3249    match &expr.kind {
3250        crate::ast::expr::ExprKind::FunctionCall { args, over, .. } => {
3251            over.is_some() || args.iter().any(expr_contains_window)
3252        }
3253        crate::ast::expr::ExprKind::BinaryOp { left, right, .. } => {
3254            expr_contains_window(left) || expr_contains_window(right)
3255        }
3256        crate::ast::expr::ExprKind::UnaryOp { operand, .. }
3257        | crate::ast::expr::ExprKind::Cast { expr: operand, .. }
3258        | crate::ast::expr::ExprKind::IsNull { expr: operand, .. } => expr_contains_window(operand),
3259        crate::ast::expr::ExprKind::Between {
3260            expr, low, high, ..
3261        } => expr_contains_window(expr) || expr_contains_window(low) || expr_contains_window(high),
3262        crate::ast::expr::ExprKind::Like {
3263            expr,
3264            pattern,
3265            escape,
3266            ..
3267        } => {
3268            expr_contains_window(expr)
3269                || expr_contains_window(pattern)
3270                || escape.as_deref().is_some_and(expr_contains_window)
3271        }
3272        crate::ast::expr::ExprKind::InList { expr, list, .. } => {
3273            expr_contains_window(expr) || list.iter().any(expr_contains_window)
3274        }
3275        _ => false,
3276    }
3277}
3278
3279fn typed_expr_contains_window(expr: &TypedExpr) -> bool {
3280    match &expr.kind {
3281        TypedExprKind::FunctionCall { args, over, .. } => {
3282            over.is_some() || args.iter().any(typed_expr_contains_window)
3283        }
3284        TypedExprKind::BinaryOp { left, right, .. } => {
3285            typed_expr_contains_window(left) || typed_expr_contains_window(right)
3286        }
3287        TypedExprKind::UnaryOp { operand, .. }
3288        | TypedExprKind::Cast { expr: operand, .. }
3289        | TypedExprKind::IsNull { expr: operand, .. } => typed_expr_contains_window(operand),
3290        TypedExprKind::Between {
3291            expr, low, high, ..
3292        } => {
3293            typed_expr_contains_window(expr)
3294                || typed_expr_contains_window(low)
3295                || typed_expr_contains_window(high)
3296        }
3297        TypedExprKind::Like {
3298            expr,
3299            pattern,
3300            escape,
3301            ..
3302        } => {
3303            typed_expr_contains_window(expr)
3304                || typed_expr_contains_window(pattern)
3305                || escape.as_deref().is_some_and(typed_expr_contains_window)
3306        }
3307        TypedExprKind::InList { expr, list, .. } => {
3308            typed_expr_contains_window(expr) || list.iter().any(typed_expr_contains_window)
3309        }
3310        _ => false,
3311    }
3312}
3313
3314fn rewrite_projection_for_windows(
3315    projection: &Projection,
3316    window_map: &HashMap<String, usize>,
3317    base_width: usize,
3318    window_names: &[String],
3319) -> Result<Projection, PlannerError> {
3320    match projection {
3321        Projection::All(names) => Ok(Projection::All(names.clone())),
3322        Projection::Columns(columns) => Ok(Projection::Columns(
3323            columns
3324                .iter()
3325                .map(|column| {
3326                    Ok(ProjectedColumn {
3327                        expr: rewrite_expr_for_windows(
3328                            &column.expr,
3329                            window_map,
3330                            base_width,
3331                            window_names,
3332                        )?,
3333                        alias: column.alias.clone(),
3334                    })
3335                })
3336                .collect::<Result<Vec<_>, PlannerError>>()?,
3337        )),
3338    }
3339}
3340
3341fn rewrite_expr_for_windows(
3342    expr: &TypedExpr,
3343    window_map: &HashMap<String, usize>,
3344    base_width: usize,
3345    window_names: &[String],
3346) -> Result<TypedExpr, PlannerError> {
3347    if let Some(index) = window_map.get(&expr_key(expr)) {
3348        return Ok(TypedExpr::column_ref(
3349            "__window__".to_string(),
3350            window_names
3351                .get(*index)
3352                .cloned()
3353                .unwrap_or_else(|| format!("__window_{index}")),
3354            base_width + index,
3355            expr.resolved_type.clone(),
3356            expr.span,
3357        ));
3358    }
3359
3360    let rewrite =
3361        |inner: &TypedExpr| rewrite_expr_for_windows(inner, window_map, base_width, window_names);
3362    let kind = match &expr.kind {
3363        TypedExprKind::FunctionCall {
3364            name,
3365            args,
3366            distinct,
3367            star,
3368            over,
3369        } => {
3370            if over.is_some() {
3371                return Err(PlannerError::invalid_expression(
3372                    "window expression is not part of the window plan".to_string(),
3373                ));
3374            }
3375            TypedExprKind::FunctionCall {
3376                name: name.clone(),
3377                args: args.iter().map(rewrite).collect::<Result<Vec<_>, _>>()?,
3378                distinct: *distinct,
3379                star: *star,
3380                over: None,
3381            }
3382        }
3383        TypedExprKind::BinaryOp { left, op, right } => TypedExprKind::BinaryOp {
3384            left: Box::new(rewrite(left)?),
3385            op: *op,
3386            right: Box::new(rewrite(right)?),
3387        },
3388        TypedExprKind::UnaryOp { op, operand } => TypedExprKind::UnaryOp {
3389            op: *op,
3390            operand: Box::new(rewrite(operand)?),
3391        },
3392        TypedExprKind::Cast {
3393            expr: inner,
3394            target_type,
3395        } => TypedExprKind::Cast {
3396            expr: Box::new(rewrite(inner)?),
3397            target_type: target_type.clone(),
3398        },
3399        TypedExprKind::Between {
3400            expr: inner,
3401            low,
3402            high,
3403            negated,
3404        } => TypedExprKind::Between {
3405            expr: Box::new(rewrite(inner)?),
3406            low: Box::new(rewrite(low)?),
3407            high: Box::new(rewrite(high)?),
3408            negated: *negated,
3409        },
3410        TypedExprKind::Like {
3411            expr: inner,
3412            pattern,
3413            escape,
3414            negated,
3415            kind,
3416        } => TypedExprKind::Like {
3417            expr: Box::new(rewrite(inner)?),
3418            pattern: Box::new(rewrite(pattern)?),
3419            escape: escape.as_deref().map(rewrite).transpose()?.map(Box::new),
3420            negated: *negated,
3421            kind: *kind,
3422        },
3423        TypedExprKind::InList {
3424            expr: inner,
3425            list,
3426            negated,
3427        } => TypedExprKind::InList {
3428            expr: Box::new(rewrite(inner)?),
3429            list: list.iter().map(rewrite).collect::<Result<Vec<_>, _>>()?,
3430            negated: *negated,
3431        },
3432        TypedExprKind::IsNull {
3433            expr: inner,
3434            negated,
3435        } => TypedExprKind::IsNull {
3436            expr: Box::new(rewrite(inner)?),
3437            negated: *negated,
3438        },
3439        _ => return Ok(expr.clone()),
3440    };
3441    Ok(TypedExpr {
3442        kind,
3443        resolved_type: expr.resolved_type.clone(),
3444        span: expr.span,
3445    })
3446}
3447
3448fn map_join_type(join_type: crate::ast::dml::JoinType) -> JoinType {
3449    match join_type {
3450        crate::ast::dml::JoinType::Inner => JoinType::Inner,
3451        crate::ast::dml::JoinType::Left => JoinType::Left,
3452        crate::ast::dml::JoinType::Right => JoinType::Right,
3453        crate::ast::dml::JoinType::Full => JoinType::Full,
3454        crate::ast::dml::JoinType::Cross => JoinType::Cross,
3455    }
3456}
3457
3458struct FoundScopedColumn {
3459    table: String,
3460    index: usize,
3461    ty: ResolvedType,
3462    partner_indices: Vec<usize>,
3463}
3464
3465fn find_scoped_column(
3466    scope: &[ScopedTable],
3467    column: &str,
3468    span: crate::ast::Span,
3469) -> Result<FoundScopedColumn, PlannerError> {
3470    let mut matches = Vec::new();
3471    for table in scope {
3472        if table.hidden_unqualified_columns.contains(column) {
3473            continue;
3474        }
3475        if let Some(local_idx) = table.table.get_column_index(column) {
3476            let meta = &table.table.columns[local_idx];
3477            matches.push(FoundScopedColumn {
3478                table: table.table.name.clone(),
3479                index: table.start_index + local_idx,
3480                ty: meta.data_type.clone(),
3481                partner_indices: table
3482                    .merged_column_partners
3483                    .get(column)
3484                    .cloned()
3485                    .unwrap_or_default(),
3486            });
3487        }
3488    }
3489    match matches.len() {
3490        0 => Err(PlannerError::column_not_found(column, "JOIN input", span)),
3491        1 => Ok(matches.remove(0)),
3492        _ => Err(PlannerError::ambiguous_column(
3493            column,
3494            scope.iter().map(|s| s.table.name.clone()).collect(),
3495            span,
3496        )),
3497    }
3498}
3499
3500fn merged_scoped_column_expr(
3501    found: &FoundScopedColumn,
3502    column: &str,
3503    span: crate::ast::Span,
3504) -> TypedExpr {
3505    let own = TypedExpr::column_ref(
3506        found.table.clone(),
3507        column.to_string(),
3508        found.index,
3509        found.ty.clone(),
3510        span,
3511    );
3512    if found.partner_indices.is_empty() {
3513        return own;
3514    }
3515
3516    let mut args = Vec::with_capacity(found.partner_indices.len() + 1);
3517    args.push(own);
3518    args.extend(found.partner_indices.iter().map(|&index| {
3519        TypedExpr::column_ref(
3520            found.table.clone(),
3521            column.to_string(),
3522            index,
3523            found.ty.clone(),
3524            span,
3525        )
3526    }));
3527    TypedExpr {
3528        kind: TypedExprKind::FunctionCall {
3529            name: "coalesce".to_string(),
3530            args,
3531            distinct: false,
3532            star: false,
3533            over: None,
3534        },
3535        resolved_type: found.ty.clone(),
3536        span,
3537    }
3538}
3539
3540fn projection_schema(
3541    projection: &Projection,
3542    input_schema: &[ColumnMetadata],
3543) -> Vec<ColumnMetadata> {
3544    match projection {
3545        Projection::All(names) => names
3546            .iter()
3547            .enumerate()
3548            .map(|(idx, name)| {
3549                let ty = (names.len() == input_schema.len())
3550                    .then(|| input_schema.get(idx))
3551                    .flatten()
3552                    .or_else(|| input_schema.iter().find(|col| &col.name == name))
3553                    .map(|col| col.data_type.clone())
3554                    .unwrap_or(ResolvedType::Null);
3555                ColumnMetadata::new(name.clone(), ty)
3556            })
3557            .collect(),
3558        Projection::Columns(columns) => columns
3559            .iter()
3560            .enumerate()
3561            .map(|(idx, col)| {
3562                let name = col
3563                    .alias
3564                    .clone()
3565                    .or_else(|| match &col.expr.kind {
3566                        TypedExprKind::ColumnRef { column, .. } => Some(column.clone()),
3567                        // A USING/NATURAL common column is planned as
3568                        // COALESCE(left, right); it still names the merged
3569                        // column, not an anonymous expression.
3570                        TypedExprKind::FunctionCall { name, args, .. }
3571                            if name == "coalesce" && !args.is_empty() =>
3572                        {
3573                            let first_column = match &args[0].kind {
3574                                TypedExprKind::ColumnRef { column, .. } => Some(column),
3575                                _ => None,
3576                            };
3577                            first_column
3578                                .filter(|column| {
3579                                    args.iter().all(|arg| {
3580                                        matches!(
3581                                            &arg.kind,
3582                                            TypedExprKind::ColumnRef { column: other, .. }
3583                                                if other == *column
3584                                        )
3585                                    })
3586                                })
3587                                .cloned()
3588                        }
3589                        _ => None,
3590                    })
3591                    .unwrap_or_else(|| format!("col_{idx}"));
3592                ColumnMetadata::new(name, col.expr.resolved_type.clone())
3593            })
3594            .collect(),
3595    }
3596}
3597
3598fn visible_wildcard_columns(schema: &[ColumnMetadata], scope: &[ScopedTable]) -> Vec<String> {
3599    schema
3600        .iter()
3601        .enumerate()
3602        .filter(|(index, column)| {
3603            !scope.iter().any(|table| {
3604                *index >= table.start_index
3605                    && *index < table.start_index + table.table.columns.len()
3606                    && table.hidden_unqualified_columns.contains(&column.name)
3607            })
3608        })
3609        .map(|(_, column)| column.name.clone())
3610        .collect()
3611}
3612
3613fn offset_scope(scope: &[ScopedTable], offset: usize) -> Vec<ScopedTable> {
3614    scope
3615        .iter()
3616        .cloned()
3617        .map(|mut table| {
3618            table.start_index += offset;
3619            table.scope_level += 1;
3620            table
3621        })
3622        .collect()
3623}
3624
3625fn natural_join_columns(
3626    left_schema: &[ColumnMetadata],
3627    right_schema: &[ColumnMetadata],
3628) -> Vec<String> {
3629    // Pairing every left column against every right column is quadratic in the
3630    // join width, so the right side is hashed once. Iteration stays over the
3631    // left schema because the common columns keep the left table's order.
3632    let right_names = right_schema
3633        .iter()
3634        .map(|column| column.name.as_str())
3635        .collect::<HashSet<_>>();
3636    left_schema
3637        .iter()
3638        .filter(|left| right_names.contains(left.name.as_str()))
3639        .map(|column| column.name.clone())
3640        .collect()
3641}
3642
3643fn install_base_projection(plan: &mut LogicalPlan, projection: &Projection) {
3644    match plan {
3645        LogicalPlan::Scan {
3646            projection: scan_projection,
3647            ..
3648        } => *scan_projection = projection.clone(),
3649        LogicalPlan::Filter { input, .. } => install_base_projection(input, projection),
3650        _ => {}
3651    }
3652}
3653
3654fn is_aggregate_function(name: &str) -> bool {
3655    matches!(
3656        name.to_ascii_lowercase().as_str(),
3657        "count" | "sum" | "total" | "avg" | "min" | "max" | "group_concat" | "string_agg"
3658    )
3659}
3660
3661fn expr_key(expr: &TypedExpr) -> String {
3662    format!("{:?}", expr.kind)
3663}
3664
3665fn aggregate_signature(
3666    name: &str,
3667    distinct: bool,
3668    star: bool,
3669    arg: Option<&TypedExpr>,
3670    separator: Option<&String>,
3671    _expr: &TypedExpr,
3672) -> AggregateSignature {
3673    AggregateSignature {
3674        name: name.to_ascii_lowercase(),
3675        distinct,
3676        star,
3677        arg_key: arg.map(expr_key),
3678        separator: separator.cloned(),
3679    }
3680}
3681
3682fn build_group_key_map(group_keys: &[TypedExpr]) -> HashMap<String, usize> {
3683    let mut map = HashMap::new();
3684    for (idx, key) in group_keys.iter().enumerate() {
3685        map.insert(expr_key(key), idx);
3686    }
3687    map
3688}
3689
3690fn build_aggregate_map(aggregates: &[AggregateExpr]) -> HashMap<AggregateSignature, usize> {
3691    let mut map = HashMap::new();
3692    for (idx, agg) in aggregates.iter().enumerate() {
3693        let (name, separator, star, arg) = match &agg.function {
3694            AggregateFunction::Count => (
3695                "count".to_string(),
3696                None,
3697                agg.arg.is_none(),
3698                agg.arg.as_ref(),
3699            ),
3700            AggregateFunction::Sum => ("sum".to_string(), None, false, agg.arg.as_ref()),
3701            AggregateFunction::Total => ("total".to_string(), None, false, agg.arg.as_ref()),
3702            AggregateFunction::Avg => ("avg".to_string(), None, false, agg.arg.as_ref()),
3703            AggregateFunction::Min => ("min".to_string(), None, false, agg.arg.as_ref()),
3704            AggregateFunction::Max => ("max".to_string(), None, false, agg.arg.as_ref()),
3705            AggregateFunction::GroupConcat { separator } => (
3706                "group_concat".to_string(),
3707                separator.clone(),
3708                false,
3709                agg.arg.as_ref(),
3710            ),
3711            AggregateFunction::StringAgg { separator } => (
3712                "string_agg".to_string(),
3713                separator.clone(),
3714                false,
3715                agg.arg.as_ref(),
3716            ),
3717        };
3718        let signature = AggregateSignature {
3719            name,
3720            distinct: agg.distinct,
3721            star,
3722            arg_key: arg.map(expr_key),
3723            separator,
3724        };
3725        map.insert(signature, idx);
3726    }
3727    map
3728}
3729
3730fn build_aggregate_schema(
3731    group_keys: &[TypedExpr],
3732    aggregates: &[AggregateExpr],
3733) -> Vec<ColumnMetadata> {
3734    let mut schema = Vec::new();
3735    for (idx, key) in group_keys.iter().enumerate() {
3736        let name = match &key.kind {
3737            TypedExprKind::ColumnRef { column, .. } => column.clone(),
3738            _ => format!("group_{idx}"),
3739        };
3740        schema.push(ColumnMetadata::new(name, key.resolved_type.clone()));
3741    }
3742    for (idx, agg) in aggregates.iter().enumerate() {
3743        let name = match &agg.function {
3744            AggregateFunction::Count => format!("count_{idx}"),
3745            AggregateFunction::Sum => format!("sum_{idx}"),
3746            AggregateFunction::Total => format!("total_{idx}"),
3747            AggregateFunction::Avg => format!("avg_{idx}"),
3748            AggregateFunction::Min => format!("min_{idx}"),
3749            AggregateFunction::Max => format!("max_{idx}"),
3750            AggregateFunction::GroupConcat { .. } => format!("group_concat_{idx}"),
3751            AggregateFunction::StringAgg { .. } => format!("string_agg_{idx}"),
3752        };
3753        schema.push(ColumnMetadata::new(name, agg.result_type.clone()));
3754    }
3755    schema
3756}
3757
3758fn rewrite_expr_with_maps(
3759    expr: &TypedExpr,
3760    group_key_map: &HashMap<String, usize>,
3761    aggregate_map: &HashMap<AggregateSignature, usize>,
3762    output_names: &[String],
3763) -> Result<TypedExpr, PlannerError> {
3764    let group_key_count = output_names.len().saturating_sub(aggregate_map.len());
3765    let key = expr_key(expr);
3766    if let Some(idx) = group_key_map.get(&key) {
3767        return Ok(make_output_column_ref(
3768            *idx,
3769            output_names,
3770            expr.resolved_type.clone(),
3771            expr.span,
3772        ));
3773    }
3774
3775    match &expr.kind {
3776        TypedExprKind::FunctionCall {
3777            name,
3778            args,
3779            distinct,
3780            star,
3781            over: None,
3782        } if is_aggregate_function(name) => {
3783            let separator = if name.eq_ignore_ascii_case("group_concat") && args.len() == 2 {
3784                if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
3785                    Some(value.clone())
3786                } else {
3787                    return Err(PlannerError::invalid_expression(
3788                        "GROUP_CONCAT separator must be a string literal".to_string(),
3789                    ));
3790                }
3791            } else if name.eq_ignore_ascii_case("string_agg") && args.len() == 2 {
3792                if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
3793                    Some(value.clone())
3794                } else {
3795                    return Err(PlannerError::invalid_expression(
3796                        "STRING_AGG separator must be a string literal".to_string(),
3797                    ));
3798                }
3799            } else {
3800                None
3801            };
3802            let signature = AggregateSignature {
3803                name: name.to_ascii_lowercase(),
3804                distinct: *distinct,
3805                star: *star,
3806                arg_key: args.first().map(expr_key),
3807                separator,
3808            };
3809            let idx = aggregate_map.get(&signature).ok_or_else(|| {
3810                PlannerError::invalid_expression(
3811                    "aggregate in expression is not part of plan".to_string(),
3812                )
3813            })?;
3814            let output_index = group_key_count + idx;
3815            Ok(make_output_column_ref(
3816                output_index,
3817                output_names,
3818                expr.resolved_type.clone(),
3819                expr.span,
3820            ))
3821        }
3822        TypedExprKind::FunctionCall {
3823            name,
3824            args,
3825            distinct,
3826            star,
3827            over,
3828        } => {
3829            if over.is_some() {
3830                return Err(PlannerError::invalid_expression(
3831                    "window function reached aggregate expression rewrite".to_string(),
3832                ));
3833            }
3834            if *distinct || *star {
3835                return Err(PlannerError::invalid_expression(
3836                    "DISTINCT/STAR modifiers are only supported for aggregates".to_string(),
3837                ));
3838            }
3839            let mut rewritten_args = Vec::with_capacity(args.len());
3840            for arg in args {
3841                rewritten_args.push(rewrite_expr_with_maps(
3842                    arg,
3843                    group_key_map,
3844                    aggregate_map,
3845                    output_names,
3846                )?);
3847            }
3848            Ok(TypedExpr {
3849                kind: TypedExprKind::FunctionCall {
3850                    name: name.clone(),
3851                    args: rewritten_args,
3852                    distinct: false,
3853                    star: false,
3854                    over: None,
3855                },
3856                resolved_type: expr.resolved_type.clone(),
3857                span: expr.span,
3858            })
3859        }
3860        TypedExprKind::BinaryOp { left, op, right } => {
3861            let left = rewrite_expr_with_maps(left, group_key_map, aggregate_map, output_names)?;
3862            let right = rewrite_expr_with_maps(right, group_key_map, aggregate_map, output_names)?;
3863            Ok(TypedExpr {
3864                kind: TypedExprKind::BinaryOp {
3865                    left: Box::new(left),
3866                    op: *op,
3867                    right: Box::new(right),
3868                },
3869                resolved_type: expr.resolved_type.clone(),
3870                span: expr.span,
3871            })
3872        }
3873        TypedExprKind::UnaryOp { op, operand } => {
3874            let operand =
3875                rewrite_expr_with_maps(operand, group_key_map, aggregate_map, output_names)?;
3876            Ok(TypedExpr {
3877                kind: TypedExprKind::UnaryOp {
3878                    op: *op,
3879                    operand: Box::new(operand),
3880                },
3881                resolved_type: expr.resolved_type.clone(),
3882                span: expr.span,
3883            })
3884        }
3885        TypedExprKind::Case {
3886            operand,
3887            branches,
3888            else_expr,
3889        } => {
3890            let operand = operand
3891                .as_deref()
3892                .map(|operand| {
3893                    rewrite_expr_with_maps(operand, group_key_map, aggregate_map, output_names)
3894                        .map(Box::new)
3895                })
3896                .transpose()?;
3897            let mut rewritten_branches = Vec::with_capacity(branches.len());
3898            for branch in branches {
3899                rewritten_branches.push(TypedCaseWhen {
3900                    when: rewrite_expr_with_maps(
3901                        &branch.when,
3902                        group_key_map,
3903                        aggregate_map,
3904                        output_names,
3905                    )?,
3906                    then: rewrite_expr_with_maps(
3907                        &branch.then,
3908                        group_key_map,
3909                        aggregate_map,
3910                        output_names,
3911                    )?,
3912                });
3913            }
3914            let else_expr = else_expr
3915                .as_deref()
3916                .map(|else_expr| {
3917                    rewrite_expr_with_maps(else_expr, group_key_map, aggregate_map, output_names)
3918                        .map(Box::new)
3919                })
3920                .transpose()?;
3921            Ok(TypedExpr {
3922                kind: TypedExprKind::Case {
3923                    operand,
3924                    branches: rewritten_branches,
3925                    else_expr,
3926                },
3927                resolved_type: expr.resolved_type.clone(),
3928                span: expr.span,
3929            })
3930        }
3931        TypedExprKind::Between {
3932            expr: inner,
3933            low,
3934            high,
3935            negated,
3936        } => {
3937            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
3938            let low = rewrite_expr_with_maps(low, group_key_map, aggregate_map, output_names)?;
3939            let high = rewrite_expr_with_maps(high, group_key_map, aggregate_map, output_names)?;
3940            Ok(TypedExpr {
3941                kind: TypedExprKind::Between {
3942                    expr: Box::new(inner),
3943                    low: Box::new(low),
3944                    high: Box::new(high),
3945                    negated: *negated,
3946                },
3947                resolved_type: expr.resolved_type.clone(),
3948                span: expr.span,
3949            })
3950        }
3951        TypedExprKind::Like {
3952            expr: inner,
3953            pattern,
3954            escape,
3955            negated,
3956            kind,
3957        } => {
3958            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
3959            let pattern =
3960                rewrite_expr_with_maps(pattern, group_key_map, aggregate_map, output_names)?;
3961            let escape = if let Some(esc) = escape {
3962                Some(Box::new(rewrite_expr_with_maps(
3963                    esc,
3964                    group_key_map,
3965                    aggregate_map,
3966                    output_names,
3967                )?))
3968            } else {
3969                None
3970            };
3971            Ok(TypedExpr {
3972                kind: TypedExprKind::Like {
3973                    expr: Box::new(inner),
3974                    pattern: Box::new(pattern),
3975                    escape,
3976                    negated: *negated,
3977                    kind: *kind,
3978                },
3979                resolved_type: expr.resolved_type.clone(),
3980                span: expr.span,
3981            })
3982        }
3983        TypedExprKind::InList {
3984            expr: inner,
3985            list,
3986            negated,
3987        } => {
3988            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
3989            let mut rewritten_list = Vec::with_capacity(list.len());
3990            for item in list {
3991                rewritten_list.push(rewrite_expr_with_maps(
3992                    item,
3993                    group_key_map,
3994                    aggregate_map,
3995                    output_names,
3996                )?);
3997            }
3998            Ok(TypedExpr {
3999                kind: TypedExprKind::InList {
4000                    expr: Box::new(inner),
4001                    list: rewritten_list,
4002                    negated: *negated,
4003                },
4004                resolved_type: expr.resolved_type.clone(),
4005                span: expr.span,
4006            })
4007        }
4008        TypedExprKind::IsNull {
4009            expr: inner,
4010            negated,
4011        } => {
4012            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
4013            Ok(TypedExpr {
4014                kind: TypedExprKind::IsNull {
4015                    expr: Box::new(inner),
4016                    negated: *negated,
4017                },
4018                resolved_type: expr.resolved_type.clone(),
4019                span: expr.span,
4020            })
4021        }
4022        TypedExprKind::Literal(_) | TypedExprKind::VectorLiteral(_) => Ok(expr.clone()),
4023        TypedExprKind::ColumnRef { .. } => Err(PlannerError::invalid_expression(
4024            "column reference must appear in GROUP BY or be aggregated".to_string(),
4025        )),
4026        TypedExprKind::Cast {
4027            expr: inner,
4028            target_type,
4029        } => {
4030            let inner = rewrite_expr_with_maps(inner, group_key_map, aggregate_map, output_names)?;
4031            Ok(TypedExpr {
4032                kind: TypedExprKind::Cast {
4033                    expr: Box::new(inner),
4034                    target_type: target_type.clone(),
4035                },
4036                resolved_type: expr.resolved_type.clone(),
4037                span: expr.span,
4038            })
4039        }
4040        TypedExprKind::ScalarSubquery(_)
4041        | TypedExprKind::InSubquery { .. }
4042        | TypedExprKind::Exists { .. }
4043        | TypedExprKind::Quantified { .. } => Ok(expr.clone()),
4044    }
4045}
4046
4047fn make_output_column_ref(
4048    index: usize,
4049    output_names: &[String],
4050    resolved_type: ResolvedType,
4051    span: crate::ast::Span,
4052) -> TypedExpr {
4053    let name = output_names
4054        .get(index)
4055        .cloned()
4056        .unwrap_or_else(|| format!("col_{index}"));
4057    TypedExpr::column_ref("__agg__".to_string(), name, index, resolved_type, span)
4058}