Skip to main content

alopex_sql/planner/
type_checker.rs

1//! Type checking module for the Alopex SQL dialect.
2//!
3//! This module provides type inference and validation for SQL expressions.
4//! It checks that expressions are well-typed and that operations are valid
5//! for the types involved.
6
7use crate::ast::Span;
8use crate::ast::Statement;
9use crate::ast::ddl::VectorMetric;
10use crate::ast::expr::{
11    BinaryOp, Expr, ExprKind, Literal, PatternMatchKind, Quantifier as AstQuantifier, UnaryOp,
12    WindowFrame, WindowFrameBound, WindowFrameUnits, WindowSpec,
13};
14use crate::catalog::{Catalog, ColumnMetadata, TableMetadata};
15use crate::planner::aggregate_expr::{AggregateExpr, AggregateFunction};
16use crate::planner::error::PlannerError;
17use crate::planner::logical_plan::LogicalPlan;
18use crate::planner::typed_expr::{
19    Quantifier, SortExpr, TypedCaseWhen, TypedExpr, TypedExprKind, TypedWindowSpec,
20};
21use crate::planner::types::ResolvedType;
22use std::collections::{BTreeSet, HashMap, HashSet};
23use std::sync::Arc;
24
25/// A table visible to expression name resolution.
26///
27/// The metadata is shared rather than owned: every enclosing scope is copied
28/// into each nested scope, and copying whole schemas there made resolution cost
29/// grow with the square of the nesting depth.
30#[derive(Debug, Clone)]
31pub struct ScopedTable {
32    pub table: Arc<TableMetadata>,
33    pub start_index: usize,
34    /// Lexical nesting level; zero is the current SELECT and larger values
35    /// are successively enclosing SELECT scopes.
36    pub scope_level: usize,
37    /// Columns coalesced by a JOIN ... USING or NATURAL JOIN. They remain
38    /// addressable by a qualified right-hand reference, but are not candidates
39    /// for an unqualified reference because the merged output column owns
40    /// the name.
41    pub hidden_unqualified_columns: HashSet<String>,
42    /// For a column merged by USING or NATURAL, the output indexes of every
43    /// other side. An unqualified reference to a merged name resolves to
44    /// `COALESCE(left, right, ...)` so that RIGHT and FULL joins report the key
45    /// from whichever joined input is present.
46    pub merged_column_partners: HashMap<String, Vec<usize>>,
47    /// Column name to position in `table.columns`, built once when the table
48    /// enters scope. Resolution looks a name up once per reference, so scanning
49    /// the column list made a wide projection cost the square of its width.
50    /// Shared alongside the metadata it indexes so the two cannot drift apart.
51    ///
52    /// `None` for narrow tables, where building the map costs more than the
53    /// scans it saves; see [`COLUMN_INDEX_THRESHOLD`].
54    column_index: Option<Arc<HashMap<String, usize>>>,
55}
56
57/// Column count above which a scoped table gets a hash index.
58///
59/// Below this a linear scan of the column list wins: the map allocation is paid
60/// once per table per scope, and measurement showed narrow tables getting 10-15%
61/// slower when every table was indexed unconditionally.
62const COLUMN_INDEX_THRESHOLD: usize = 32;
63
64impl ScopedTable {
65    pub fn new(table: impl Into<Arc<TableMetadata>>, start_index: usize) -> Self {
66        let table = table.into();
67        let column_index = (table.columns.len() > COLUMN_INDEX_THRESHOLD).then(|| {
68            // On a duplicate name the first position wins, matching the linear
69            // scan this replaces.
70            let mut index = HashMap::with_capacity(table.columns.len());
71            for (position, column) in table.columns.iter().enumerate() {
72                index.entry(column.name.clone()).or_insert(position);
73            }
74            Arc::new(index)
75        });
76        Self {
77            table,
78            start_index,
79            scope_level: 0,
80            hidden_unqualified_columns: HashSet::new(),
81            merged_column_partners: HashMap::new(),
82            column_index,
83        }
84    }
85
86    /// Position of `column` in this table, or `None` if it has no such column.
87    pub fn column_position(&self, column: &str) -> Option<usize> {
88        match &self.column_index {
89            Some(index) => index.get(column).copied(),
90            None => self.table.get_column_index(column),
91        }
92    }
93
94    pub fn hide_unqualified_columns(&mut self, columns: &[String]) {
95        self.hidden_unqualified_columns
96            .extend(columns.iter().cloned());
97    }
98
99    /// Record that `column` is merged with the output column at `partner_index`.
100    pub fn merge_column_with(&mut self, column: &str, partner_index: usize) {
101        let partners = self
102            .merged_column_partners
103            .entry(column.to_string())
104            .or_default();
105        if !partners.contains(&partner_index) {
106            partners.push(partner_index);
107        }
108    }
109}
110
111pub type SubqueryPlanner<'p> = dyn Fn(&Statement, &[ScopedTable]) -> Result<(LogicalPlan, Vec<ColumnMetadata>), PlannerError>
112    + 'p;
113
114/// Type checker for SQL expressions.
115///
116/// Performs type inference and validation for expressions, ensuring that
117/// operations are valid for the types involved and that constraints are met.
118///
119/// # Examples
120///
121/// ```
122/// use alopex_sql::catalog::MemoryCatalog;
123/// use alopex_sql::planner::type_checker::TypeChecker;
124///
125/// let catalog = MemoryCatalog::new();
126/// let type_checker = TypeChecker::new(&catalog);
127/// ```
128pub struct TypeChecker<'a, C: Catalog + ?Sized> {
129    catalog: &'a C,
130}
131
132impl<'a, C: Catalog + ?Sized> TypeChecker<'a, C> {
133    /// Create a new TypeChecker with the given catalog.
134    pub fn new(catalog: &'a C) -> Self {
135        Self { catalog }
136    }
137
138    /// Get a reference to the catalog.
139    pub fn catalog(&self) -> &'a C {
140        self.catalog
141    }
142
143    /// Infer the type of an expression within a table context.
144    ///
145    /// Recursively analyzes the expression to determine its type, resolving
146    /// column references against the provided table metadata.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if:
151    /// - A column reference cannot be resolved
152    /// - A binary operation is invalid for the operand types
153    /// - A function call has invalid arguments
154    pub fn infer_type(
155        &self,
156        expr: &Expr,
157        table: &TableMetadata,
158    ) -> Result<TypedExpr, PlannerError> {
159        let scope = [ScopedTable::new(table.clone(), 0)];
160        self.infer_type_with_scope(expr, &scope, &|stmt, _outer| {
161            let planner = crate::planner::Planner::new(self.catalog);
162            let plan = planner.plan(stmt)?;
163            Ok((plan, Vec::new()))
164        })
165    }
166
167    pub fn infer_type_with_scope(
168        &self,
169        expr: &Expr,
170        scope: &[ScopedTable],
171        plan_subquery: &SubqueryPlanner<'_>,
172    ) -> Result<TypedExpr, PlannerError> {
173        let span = expr.span;
174        match &expr.kind {
175            ExprKind::Literal { literal: lit } => self.infer_literal_type(lit, span),
176
177            ExprKind::ColumnRef {
178                table: table_qualifier,
179                column,
180            } => self.infer_column_ref_type_with_scope(
181                scope,
182                table_qualifier.as_deref(),
183                column,
184                span,
185            ),
186
187            ExprKind::BinaryOp { left, op, right } => {
188                self.infer_binary_op_type_with_scope(left, *op, right, scope, plan_subquery, span)
189            }
190
191            ExprKind::UnaryOp { op, operand } => {
192                self.infer_unary_op_type_with_scope(*op, operand, scope, plan_subquery, span)
193            }
194
195            ExprKind::Case {
196                operand,
197                branches,
198                else_expr,
199            } => self.infer_case_type_with_scope(
200                operand.as_deref(),
201                branches,
202                else_expr.as_deref(),
203                scope,
204                plan_subquery,
205                span,
206            ),
207
208            ExprKind::FunctionCall {
209                name,
210                args,
211                distinct,
212                star,
213                over,
214            } => self.infer_function_call_type_with_scope(
215                name,
216                args,
217                *distinct,
218                *star,
219                over.as_ref(),
220                scope,
221                plan_subquery,
222                span,
223            ),
224
225            ExprKind::Cast { expr, target_type } => {
226                let typed_expr = self.infer_type_with_scope(expr, scope, plan_subquery)?;
227                Ok(TypedExpr::cast(
228                    typed_expr,
229                    ResolvedType::from_ast(target_type),
230                    span,
231                ))
232            }
233
234            ExprKind::Between {
235                expr,
236                low,
237                high,
238                negated,
239            } => self.infer_between_type_with_scope(
240                expr,
241                low,
242                high,
243                *negated,
244                scope,
245                plan_subquery,
246                span,
247            ),
248
249            ExprKind::Like {
250                expr,
251                pattern,
252                escape,
253                negated,
254                kind,
255            } => self.infer_like_type_with_scope(
256                expr,
257                pattern,
258                escape.as_deref(),
259                *negated,
260                *kind,
261                scope,
262                plan_subquery,
263                span,
264            ),
265
266            ExprKind::InList {
267                expr,
268                list,
269                negated,
270            } => {
271                self.infer_in_list_type_with_scope(expr, list, *negated, scope, plan_subquery, span)
272            }
273
274            ExprKind::IsNull { expr, negated } => {
275                self.infer_is_null_type_with_scope(expr, *negated, scope, plan_subquery, span)
276            }
277
278            ExprKind::VectorLiteral { values } => self.infer_vector_literal_type(values, span),
279
280            ExprKind::ScalarSubquery { subquery } => {
281                let (plan, schema) = plan_subquery(subquery, scope)?;
282                let value_type = single_column_type(&schema, span)?;
283                Ok(TypedExpr {
284                    kind: TypedExprKind::ScalarSubquery(Box::new(plan)),
285                    resolved_type: value_type,
286                    span,
287                })
288            }
289            ExprKind::InSubquery {
290                expr,
291                subquery,
292                negated,
293            } => {
294                let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
295                let (plan, schema) = plan_subquery(subquery, scope)?;
296                let value_type = single_column_type(&schema, span)?;
297                self.check_comparison_op(&expr_typed.resolved_type, &value_type, span)?;
298                Ok(TypedExpr {
299                    kind: TypedExprKind::InSubquery {
300                        expr: Box::new(expr_typed),
301                        subquery: Box::new(plan),
302                        negated: *negated,
303                    },
304                    resolved_type: ResolvedType::Boolean,
305                    span,
306                })
307            }
308            ExprKind::Exists { subquery, negated } => {
309                let (plan, _schema) = plan_subquery(subquery, scope)?;
310                Ok(TypedExpr {
311                    kind: TypedExprKind::Exists {
312                        subquery: Box::new(plan),
313                        negated: *negated,
314                    },
315                    resolved_type: ResolvedType::Boolean,
316                    span,
317                })
318            }
319            ExprKind::Quantified {
320                expr,
321                op,
322                quantifier,
323                subquery,
324            } => {
325                let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
326                let (plan, schema) = plan_subquery(subquery, scope)?;
327                let value_type = single_column_type(&schema, span)?;
328                self.check_binary_op(*op, &expr_typed.resolved_type, &value_type, span)?;
329                Ok(TypedExpr {
330                    kind: TypedExprKind::Quantified {
331                        expr: Box::new(expr_typed),
332                        op: *op,
333                        quantifier: match quantifier {
334                            AstQuantifier::Any => Quantifier::Any,
335                            AstQuantifier::All => Quantifier::All,
336                        },
337                        subquery: Box::new(plan),
338                    },
339                    resolved_type: ResolvedType::Boolean,
340                    span,
341                })
342            }
343        }
344    }
345
346    /// Infer the type of a literal value.
347    fn infer_literal_type(&self, lit: &Literal, span: Span) -> Result<TypedExpr, PlannerError> {
348        let (kind, resolved_type) = match lit {
349            Literal::Number(s) => {
350                // Determine if it's integer or floating point
351                let resolved_type = if s.contains('.') || s.contains('e') || s.contains('E') {
352                    ResolvedType::Double
353                } else {
354                    // Check if it fits in i32 or needs i64
355                    if s.parse::<i32>().is_ok() {
356                        ResolvedType::Integer
357                    } else {
358                        ResolvedType::BigInt
359                    }
360                };
361                (TypedExprKind::Literal(lit.clone()), resolved_type)
362            }
363            Literal::String(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Text),
364            Literal::Interval(_) => {
365                return Err(PlannerError::unsupported_feature(
366                    "INTERVAL literals require a SQL-TS semantic layer",
367                    "0.9.0",
368                    span,
369                ));
370            }
371            Literal::Boolean(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Boolean),
372            Literal::Null => (TypedExprKind::Literal(lit.clone()), ResolvedType::Null),
373        };
374
375        Ok(TypedExpr {
376            kind,
377            resolved_type,
378            span,
379        })
380    }
381
382    /// Infer the type of a column reference.
383    #[allow(dead_code)]
384    fn infer_column_ref_type(
385        &self,
386        table: &TableMetadata,
387        column_name: &str,
388        span: Span,
389    ) -> Result<TypedExpr, PlannerError> {
390        // Find the column in the table
391        let (column_index, column) = table
392            .columns
393            .iter()
394            .enumerate()
395            .find(|(_, c)| c.name == column_name)
396            .ok_or_else(|| PlannerError::ColumnNotFound {
397                column: column_name.to_string(),
398                table: table.name.clone(),
399                line: span.start.line,
400                col: span.start.column,
401            })?;
402
403        Ok(TypedExpr {
404            kind: TypedExprKind::ColumnRef {
405                table: table.name.clone(),
406                column: column_name.to_string(),
407                column_index,
408            },
409            resolved_type: column.data_type.clone(),
410            span,
411        })
412    }
413
414    fn infer_column_ref_type_with_scope(
415        &self,
416        scope: &[ScopedTable],
417        table_qualifier: Option<&str>,
418        column_name: &str,
419        span: Span,
420    ) -> Result<TypedExpr, PlannerError> {
421        let levels = scope
422            .iter()
423            .map(|table| table.scope_level)
424            .collect::<BTreeSet<_>>();
425        let mut qualifier_found = false;
426
427        for level in levels {
428            let candidates = scope
429                .iter()
430                .filter(|table| table.scope_level == level)
431                .filter(|table| {
432                    table_qualifier.is_some()
433                        || !table.hidden_unqualified_columns.contains(column_name)
434                })
435                .collect::<Vec<_>>();
436            if candidates.is_empty() {
437                continue;
438            }
439            if let Some(qualifier) = table_qualifier {
440                let qualified = candidates
441                    .iter()
442                    .filter(|table| table.table.name == qualifier)
443                    .collect::<Vec<_>>();
444                match qualified.len() {
445                    0 => continue,
446                    1 => qualifier_found = true,
447                    _ => {
448                        return Err(PlannerError::ambiguous_column(
449                            column_name,
450                            qualified
451                                .iter()
452                                .map(|table| table.table.name.clone())
453                                .collect(),
454                            span,
455                        ));
456                    }
457                }
458            }
459
460            // Resolution happens through each table's own column index rather
461            // than by scanning its column list, because this runs once per
462            // column reference and the scan made a wide projection quadratic.
463            let mut matches = candidates.iter().filter(|table| {
464                table_qualifier.is_none_or(|qualifier| table.table.name == qualifier)
465                    && table.column_position(column_name).is_some()
466            });
467            let found = matches.next();
468            let second = matches.next();
469
470            match (found, second) {
471                (Some(_), Some(_)) => {
472                    return Err(PlannerError::ambiguous_column(
473                        column_name,
474                        candidates
475                            .iter()
476                            .filter(|table| table.column_position(column_name).is_some())
477                            .map(|table| table.table.name.clone())
478                            .collect(),
479                        span,
480                    ));
481                }
482                (None, _) => {
483                    if table_qualifier.is_some() {
484                        // A qualified name that the named table does not have is
485                        // an error here; it cannot be a correlated reference.
486                        return Err(PlannerError::column_not_found(
487                            column_name,
488                            candidates
489                                .first()
490                                .map(|table| table.table.name.as_str())
491                                .unwrap_or("unknown"),
492                            span,
493                        ));
494                    }
495                    // A missing local name may be a correlated reference. Only
496                    // this case falls back to the next enclosing scope.
497                    continue;
498                }
499                (Some(scoped), None) => {
500                    let column_index = scoped
501                        .column_position(column_name)
502                        .expect("filtered on the column being present");
503                    let column = &scoped.table.columns[column_index];
504                    let own_ref = TypedExpr {
505                        kind: TypedExprKind::ColumnRef {
506                            table: scoped.table.name.clone(),
507                            column: column_name.to_string(),
508                            column_index: scoped.start_index + column_index,
509                        },
510                        resolved_type: column.data_type.clone(),
511                        span,
512                    };
513
514                    // A USING/NATURAL common column is one output column formed
515                    // from both inputs. An unqualified reference must see the
516                    // merged value, otherwise a RIGHT or FULL join reports the
517                    // left side's NULL for rows that only exist on the right.
518                    if table_qualifier.is_none()
519                        && let Some(partner_indices) =
520                            scoped.merged_column_partners.get(column_name)
521                    {
522                        let mut args = Vec::with_capacity(partner_indices.len() + 1);
523                        args.push(own_ref);
524                        args.extend(partner_indices.iter().map(|&partner_index| TypedExpr {
525                            kind: TypedExprKind::ColumnRef {
526                                table: scoped.table.name.clone(),
527                                column: column_name.to_string(),
528                                column_index: partner_index,
529                            },
530                            resolved_type: column.data_type.clone(),
531                            span,
532                        }));
533                        return Ok(TypedExpr {
534                            kind: TypedExprKind::FunctionCall {
535                                name: "coalesce".to_string(),
536                                args,
537                                distinct: false,
538                                star: false,
539                                over: None,
540                            },
541                            resolved_type: column.data_type.clone(),
542                            span,
543                        });
544                    }
545
546                    return Ok(own_ref);
547                }
548            }
549        }
550
551        let table = scope
552            .iter()
553            .min_by_key(|table| table.scope_level)
554            .map(|table| table.table.name.clone())
555            .unwrap_or_else(|| "unknown".to_string());
556        if let Some(qualifier) = table_qualifier
557            && !qualifier_found
558        {
559            return Err(PlannerError::table_not_found(qualifier, span));
560        }
561        Err(PlannerError::column_not_found(column_name, table, span))
562    }
563
564    /// Infer the type of a binary operation.
565    #[allow(dead_code)]
566    fn infer_binary_op_type(
567        &self,
568        left: &Expr,
569        op: BinaryOp,
570        right: &Expr,
571        table: &TableMetadata,
572        span: Span,
573    ) -> Result<TypedExpr, PlannerError> {
574        let left_typed = self.infer_type(left, table)?;
575        let right_typed = self.infer_type(right, table)?;
576
577        let result_type = self.check_binary_op(
578            op,
579            &left_typed.resolved_type,
580            &right_typed.resolved_type,
581            span,
582        )?;
583
584        Ok(TypedExpr {
585            kind: TypedExprKind::BinaryOp {
586                left: Box::new(left_typed),
587                op,
588                right: Box::new(right_typed),
589            },
590            resolved_type: result_type,
591            span,
592        })
593    }
594
595    fn infer_binary_op_type_with_scope(
596        &self,
597        left: &Expr,
598        op: BinaryOp,
599        right: &Expr,
600        scope: &[ScopedTable],
601        plan_subquery: &SubqueryPlanner<'_>,
602        span: Span,
603    ) -> Result<TypedExpr, PlannerError> {
604        let left_typed = self.infer_type_with_scope(left, scope, plan_subquery)?;
605        let right_typed = self.infer_type_with_scope(right, scope, plan_subquery)?;
606
607        let result_type = self.check_binary_op(
608            op,
609            &left_typed.resolved_type,
610            &right_typed.resolved_type,
611            span,
612        )?;
613
614        Ok(TypedExpr {
615            kind: TypedExprKind::BinaryOp {
616                left: Box::new(left_typed),
617                op,
618                right: Box::new(right_typed),
619            },
620            resolved_type: result_type,
621            span,
622        })
623    }
624
625    fn infer_case_type_with_scope(
626        &self,
627        operand: Option<&Expr>,
628        branches: &[crate::ast::expr::CaseWhen],
629        else_expr: Option<&Expr>,
630        scope: &[ScopedTable],
631        plan_subquery: &SubqueryPlanner<'_>,
632        span: Span,
633    ) -> Result<TypedExpr, PlannerError> {
634        if branches.is_empty() {
635            return Err(PlannerError::invalid_expression(
636                "CASE expression requires at least one WHEN branch",
637            ));
638        }
639        let typed_operand = operand
640            .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
641            .transpose()?;
642        let mut typed_branches = Vec::with_capacity(branches.len());
643        let mut result_type = ResolvedType::Null;
644
645        for branch in branches {
646            let condition = self.infer_type_with_scope(&branch.when, scope, plan_subquery)?;
647            if let Some(operand) = &typed_operand {
648                self.check_comparison_op(
649                    &operand.resolved_type,
650                    &condition.resolved_type,
651                    condition.span,
652                )?;
653            } else if !matches!(
654                condition.resolved_type,
655                ResolvedType::Boolean | ResolvedType::Null
656            ) {
657                return Err(PlannerError::type_mismatch(
658                    "Boolean",
659                    condition.resolved_type.type_name(),
660                    condition.span,
661                ));
662            }
663
664            let result = self.infer_type_with_scope(&branch.then, scope, plan_subquery)?;
665            result_type =
666                self.common_case_result_type(&result_type, &result.resolved_type, result.span)?;
667            typed_branches.push(TypedCaseWhen {
668                when: condition,
669                then: result,
670            });
671        }
672
673        let mut typed_else = else_expr
674            .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
675            .transpose()?;
676        if let Some(else_expr) = &typed_else {
677            result_type = self.common_case_result_type(
678                &result_type,
679                &else_expr.resolved_type,
680                else_expr.span,
681            )?;
682        }
683
684        for branch in &mut typed_branches {
685            coerce_case_result(&mut branch.then, &result_type);
686        }
687        if let Some(else_expr) = &mut typed_else {
688            coerce_case_result(else_expr, &result_type);
689        }
690
691        Ok(TypedExpr {
692            kind: TypedExprKind::Case {
693                operand: typed_operand.map(Box::new),
694                branches: typed_branches,
695                else_expr: typed_else.map(Box::new),
696            },
697            resolved_type: result_type,
698            span,
699        })
700    }
701
702    fn common_case_result_type(
703        &self,
704        current: &ResolvedType,
705        next: &ResolvedType,
706        span: Span,
707    ) -> Result<ResolvedType, PlannerError> {
708        if matches!(current, ResolvedType::Null) {
709            return Ok(next.clone());
710        }
711        if matches!(next, ResolvedType::Null) || current == next {
712            return Ok(current.clone());
713        }
714        if is_numeric_type(current) && is_numeric_type(next) {
715            return self.check_arithmetic_op(current, next, span);
716        }
717        Err(PlannerError::type_mismatch(
718            current.type_name(),
719            next.type_name(),
720            span,
721        ))
722    }
723
724    /// Check binary operation and return the result type.
725    ///
726    /// Validates that the operator is valid for the given operand types
727    /// and returns the result type.
728    ///
729    /// # Type Rules
730    ///
731    /// - Arithmetic operators (+, -, *, /, %): Require numeric operands
732    /// - Comparison operators (=, <>, <, >, <=, >=): Require compatible types
733    /// - Logical operators (AND, OR): Require boolean operands
734    /// - String concatenation (||): Requires text operands
735    pub fn check_binary_op(
736        &self,
737        op: BinaryOp,
738        left: &ResolvedType,
739        right: &ResolvedType,
740        span: Span,
741    ) -> Result<ResolvedType, PlannerError> {
742        use BinaryOp::*;
743        use ResolvedType::*;
744
745        match op {
746            // Arithmetic operators: require numeric types
747            Add | Sub | Mul | Div => {
748                let result = self.check_arithmetic_op(left, right, span)?;
749                Ok(result)
750            }
751
752            // Remainder is defined only for integral operands.
753            Mod => self.check_modulo_op(left, right, span),
754
755            // Comparison operators: require compatible types, return boolean
756            Eq | Neq | Lt | Gt | LtEq | GtEq => {
757                self.check_comparison_op(left, right, span)?;
758                Ok(Boolean)
759            }
760
761            // Logical operators: require boolean types
762            And | Or => {
763                self.check_logical_op(left, right, span)?;
764                Ok(Boolean)
765            }
766
767            // String concatenation: requires text types
768            StringConcat => {
769                self.check_string_concat_op(left, right, span)?;
770                Ok(Text)
771            }
772        }
773    }
774
775    /// Check arithmetic operation and return the result type.
776    fn check_arithmetic_op(
777        &self,
778        left: &ResolvedType,
779        right: &ResolvedType,
780        span: Span,
781    ) -> Result<ResolvedType, PlannerError> {
782        use ResolvedType::*;
783
784        // Handle NULL propagation
785        if matches!(left, Null) || matches!(right, Null) {
786            return Ok(Null);
787        }
788
789        // Determine result type based on numeric type hierarchy
790        match (left, right) {
791            // Integer operations
792            (Integer, Integer) => Ok(Integer),
793            (Integer, BigInt) | (BigInt, Integer) | (BigInt, BigInt) => Ok(BigInt),
794            (Float, Float) => Ok(Float),
795            // f32 has 24 bits of mantissa and cannot hold the whole i32 range,
796            // so an INTEGER mixed with FLOAT widens to DOUBLE.
797            (Integer, Float)
798            | (Float, Integer)
799            | (Integer, Double)
800            | (Double, Integer)
801            | (BigInt, Float)
802            | (Float, BigInt)
803            | (BigInt, Double)
804            | (Double, BigInt)
805            | (Float, Double)
806            | (Double, Float)
807            | (Double, Double) => Ok(Double),
808
809            _ => Err(PlannerError::InvalidOperator {
810                op: "arithmetic".to_string(),
811                type_name: format!("{} and {}", left.type_name(), right.type_name()),
812                line: span.start.line,
813                column: span.start.column,
814            }),
815        }
816    }
817
818    /// Check remainder operands and return the integral result type.
819    fn check_modulo_op(
820        &self,
821        left: &ResolvedType,
822        right: &ResolvedType,
823        span: Span,
824    ) -> Result<ResolvedType, PlannerError> {
825        use ResolvedType::*;
826
827        if matches!(left, Null) || matches!(right, Null) {
828            return Ok(Null);
829        }
830
831        match (left, right) {
832            (Integer, Integer) => Ok(Integer),
833            (Integer, BigInt) | (BigInt, Integer) | (BigInt, BigInt) => Ok(BigInt),
834            _ => Err(PlannerError::InvalidOperator {
835                op: "modulo".to_string(),
836                type_name: format!("{} and {}", left.type_name(), right.type_name()),
837                line: span.start.line,
838                column: span.start.column,
839            }),
840        }
841    }
842
843    /// Check comparison operation for compatible types.
844    pub(crate) fn check_comparison_op(
845        &self,
846        left: &ResolvedType,
847        right: &ResolvedType,
848        span: Span,
849    ) -> Result<(), PlannerError> {
850        use ResolvedType::*;
851
852        // NULL can be compared with anything
853        if matches!(left, Null) || matches!(right, Null) {
854            return Ok(());
855        }
856
857        // Check type compatibility
858        let compatible = match (left, right) {
859            // Same types are always comparable
860            (a, b) if a == b => true,
861
862            // Numeric types are comparable with each other
863            (Integer | BigInt | Float | Double, Integer | BigInt | Float | Double) => true,
864
865            // Text types
866            (Text, Text) => true,
867
868            // Boolean types
869            (Boolean, Boolean) => true,
870
871            // Timestamp types
872            (Timestamp, Timestamp) => true,
873
874            // Vector types (for equality only, dimension must match)
875            (Vector { dimension: d1, .. }, Vector { dimension: d2, .. }) => d1 == d2,
876
877            _ => false,
878        };
879
880        if compatible {
881            Ok(())
882        } else {
883            Err(PlannerError::TypeMismatch {
884                expected: left.type_name().to_string(),
885                found: right.type_name().to_string(),
886                line: span.start.line,
887                column: span.start.column,
888            })
889        }
890    }
891
892    /// Check logical operation for boolean types.
893    fn check_logical_op(
894        &self,
895        left: &ResolvedType,
896        right: &ResolvedType,
897        span: Span,
898    ) -> Result<(), PlannerError> {
899        use ResolvedType::*;
900
901        // NULL is allowed (three-valued logic)
902        let left_ok = matches!(left, Boolean | Null);
903        let right_ok = matches!(right, Boolean | Null);
904
905        if !left_ok {
906            return Err(PlannerError::TypeMismatch {
907                expected: "Boolean".to_string(),
908                found: left.type_name().to_string(),
909                line: span.start.line,
910                column: span.start.column,
911            });
912        }
913
914        if !right_ok {
915            return Err(PlannerError::TypeMismatch {
916                expected: "Boolean".to_string(),
917                found: right.type_name().to_string(),
918                line: span.start.line,
919                column: span.start.column,
920            });
921        }
922
923        Ok(())
924    }
925
926    /// Check string concatenation operation.
927    fn check_string_concat_op(
928        &self,
929        left: &ResolvedType,
930        right: &ResolvedType,
931        span: Span,
932    ) -> Result<(), PlannerError> {
933        use ResolvedType::*;
934
935        // NULL is allowed
936        let left_ok = matches!(left, Text | Null);
937        let right_ok = matches!(right, Text | Null);
938
939        if !left_ok {
940            return Err(PlannerError::TypeMismatch {
941                expected: "Text".to_string(),
942                found: left.type_name().to_string(),
943                line: span.start.line,
944                column: span.start.column,
945            });
946        }
947
948        if !right_ok {
949            return Err(PlannerError::TypeMismatch {
950                expected: "Text".to_string(),
951                found: right.type_name().to_string(),
952                line: span.start.line,
953                column: span.start.column,
954            });
955        }
956
957        Ok(())
958    }
959
960    /// Infer the type of a unary operation.
961    #[allow(dead_code)]
962    fn infer_unary_op_type(
963        &self,
964        op: UnaryOp,
965        operand: &Expr,
966        table: &TableMetadata,
967        span: Span,
968    ) -> Result<TypedExpr, PlannerError> {
969        let operand_typed = self.infer_type(operand, table)?;
970
971        let result_type = match op {
972            UnaryOp::Not => {
973                // NOT requires boolean operand
974                if !matches!(
975                    operand_typed.resolved_type,
976                    ResolvedType::Boolean | ResolvedType::Null
977                ) {
978                    return Err(PlannerError::TypeMismatch {
979                        expected: "Boolean".to_string(),
980                        found: operand_typed.resolved_type.type_name().to_string(),
981                        line: span.start.line,
982                        column: span.start.column,
983                    });
984                }
985                ResolvedType::Boolean
986            }
987            UnaryOp::Minus => {
988                // Unary minus requires numeric operand
989                match &operand_typed.resolved_type {
990                    ResolvedType::Integer => ResolvedType::Integer,
991                    ResolvedType::BigInt => ResolvedType::BigInt,
992                    ResolvedType::Float => ResolvedType::Float,
993                    ResolvedType::Double => ResolvedType::Double,
994                    ResolvedType::Null => ResolvedType::Null,
995                    other => {
996                        return Err(PlannerError::InvalidOperator {
997                            op: "unary minus".to_string(),
998                            type_name: other.type_name().to_string(),
999                            line: span.start.line,
1000                            column: span.start.column,
1001                        });
1002                    }
1003                }
1004            }
1005        };
1006
1007        Ok(TypedExpr {
1008            kind: TypedExprKind::UnaryOp {
1009                op,
1010                operand: Box::new(operand_typed),
1011            },
1012            resolved_type: result_type,
1013            span,
1014        })
1015    }
1016
1017    fn infer_unary_op_type_with_scope(
1018        &self,
1019        op: UnaryOp,
1020        operand: &Expr,
1021        scope: &[ScopedTable],
1022        plan_subquery: &SubqueryPlanner<'_>,
1023        span: Span,
1024    ) -> Result<TypedExpr, PlannerError> {
1025        let operand_typed = self.infer_type_with_scope(operand, scope, plan_subquery)?;
1026
1027        let result_type = match op {
1028            UnaryOp::Not => {
1029                if !matches!(
1030                    operand_typed.resolved_type,
1031                    ResolvedType::Boolean | ResolvedType::Null
1032                ) {
1033                    return Err(PlannerError::TypeMismatch {
1034                        expected: "Boolean".to_string(),
1035                        found: operand_typed.resolved_type.type_name().to_string(),
1036                        line: span.start.line,
1037                        column: span.start.column,
1038                    });
1039                }
1040                ResolvedType::Boolean
1041            }
1042            UnaryOp::Minus => match &operand_typed.resolved_type {
1043                ResolvedType::Integer => ResolvedType::Integer,
1044                ResolvedType::BigInt => ResolvedType::BigInt,
1045                ResolvedType::Float => ResolvedType::Float,
1046                ResolvedType::Double => ResolvedType::Double,
1047                ResolvedType::Null => ResolvedType::Null,
1048                other => {
1049                    return Err(PlannerError::InvalidOperator {
1050                        op: "unary minus".to_string(),
1051                        type_name: other.type_name().to_string(),
1052                        line: span.start.line,
1053                        column: span.start.column,
1054                    });
1055                }
1056            },
1057        };
1058
1059        Ok(TypedExpr {
1060            kind: TypedExprKind::UnaryOp {
1061                op,
1062                operand: Box::new(operand_typed),
1063            },
1064            resolved_type: result_type,
1065            span,
1066        })
1067    }
1068
1069    /// Infer the type of a function call.
1070    #[allow(dead_code)]
1071    fn infer_function_call_type(
1072        &self,
1073        name: &str,
1074        args: &[Expr],
1075        distinct: bool,
1076        star: bool,
1077        table: &TableMetadata,
1078        span: Span,
1079    ) -> Result<TypedExpr, PlannerError> {
1080        // Type-check all arguments first
1081        let typed_args: Vec<TypedExpr> = args
1082            .iter()
1083            .map(|arg| self.infer_type(arg, table))
1084            .collect::<Result<Vec<_>, _>>()?;
1085
1086        // Delegate to check_function_call for validation and return type
1087        let result_type = self.check_function_call(name, &typed_args, distinct, star, span)?;
1088
1089        Ok(TypedExpr {
1090            kind: TypedExprKind::FunctionCall {
1091                name: name.to_string(),
1092                args: typed_args,
1093                distinct,
1094                star,
1095                over: None,
1096            },
1097            resolved_type: result_type,
1098            span,
1099        })
1100    }
1101
1102    #[allow(clippy::too_many_arguments)]
1103    fn infer_function_call_type_with_scope(
1104        &self,
1105        name: &str,
1106        args: &[Expr],
1107        distinct: bool,
1108        star: bool,
1109        over: Option<&WindowSpec>,
1110        scope: &[ScopedTable],
1111        plan_subquery: &SubqueryPlanner<'_>,
1112        span: Span,
1113    ) -> Result<TypedExpr, PlannerError> {
1114        let lower_name = name.to_ascii_lowercase();
1115        if over.is_some() && matches!(lower_name.as_str(), "lag" | "lead") {
1116            validate_offset_window_call(name, args.len(), distinct, star)?;
1117        }
1118
1119        let mut typed_args: Vec<TypedExpr> = args
1120            .iter()
1121            .map(|arg| self.infer_type_with_scope(arg, scope, plan_subquery))
1122            .collect::<Result<Vec<_>, _>>()?;
1123        let result_type = if over.is_some() {
1124            match lower_name.as_str() {
1125                "lag" | "lead" => self.infer_offset_window_result_type(name, &mut typed_args)?,
1126                "row_number" | "rank" | "dense_rank" => {
1127                    if !typed_args.is_empty() || distinct || star {
1128                        return Err(PlannerError::invalid_expression(format!(
1129                            "{}() window function takes no arguments",
1130                            name.to_ascii_uppercase()
1131                        )));
1132                    }
1133                    ResolvedType::BigInt
1134                }
1135                "sum" | "count" | "avg" | "min" | "max" => {
1136                    self.check_function_call(name, &typed_args, distinct, star, span)?
1137                }
1138                _ => {
1139                    return Err(PlannerError::unsupported_feature(
1140                        format!("function '{}' with OVER", name),
1141                        "future",
1142                        span,
1143                    ));
1144                }
1145            }
1146        } else {
1147            self.check_function_call(name, &typed_args, distinct, star, span)?
1148        };
1149
1150        let typed_over = over
1151            .map(|window| {
1152                let partition_by = window
1153                    .partition_by
1154                    .iter()
1155                    .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
1156                    .collect::<Result<Vec<_>, _>>()?;
1157                let order_by = window
1158                    .order_by
1159                    .iter()
1160                    .map(|order| {
1161                        let expr = self.infer_type_with_scope(&order.expr, scope, plan_subquery)?;
1162                        Ok(SortExpr::new(
1163                            expr,
1164                            order.asc.unwrap_or(true),
1165                            order.nulls_first.unwrap_or(false),
1166                        ))
1167                    })
1168                    .collect::<Result<Vec<_>, PlannerError>>()?;
1169                if let Some(frame) = &window.frame {
1170                    validate_window_frame(&lower_name, frame, &order_by)?;
1171                }
1172                Ok(TypedWindowSpec {
1173                    partition_by,
1174                    order_by,
1175                    frame: window.frame.clone(),
1176                })
1177            })
1178            .transpose()?;
1179
1180        Ok(TypedExpr {
1181            kind: TypedExprKind::FunctionCall {
1182                name: name.to_string(),
1183                args: typed_args,
1184                distinct,
1185                star,
1186                over: typed_over,
1187            },
1188            resolved_type: result_type,
1189            span,
1190        })
1191    }
1192
1193    fn infer_offset_window_result_type(
1194        &self,
1195        name: &str,
1196        args: &mut [TypedExpr],
1197    ) -> Result<ResolvedType, PlannerError> {
1198        if let Some(offset) = args.get(1)
1199            && !matches!(
1200                offset.resolved_type,
1201                ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Null
1202            )
1203        {
1204            return Err(PlannerError::type_mismatch(
1205                "INTEGER offset",
1206                offset.resolved_type.type_name(),
1207                offset.span,
1208            ));
1209        }
1210
1211        let value_type = args
1212            .first()
1213            .map(|arg| arg.resolved_type.clone())
1214            .ok_or_else(|| {
1215                PlannerError::invalid_expression(format!(
1216                    "{}() window function expects 1 to 3 arguments",
1217                    name.to_ascii_uppercase()
1218                ))
1219            })?;
1220        let result_type = if let Some(default) = args.get(2) {
1221            self.common_compatible_result_type(&value_type, &default.resolved_type, default.span)?
1222        } else {
1223            value_type
1224        };
1225
1226        coerce_compatible_result(&mut args[0], &result_type);
1227        if let Some(default) = args.get_mut(2) {
1228            coerce_compatible_result(default, &result_type);
1229        }
1230
1231        Ok(result_type)
1232    }
1233
1234    fn common_compatible_result_type(
1235        &self,
1236        current: &ResolvedType,
1237        next: &ResolvedType,
1238        span: Span,
1239    ) -> Result<ResolvedType, PlannerError> {
1240        if matches!(current, ResolvedType::Null) {
1241            return Ok(next.clone());
1242        }
1243        if matches!(next, ResolvedType::Null) || current == next {
1244            return Ok(current.clone());
1245        }
1246        if is_numeric_type(current) && is_numeric_type(next) {
1247            return self.check_arithmetic_op(current, next, span);
1248        }
1249        if next.can_cast_to(current) {
1250            return Ok(current.clone());
1251        }
1252        if current.can_cast_to(next) {
1253            return Ok(next.clone());
1254        }
1255        Err(PlannerError::type_mismatch(
1256            current.type_name(),
1257            next.type_name(),
1258            span,
1259        ))
1260    }
1261
1262    /// Infer the type of a BETWEEN expression.
1263    #[allow(dead_code)]
1264    fn infer_between_type(
1265        &self,
1266        expr: &Expr,
1267        low: &Expr,
1268        high: &Expr,
1269        negated: bool,
1270        table: &TableMetadata,
1271        span: Span,
1272    ) -> Result<TypedExpr, PlannerError> {
1273        let expr_typed = self.infer_type(expr, table)?;
1274        let low_typed = self.infer_type(low, table)?;
1275        let high_typed = self.infer_type(high, table)?;
1276
1277        // Check that all three expressions have compatible types
1278        self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
1279        self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
1280
1281        Ok(TypedExpr {
1282            kind: TypedExprKind::Between {
1283                expr: Box::new(expr_typed),
1284                low: Box::new(low_typed),
1285                high: Box::new(high_typed),
1286                negated,
1287            },
1288            resolved_type: ResolvedType::Boolean,
1289            span,
1290        })
1291    }
1292
1293    #[allow(clippy::too_many_arguments)]
1294    fn infer_between_type_with_scope(
1295        &self,
1296        expr: &Expr,
1297        low: &Expr,
1298        high: &Expr,
1299        negated: bool,
1300        scope: &[ScopedTable],
1301        plan_subquery: &SubqueryPlanner<'_>,
1302        span: Span,
1303    ) -> Result<TypedExpr, PlannerError> {
1304        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1305        let low_typed = self.infer_type_with_scope(low, scope, plan_subquery)?;
1306        let high_typed = self.infer_type_with_scope(high, scope, plan_subquery)?;
1307        self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
1308        self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
1309
1310        Ok(TypedExpr {
1311            kind: TypedExprKind::Between {
1312                expr: Box::new(expr_typed),
1313                low: Box::new(low_typed),
1314                high: Box::new(high_typed),
1315                negated,
1316            },
1317            resolved_type: ResolvedType::Boolean,
1318            span,
1319        })
1320    }
1321
1322    /// Infer the type of a LIKE expression.
1323    #[allow(dead_code)]
1324    #[allow(clippy::too_many_arguments)]
1325    fn infer_like_type(
1326        &self,
1327        expr: &Expr,
1328        pattern: &Expr,
1329        escape: Option<&Expr>,
1330        negated: bool,
1331        kind: PatternMatchKind,
1332        table: &TableMetadata,
1333        span: Span,
1334    ) -> Result<TypedExpr, PlannerError> {
1335        let expr_typed = self.infer_type(expr, table)?;
1336        let pattern_typed = self.infer_type(pattern, table)?;
1337
1338        // Expression must be text
1339        if !matches!(
1340            expr_typed.resolved_type,
1341            ResolvedType::Text | ResolvedType::Null
1342        ) {
1343            return Err(PlannerError::TypeMismatch {
1344                expected: "Text".to_string(),
1345                found: expr_typed.resolved_type.type_name().to_string(),
1346                line: expr.span.start.line,
1347                column: expr.span.start.column,
1348            });
1349        }
1350
1351        // Pattern must be text
1352        if !matches!(
1353            pattern_typed.resolved_type,
1354            ResolvedType::Text | ResolvedType::Null
1355        ) {
1356            return Err(PlannerError::TypeMismatch {
1357                expected: "Text".to_string(),
1358                found: pattern_typed.resolved_type.type_name().to_string(),
1359                line: pattern.span.start.line,
1360                column: pattern.span.start.column,
1361            });
1362        }
1363
1364        let escape_typed = if let Some(esc) = escape {
1365            let typed = self.infer_type(esc, table)?;
1366            if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
1367                return Err(PlannerError::TypeMismatch {
1368                    expected: "Text".to_string(),
1369                    found: typed.resolved_type.type_name().to_string(),
1370                    line: esc.span.start.line,
1371                    column: esc.span.start.column,
1372                });
1373            }
1374            Some(Box::new(typed))
1375        } else {
1376            None
1377        };
1378
1379        Ok(TypedExpr {
1380            kind: TypedExprKind::Like {
1381                expr: Box::new(expr_typed),
1382                pattern: Box::new(pattern_typed),
1383                escape: escape_typed,
1384                negated,
1385                kind,
1386            },
1387            resolved_type: ResolvedType::Boolean,
1388            span,
1389        })
1390    }
1391
1392    #[allow(clippy::too_many_arguments)]
1393    #[allow(clippy::too_many_arguments)]
1394    fn infer_like_type_with_scope(
1395        &self,
1396        expr: &Expr,
1397        pattern: &Expr,
1398        escape: Option<&Expr>,
1399        negated: bool,
1400        kind: PatternMatchKind,
1401        scope: &[ScopedTable],
1402        plan_subquery: &SubqueryPlanner<'_>,
1403        span: Span,
1404    ) -> Result<TypedExpr, PlannerError> {
1405        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1406        let pattern_typed = self.infer_type_with_scope(pattern, scope, plan_subquery)?;
1407
1408        if !matches!(
1409            expr_typed.resolved_type,
1410            ResolvedType::Text | ResolvedType::Null
1411        ) {
1412            return Err(PlannerError::TypeMismatch {
1413                expected: "Text".to_string(),
1414                found: expr_typed.resolved_type.type_name().to_string(),
1415                line: expr.span.start.line,
1416                column: expr.span.start.column,
1417            });
1418        }
1419
1420        if !matches!(
1421            pattern_typed.resolved_type,
1422            ResolvedType::Text | ResolvedType::Null
1423        ) {
1424            return Err(PlannerError::TypeMismatch {
1425                expected: "Text".to_string(),
1426                found: pattern_typed.resolved_type.type_name().to_string(),
1427                line: pattern.span.start.line,
1428                column: pattern.span.start.column,
1429            });
1430        }
1431
1432        let escape_typed = if let Some(esc) = escape {
1433            let typed = self.infer_type_with_scope(esc, scope, plan_subquery)?;
1434            if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
1435                return Err(PlannerError::TypeMismatch {
1436                    expected: "Text".to_string(),
1437                    found: typed.resolved_type.type_name().to_string(),
1438                    line: esc.span.start.line,
1439                    column: esc.span.start.column,
1440                });
1441            }
1442            Some(Box::new(typed))
1443        } else {
1444            None
1445        };
1446
1447        Ok(TypedExpr {
1448            kind: TypedExprKind::Like {
1449                expr: Box::new(expr_typed),
1450                pattern: Box::new(pattern_typed),
1451                escape: escape_typed,
1452                negated,
1453                kind,
1454            },
1455            resolved_type: ResolvedType::Boolean,
1456            span,
1457        })
1458    }
1459
1460    /// Infer the type of an IN list expression.
1461    #[allow(dead_code)]
1462    fn infer_in_list_type(
1463        &self,
1464        expr: &Expr,
1465        list: &[Expr],
1466        negated: bool,
1467        table: &TableMetadata,
1468        span: Span,
1469    ) -> Result<TypedExpr, PlannerError> {
1470        let expr_typed = self.infer_type(expr, table)?;
1471
1472        let typed_list: Vec<TypedExpr> = list
1473            .iter()
1474            .map(|item| {
1475                let typed = self.infer_type(item, table)?;
1476                // Check each item is compatible with the expression
1477                self.check_comparison_op(
1478                    &expr_typed.resolved_type,
1479                    &typed.resolved_type,
1480                    item.span,
1481                )?;
1482                Ok(typed)
1483            })
1484            .collect::<Result<Vec<_>, PlannerError>>()?;
1485
1486        Ok(TypedExpr {
1487            kind: TypedExprKind::InList {
1488                expr: Box::new(expr_typed),
1489                list: typed_list,
1490                negated,
1491            },
1492            resolved_type: ResolvedType::Boolean,
1493            span,
1494        })
1495    }
1496
1497    fn infer_in_list_type_with_scope(
1498        &self,
1499        expr: &Expr,
1500        list: &[Expr],
1501        negated: bool,
1502        scope: &[ScopedTable],
1503        plan_subquery: &SubqueryPlanner<'_>,
1504        span: Span,
1505    ) -> Result<TypedExpr, PlannerError> {
1506        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1507
1508        let typed_list: Vec<TypedExpr> = list
1509            .iter()
1510            .map(|item| {
1511                let typed = self.infer_type_with_scope(item, scope, plan_subquery)?;
1512                self.check_comparison_op(
1513                    &expr_typed.resolved_type,
1514                    &typed.resolved_type,
1515                    item.span,
1516                )?;
1517                Ok(typed)
1518            })
1519            .collect::<Result<Vec<_>, PlannerError>>()?;
1520
1521        Ok(TypedExpr {
1522            kind: TypedExprKind::InList {
1523                expr: Box::new(expr_typed),
1524                list: typed_list,
1525                negated,
1526            },
1527            resolved_type: ResolvedType::Boolean,
1528            span,
1529        })
1530    }
1531
1532    /// Infer the type of an IS NULL expression.
1533    #[allow(dead_code)]
1534    fn infer_is_null_type(
1535        &self,
1536        expr: &Expr,
1537        negated: bool,
1538        table: &TableMetadata,
1539        span: Span,
1540    ) -> Result<TypedExpr, PlannerError> {
1541        let expr_typed = self.infer_type(expr, table)?;
1542
1543        Ok(TypedExpr {
1544            kind: TypedExprKind::IsNull {
1545                expr: Box::new(expr_typed),
1546                negated,
1547            },
1548            resolved_type: ResolvedType::Boolean,
1549            span,
1550        })
1551    }
1552
1553    fn infer_is_null_type_with_scope(
1554        &self,
1555        expr: &Expr,
1556        negated: bool,
1557        scope: &[ScopedTable],
1558        plan_subquery: &SubqueryPlanner<'_>,
1559        span: Span,
1560    ) -> Result<TypedExpr, PlannerError> {
1561        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1562
1563        Ok(TypedExpr {
1564            kind: TypedExprKind::IsNull {
1565                expr: Box::new(expr_typed),
1566                negated,
1567            },
1568            resolved_type: ResolvedType::Boolean,
1569            span,
1570        })
1571    }
1572
1573    /// Infer the type of a vector literal.
1574    fn infer_vector_literal_type(
1575        &self,
1576        values: &[f64],
1577        span: Span,
1578    ) -> Result<TypedExpr, PlannerError> {
1579        Ok(TypedExpr {
1580            kind: TypedExprKind::VectorLiteral(values.to_vec()),
1581            resolved_type: ResolvedType::Vector {
1582                dimension: values.len() as u32,
1583                metric: VectorMetric::Cosine, // Default metric for literals
1584            },
1585            span,
1586        })
1587    }
1588
1589    /// Normalize a metric string to VectorMetric enum (case-insensitive).
1590    ///
1591    /// # Valid Values
1592    ///
1593    /// - "cosine" (case-insensitive) → `VectorMetric::Cosine`
1594    /// - "l2" (case-insensitive) → `VectorMetric::L2`
1595    /// - "inner" (case-insensitive) → `VectorMetric::Inner`
1596    ///
1597    /// # Errors
1598    ///
1599    /// Returns `PlannerError::InvalidMetric` if the value is not recognized.
1600    pub fn normalize_metric(&self, metric: &str, span: Span) -> Result<VectorMetric, PlannerError> {
1601        match metric.to_lowercase().as_str() {
1602            "cosine" => Ok(VectorMetric::Cosine),
1603            "l2" => Ok(VectorMetric::L2),
1604            "inner" => Ok(VectorMetric::Inner),
1605            _ => Err(PlannerError::InvalidMetric {
1606                value: metric.to_string(),
1607                line: span.start.line,
1608                column: span.start.column,
1609            }),
1610        }
1611    }
1612
1613    /// Check function call and return the result type.
1614    ///
1615    /// Validates that the function arguments have correct types and returns
1616    /// the result type.
1617    pub fn check_function_call(
1618        &self,
1619        name: &str,
1620        args: &[TypedExpr],
1621        distinct: bool,
1622        star: bool,
1623        span: Span,
1624    ) -> Result<ResolvedType, PlannerError> {
1625        let lower_name = name.to_ascii_lowercase();
1626
1627        match lower_name.as_str() {
1628            "count" => self.check_count(args, distinct, star, span),
1629            "sum" => self.check_sum(args, distinct, star, span),
1630            "total" => self.check_total(args, distinct, star, span),
1631            "avg" => self.check_avg(args, distinct, star, span),
1632            "min" => self.check_min_max(args, distinct, star, span),
1633            "max" => self.check_min_max(args, distinct, star, span),
1634            "group_concat" => self.check_group_concat(args, distinct, star, span),
1635            "string_agg" => self.check_string_agg(args, distinct, star, span),
1636            _ => {
1637                let Some(signature) = crate::scalar::signature(&lower_name) else {
1638                    return Err(PlannerError::unsupported_feature(
1639                        format!("function '{name}'"),
1640                        "future",
1641                        span,
1642                    ));
1643                };
1644                if distinct || star {
1645                    return Err(PlannerError::invalid_expression(format!(
1646                        "scalar function '{name}' does not support DISTINCT or *"
1647                    )));
1648                }
1649                signature.arity.validate(name, args.len(), span)?;
1650                (signature.check)(args)?;
1651                let types: Vec<_> = args.iter().map(|arg| arg.resolved_type.clone()).collect();
1652                match &signature.ret {
1653                    crate::scalar::ReturnRule::Fixed(ty) => Ok(ty.clone()),
1654                    crate::scalar::ReturnRule::FromArgs(rule) => rule(&types),
1655                }
1656            }
1657        }
1658    }
1659
1660    pub fn validate_having_expr(
1661        &self,
1662        expr: &TypedExpr,
1663        group_keys: &[TypedExpr],
1664        aggregates: &[AggregateExpr],
1665    ) -> Result<(), PlannerError> {
1666        use std::collections::HashSet;
1667
1668        let group_key_indices: HashSet<usize> = group_keys
1669            .iter()
1670            .filter_map(|expr| match &expr.kind {
1671                TypedExprKind::ColumnRef { column_index, .. } => Some(*column_index),
1672                _ => None,
1673            })
1674            .collect();
1675
1676        let aggregate_signatures: HashSet<AggregateSignature> = aggregates
1677            .iter()
1678            .map(aggregate_signature_from_expr)
1679            .collect();
1680
1681        fn walk(
1682            expr: &TypedExpr,
1683            group_key_indices: &HashSet<usize>,
1684            aggregate_signatures: &HashSet<AggregateSignature>,
1685        ) -> Result<(), PlannerError> {
1686            match &expr.kind {
1687                TypedExprKind::ColumnRef { column_index, .. } => {
1688                    if group_key_indices.contains(column_index) {
1689                        Ok(())
1690                    } else {
1691                        Err(PlannerError::invalid_expression(
1692                            "column in HAVING must be in GROUP BY or be aggregated".to_string(),
1693                        ))
1694                    }
1695                }
1696                TypedExprKind::FunctionCall {
1697                    name,
1698                    args,
1699                    distinct,
1700                    star,
1701                    over: _,
1702                } if is_aggregate_name(name) => {
1703                    let signature = aggregate_signature_from_call(name, args, *distinct, *star)?;
1704                    if aggregate_signatures.contains(&signature) {
1705                        Ok(())
1706                    } else {
1707                        Err(PlannerError::invalid_expression(
1708                            "aggregate in HAVING must appear in plan".to_string(),
1709                        ))
1710                    }
1711                }
1712                TypedExprKind::BinaryOp { left, right, .. } => {
1713                    walk(left, group_key_indices, aggregate_signatures)?;
1714                    walk(right, group_key_indices, aggregate_signatures)
1715                }
1716                TypedExprKind::UnaryOp { operand, .. } => {
1717                    walk(operand, group_key_indices, aggregate_signatures)
1718                }
1719                TypedExprKind::Case {
1720                    operand,
1721                    branches,
1722                    else_expr,
1723                } => {
1724                    if let Some(operand) = operand {
1725                        walk(operand, group_key_indices, aggregate_signatures)?;
1726                    }
1727                    for branch in branches {
1728                        walk(&branch.when, group_key_indices, aggregate_signatures)?;
1729                        walk(&branch.then, group_key_indices, aggregate_signatures)?;
1730                    }
1731                    if let Some(else_expr) = else_expr {
1732                        walk(else_expr, group_key_indices, aggregate_signatures)?;
1733                    }
1734                    Ok(())
1735                }
1736                TypedExprKind::FunctionCall { args, .. } => {
1737                    for arg in args {
1738                        walk(arg, group_key_indices, aggregate_signatures)?;
1739                    }
1740                    Ok(())
1741                }
1742                TypedExprKind::Between {
1743                    expr, low, high, ..
1744                } => {
1745                    walk(expr, group_key_indices, aggregate_signatures)?;
1746                    walk(low, group_key_indices, aggregate_signatures)?;
1747                    walk(high, group_key_indices, aggregate_signatures)
1748                }
1749                TypedExprKind::Like {
1750                    expr,
1751                    pattern,
1752                    escape,
1753                    ..
1754                } => {
1755                    walk(expr, group_key_indices, aggregate_signatures)?;
1756                    walk(pattern, group_key_indices, aggregate_signatures)?;
1757                    if let Some(esc) = escape {
1758                        walk(esc, group_key_indices, aggregate_signatures)?;
1759                    }
1760                    Ok(())
1761                }
1762                TypedExprKind::InList { expr, list, .. } => {
1763                    walk(expr, group_key_indices, aggregate_signatures)?;
1764                    for item in list {
1765                        walk(item, group_key_indices, aggregate_signatures)?;
1766                    }
1767                    Ok(())
1768                }
1769                TypedExprKind::IsNull { expr, .. } => {
1770                    walk(expr, group_key_indices, aggregate_signatures)
1771                }
1772                _ => Ok(()),
1773            }
1774        }
1775
1776        walk(expr, &group_key_indices, &aggregate_signatures)
1777    }
1778
1779    fn check_count(
1780        &self,
1781        args: &[TypedExpr],
1782        distinct: bool,
1783        star: bool,
1784        span: Span,
1785    ) -> Result<ResolvedType, PlannerError> {
1786        if star {
1787            if distinct {
1788                return Err(PlannerError::unsupported_feature(
1789                    "COUNT(DISTINCT *)",
1790                    "future",
1791                    span,
1792                ));
1793            }
1794            if !args.is_empty() {
1795                return Err(PlannerError::type_mismatch(
1796                    "no arguments with COUNT(*)",
1797                    format!("{} arguments", args.len()),
1798                    span,
1799                ));
1800            }
1801            return Ok(ResolvedType::BigInt);
1802        }
1803
1804        if args.len() != 1 {
1805            return Err(PlannerError::type_mismatch(
1806                "1 argument",
1807                format!("{} arguments", args.len()),
1808                span,
1809            ));
1810        }
1811
1812        if distinct {
1813            return Ok(ResolvedType::BigInt);
1814        }
1815
1816        Ok(ResolvedType::BigInt)
1817    }
1818
1819    fn check_sum(
1820        &self,
1821        args: &[TypedExpr],
1822        _distinct: bool,
1823        star: bool,
1824        span: Span,
1825    ) -> Result<ResolvedType, PlannerError> {
1826        if star {
1827            return Err(PlannerError::type_mismatch(
1828                "numeric argument",
1829                "COUNT(*) style",
1830                span,
1831            ));
1832        }
1833        let arg = self.require_single_arg(args, span)?;
1834        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
1835            return Err(PlannerError::type_mismatch(
1836                "numeric",
1837                arg.resolved_type.type_name().to_string(),
1838                arg.span,
1839            ));
1840        }
1841        Ok(crate::planner::aggregate_expr::sum_result_type(
1842            &arg.resolved_type,
1843        ))
1844    }
1845
1846    fn check_total(
1847        &self,
1848        args: &[TypedExpr],
1849        distinct: bool,
1850        star: bool,
1851        span: Span,
1852    ) -> Result<ResolvedType, PlannerError> {
1853        if star {
1854            return Err(PlannerError::type_mismatch(
1855                "numeric argument",
1856                "COUNT(*) style",
1857                span,
1858            ));
1859        }
1860        if distinct {
1861            return Err(PlannerError::unsupported_feature(
1862                "TOTAL(DISTINCT ...)",
1863                "future",
1864                span,
1865            ));
1866        }
1867        let arg = self.require_single_arg(args, span)?;
1868        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
1869            return Err(PlannerError::type_mismatch(
1870                "numeric",
1871                arg.resolved_type.type_name().to_string(),
1872                arg.span,
1873            ));
1874        }
1875        Ok(ResolvedType::Double)
1876    }
1877
1878    fn check_avg(
1879        &self,
1880        args: &[TypedExpr],
1881        _distinct: bool,
1882        star: bool,
1883        span: Span,
1884    ) -> Result<ResolvedType, PlannerError> {
1885        if star {
1886            return Err(PlannerError::type_mismatch(
1887                "numeric argument",
1888                "COUNT(*) style",
1889                span,
1890            ));
1891        }
1892        let arg = self.require_single_arg(args, span)?;
1893        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
1894            return Err(PlannerError::type_mismatch(
1895                "numeric",
1896                arg.resolved_type.type_name().to_string(),
1897                arg.span,
1898            ));
1899        }
1900        Ok(ResolvedType::Double)
1901    }
1902
1903    fn check_min_max(
1904        &self,
1905        args: &[TypedExpr],
1906        _distinct: bool,
1907        star: bool,
1908        span: Span,
1909    ) -> Result<ResolvedType, PlannerError> {
1910        if star {
1911            return Err(PlannerError::type_mismatch(
1912                "argument",
1913                "COUNT(*) style",
1914                span,
1915            ));
1916        }
1917        let arg = self.require_single_arg(args, span)?;
1918        if matches!(arg.resolved_type, ResolvedType::Vector { .. }) {
1919            return Err(PlannerError::type_mismatch(
1920                "comparable",
1921                arg.resolved_type.type_name().to_string(),
1922                arg.span,
1923            ));
1924        }
1925        Ok(arg.resolved_type.clone())
1926    }
1927
1928    fn check_group_concat(
1929        &self,
1930        args: &[TypedExpr],
1931        _distinct: bool,
1932        star: bool,
1933        span: Span,
1934    ) -> Result<ResolvedType, PlannerError> {
1935        if star {
1936            return Err(PlannerError::type_mismatch(
1937                "text argument",
1938                "COUNT(*) style",
1939                span,
1940            ));
1941        }
1942        if args.is_empty() || args.len() > 2 {
1943            return Err(PlannerError::type_mismatch(
1944                "1 or 2 arguments",
1945                format!("{} arguments", args.len()),
1946                span,
1947            ));
1948        }
1949        if !matches!(
1950            args[0].resolved_type,
1951            ResolvedType::Text | ResolvedType::Null
1952        ) {
1953            return Err(PlannerError::type_mismatch(
1954                "Text",
1955                args[0].resolved_type.type_name().to_string(),
1956                args[0].span,
1957            ));
1958        }
1959        if args.len() == 2
1960            && !matches!(
1961                args[1].resolved_type,
1962                ResolvedType::Text | ResolvedType::Null
1963            )
1964        {
1965            return Err(PlannerError::type_mismatch(
1966                "Text",
1967                args[1].resolved_type.type_name().to_string(),
1968                args[1].span,
1969            ));
1970        }
1971        Ok(ResolvedType::Text)
1972    }
1973
1974    fn check_string_agg(
1975        &self,
1976        args: &[TypedExpr],
1977        _distinct: bool,
1978        star: bool,
1979        span: Span,
1980    ) -> Result<ResolvedType, PlannerError> {
1981        if star {
1982            return Err(PlannerError::type_mismatch(
1983                "text argument",
1984                "COUNT(*) style",
1985                span,
1986            ));
1987        }
1988        if args.len() != 2 {
1989            return Err(PlannerError::type_mismatch(
1990                "2 arguments",
1991                format!("{} arguments", args.len()),
1992                span,
1993            ));
1994        }
1995        if !matches!(
1996            args[0].resolved_type,
1997            ResolvedType::Text | ResolvedType::Null
1998        ) {
1999            return Err(PlannerError::type_mismatch(
2000                "Text",
2001                args[0].resolved_type.type_name().to_string(),
2002                args[0].span,
2003            ));
2004        }
2005        if !matches!(
2006            args[1].resolved_type,
2007            ResolvedType::Text | ResolvedType::Null
2008        ) {
2009            return Err(PlannerError::type_mismatch(
2010                "Text",
2011                args[1].resolved_type.type_name().to_string(),
2012                args[1].span,
2013            ));
2014        }
2015        Ok(ResolvedType::Text)
2016    }
2017
2018    fn require_single_arg<'b>(
2019        &self,
2020        args: &'b [TypedExpr],
2021        span: Span,
2022    ) -> Result<&'b TypedExpr, PlannerError> {
2023        if args.len() != 1 {
2024            return Err(PlannerError::type_mismatch(
2025                "1 argument",
2026                format!("{} arguments", args.len()),
2027                span,
2028            ));
2029        }
2030        Ok(&args[0])
2031    }
2032
2033    /// Check vector_distance function arguments.
2034    ///
2035    /// Signature: `vector_distance(column: Vector, vector: Vector, metric: Text) -> Double`
2036    ///
2037    /// # Requirements
2038    ///
2039    /// - First argument must be a Vector type (column reference)
2040    /// - Second argument must be a Vector type (vector literal)
2041    /// - Third argument must be a Text type (metric string)
2042    /// - Vector dimensions must match
2043    pub fn check_vector_distance(
2044        &self,
2045        args: &[TypedExpr],
2046        span: Span,
2047    ) -> Result<ResolvedType, PlannerError> {
2048        if args.len() != 3 {
2049            return Err(PlannerError::TypeMismatch {
2050                expected: "3 arguments".to_string(),
2051                found: format!("{} arguments", args.len()),
2052                line: span.start.line,
2053                column: span.start.column,
2054            });
2055        }
2056
2057        // First argument: Vector column
2058        let col_dim = match &args[0].resolved_type {
2059            ResolvedType::Vector { dimension, .. } => *dimension,
2060            other => {
2061                return Err(PlannerError::TypeMismatch {
2062                    expected: "Vector".to_string(),
2063                    found: other.type_name().to_string(),
2064                    line: args[0].span.start.line,
2065                    column: args[0].span.start.column,
2066                });
2067            }
2068        };
2069
2070        // Second argument: Vector literal
2071        let vec_dim = match &args[1].resolved_type {
2072            ResolvedType::Vector { dimension, .. } => *dimension,
2073            other => {
2074                return Err(PlannerError::TypeMismatch {
2075                    expected: "Vector".to_string(),
2076                    found: other.type_name().to_string(),
2077                    line: args[1].span.start.line,
2078                    column: args[1].span.start.column,
2079                });
2080            }
2081        };
2082
2083        // Check dimension match
2084        self.check_vector_dimension(col_dim, vec_dim, args[1].span)?;
2085
2086        // Third argument: Metric string
2087        match &args[2].resolved_type {
2088            ResolvedType::Text => {
2089                // Validate metric value if it's a literal
2090                if let TypedExprKind::Literal(Literal::String(s)) = &args[2].kind {
2091                    self.normalize_metric(s, args[2].span)?;
2092                }
2093            }
2094            ResolvedType::Null => {
2095                // NULL metric is not allowed
2096                return Err(PlannerError::TypeMismatch {
2097                    expected: "Text (metric)".to_string(),
2098                    found: "Null".to_string(),
2099                    line: args[2].span.start.line,
2100                    column: args[2].span.start.column,
2101                });
2102            }
2103            other => {
2104                return Err(PlannerError::TypeMismatch {
2105                    expected: "Text (metric)".to_string(),
2106                    found: other.type_name().to_string(),
2107                    line: args[2].span.start.line,
2108                    column: args[2].span.start.column,
2109                });
2110            }
2111        }
2112
2113        Ok(ResolvedType::Double)
2114    }
2115
2116    /// Check vector_similarity function arguments.
2117    ///
2118    /// Signature: `vector_similarity(column: Vector, vector: Vector, metric: Text) -> Double`
2119    ///
2120    /// Same validation rules as vector_distance.
2121    pub fn check_vector_similarity(
2122        &self,
2123        args: &[TypedExpr],
2124        span: Span,
2125    ) -> Result<ResolvedType, PlannerError> {
2126        // Same validation as vector_distance
2127        self.check_vector_distance(args, span)
2128    }
2129
2130    /// Check that two vector dimensions match.
2131    ///
2132    /// # Errors
2133    ///
2134    /// Returns `PlannerError::VectorDimensionMismatch` if dimensions don't match.
2135    pub fn check_vector_dimension(
2136        &self,
2137        expected: u32,
2138        found: u32,
2139        span: Span,
2140    ) -> Result<(), PlannerError> {
2141        if expected != found {
2142            Err(PlannerError::VectorDimensionMismatch {
2143                expected,
2144                found,
2145                line: span.start.line,
2146                column: span.start.column,
2147            })
2148        } else {
2149            Ok(())
2150        }
2151    }
2152
2153    // ============================================================
2154    // INSERT/UPDATE Type Checking Methods (Task 13)
2155    // ============================================================
2156
2157    /// Check INSERT values against table columns.
2158    ///
2159    /// Validates that:
2160    /// - The number of values matches the number of columns
2161    /// - Each value's type is compatible with the column type
2162    /// - NOT NULL constraints are satisfied
2163    /// - Vector dimensions match for vector columns
2164    ///
2165    /// # Column Order
2166    ///
2167    /// If `columns` is empty, uses `TableMetadata.column_names()` order (definition order).
2168    ///
2169    /// # Errors
2170    ///
2171    /// - `ColumnValueCountMismatch`: Number of values doesn't match columns
2172    /// - `TypeMismatch`: Value type incompatible with column type
2173    /// - `NullConstraintViolation`: NULL value for NOT NULL column
2174    /// - `VectorDimensionMismatch`: Vector dimension mismatch
2175    pub fn check_insert_values(
2176        &self,
2177        table: &TableMetadata,
2178        columns: &[String],
2179        values: &[Vec<Expr>],
2180        span: Span,
2181    ) -> Result<Vec<Vec<TypedExpr>>, PlannerError> {
2182        // Determine the target columns
2183        let target_columns: Vec<&str> = if columns.is_empty() {
2184            table.column_names()
2185        } else {
2186            columns.iter().map(|s| s.as_str()).collect()
2187        };
2188
2189        let mut typed_rows = Vec::with_capacity(values.len());
2190
2191        for row in values {
2192            // Check value count matches column count
2193            if row.len() != target_columns.len() {
2194                return Err(PlannerError::ColumnValueCountMismatch {
2195                    columns: target_columns.len(),
2196                    values: row.len(),
2197                    line: span.start.line,
2198                    column: span.start.column,
2199                });
2200            }
2201
2202            let mut typed_values = Vec::with_capacity(row.len());
2203
2204            for (value, col_name) in row.iter().zip(target_columns.iter()) {
2205                // Get column metadata
2206                let col_meta =
2207                    table
2208                        .get_column(col_name)
2209                        .ok_or_else(|| PlannerError::ColumnNotFound {
2210                            column: col_name.to_string(),
2211                            table: table.name.clone(),
2212                            line: span.start.line,
2213                            col: span.start.column,
2214                        })?;
2215
2216                // Type-check the value expression
2217                let typed_value = self.infer_type(value, table)?;
2218
2219                // Check NOT NULL constraint
2220                self.check_null_constraint(col_meta, &typed_value, value.span)?;
2221
2222                // Check type compatibility
2223                self.check_type_compatibility(
2224                    &col_meta.data_type,
2225                    &typed_value.resolved_type,
2226                    value.span,
2227                )?;
2228
2229                let typed_value =
2230                    self.coerce_column_value(&col_meta.data_type, typed_value, value.span);
2231
2232                // For vector types, also check dimension
2233                if let (
2234                    ResolvedType::Vector {
2235                        dimension: expected_dim,
2236                        ..
2237                    },
2238                    ResolvedType::Vector {
2239                        dimension: actual_dim,
2240                        ..
2241                    },
2242                ) = (&col_meta.data_type, &typed_value.resolved_type)
2243                {
2244                    self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
2245                }
2246
2247                typed_values.push(typed_value);
2248            }
2249
2250            typed_rows.push(typed_values);
2251        }
2252
2253        Ok(typed_rows)
2254    }
2255
2256    /// Check UPDATE assignment type compatibility.
2257    ///
2258    /// Validates that the value's type is compatible with the column type.
2259    ///
2260    /// # Errors
2261    ///
2262    /// - `ColumnNotFound`: Column doesn't exist
2263    /// - `TypeMismatch`: Value type incompatible with column type
2264    /// - `NullConstraintViolation`: NULL value for NOT NULL column
2265    /// - `VectorDimensionMismatch`: Vector dimension mismatch
2266    pub fn check_assignment(
2267        &self,
2268        table: &TableMetadata,
2269        column: &str,
2270        value: &Expr,
2271        span: Span,
2272    ) -> Result<TypedExpr, PlannerError> {
2273        // Get column metadata
2274        let col_meta = table
2275            .get_column(column)
2276            .ok_or_else(|| PlannerError::ColumnNotFound {
2277                column: column.to_string(),
2278                table: table.name.clone(),
2279                line: span.start.line,
2280                col: span.start.column,
2281            })?;
2282
2283        // Type-check the value expression
2284        let typed_value = self.infer_type(value, table)?;
2285
2286        // Check NOT NULL constraint
2287        self.check_null_constraint(col_meta, &typed_value, value.span)?;
2288
2289        // Check type compatibility
2290        self.check_type_compatibility(&col_meta.data_type, &typed_value.resolved_type, value.span)?;
2291
2292        let typed_value = self.coerce_column_value(&col_meta.data_type, typed_value, value.span);
2293
2294        // For vector types, also check dimension
2295        if let (
2296            ResolvedType::Vector {
2297                dimension: expected_dim,
2298                ..
2299            },
2300            ResolvedType::Vector {
2301                dimension: actual_dim,
2302                ..
2303            },
2304        ) = (&col_meta.data_type, &typed_value.resolved_type)
2305        {
2306            self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
2307        }
2308
2309        Ok(typed_value)
2310    }
2311
2312    /// Check NOT NULL constraint for a value.
2313    ///
2314    /// # Errors
2315    ///
2316    /// Returns `PlannerError::NullConstraintViolation` if the column has NOT NULL
2317    /// constraint and the value is NULL.
2318    pub fn check_null_constraint(
2319        &self,
2320        column: &crate::catalog::ColumnMetadata,
2321        value: &TypedExpr,
2322        span: Span,
2323    ) -> Result<(), PlannerError> {
2324        if column.not_null && matches!(value.resolved_type, ResolvedType::Null) {
2325            Err(PlannerError::NullConstraintViolation {
2326                column: column.name.clone(),
2327                line: span.start.line,
2328                col: span.start.column,
2329            })
2330        } else {
2331            Ok(())
2332        }
2333    }
2334
2335    /// Check type compatibility between expected and actual types.
2336    ///
2337    /// Uses implicit type conversion rules defined in `ResolvedType::can_cast_to`.
2338    ///
2339    /// # Errors
2340    ///
2341    /// Returns `PlannerError::TypeMismatch` if types are incompatible.
2342    fn check_type_compatibility(
2343        &self,
2344        expected: &ResolvedType,
2345        actual: &ResolvedType,
2346        span: Span,
2347    ) -> Result<(), PlannerError> {
2348        // Same type is always compatible
2349        if expected == actual {
2350            return Ok(());
2351        }
2352
2353        // Check if implicit cast is allowed
2354        if actual.can_cast_to(expected) {
2355            return Ok(());
2356        }
2357
2358        // Special case: Vector types with same dimension but different metric are compatible
2359        // (the column's metric is used)
2360        if let (
2361            ResolvedType::Vector {
2362                dimension: d1,
2363                metric: _,
2364            },
2365            ResolvedType::Vector {
2366                dimension: d2,
2367                metric: _,
2368            },
2369        ) = (expected, actual)
2370        {
2371            // Dimensions must match for vector compatibility
2372            if *d1 == *d2 {
2373                return Ok(());
2374            }
2375            // Different dimensions will fall through to TypeMismatch error
2376        }
2377
2378        Err(PlannerError::TypeMismatch {
2379            expected: expected.type_name().to_string(),
2380            found: actual.type_name().to_string(),
2381            line: span.start.line,
2382            column: span.start.column,
2383        })
2384    }
2385
2386    /// Insert an execution-time coercion where a column accepts a value whose
2387    /// source representation differs from its storage representation.
2388    fn coerce_column_value(
2389        &self,
2390        expected: &ResolvedType,
2391        value: TypedExpr,
2392        span: Span,
2393    ) -> TypedExpr {
2394        if value.resolved_type != *expected
2395            && value.resolved_type != ResolvedType::Null
2396            && matches!(
2397                expected,
2398                ResolvedType::Integer
2399                    | ResolvedType::BigInt
2400                    | ResolvedType::Float
2401                    | ResolvedType::Double
2402                    | ResolvedType::Timestamp
2403            )
2404        {
2405            TypedExpr::cast(value, expected.clone(), span)
2406        } else {
2407            value
2408        }
2409    }
2410}
2411
2412fn is_numeric_type(ty: &ResolvedType) -> bool {
2413    matches!(
2414        ty,
2415        ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Float | ResolvedType::Double
2416    )
2417}
2418
2419fn validate_window_frame(
2420    function_name: &str,
2421    frame: &WindowFrame,
2422    order_by: &[SortExpr],
2423) -> Result<(), PlannerError> {
2424    if !matches!(function_name, "sum" | "count" | "avg" | "min" | "max") {
2425        return Err(PlannerError::invalid_expression(format!(
2426            "explicit window frames are only supported for aggregate functions, not {}()",
2427            function_name.to_ascii_uppercase()
2428        )));
2429    }
2430    if order_by.is_empty() {
2431        return Err(PlannerError::invalid_expression(
2432            "explicit ROWS/RANGE window frames require ORDER BY for deterministic evaluation",
2433        ));
2434    }
2435    if matches!(frame.start_bound, WindowFrameBound::UnboundedFollowing) {
2436        return Err(PlannerError::invalid_expression(
2437            "window frame start cannot be UNBOUNDED FOLLOWING",
2438        ));
2439    }
2440    if matches!(frame.end_bound, WindowFrameBound::UnboundedPreceding) {
2441        return Err(PlannerError::invalid_expression(
2442            "window frame end cannot be UNBOUNDED PRECEDING",
2443        ));
2444    }
2445    if (matches!(frame.start_bound, WindowFrameBound::CurrentRow)
2446        && matches!(frame.end_bound, WindowFrameBound::Preceding(_)))
2447        || (matches!(frame.start_bound, WindowFrameBound::Following(_))
2448            && matches!(
2449                frame.end_bound,
2450                WindowFrameBound::Preceding(_) | WindowFrameBound::CurrentRow
2451            ))
2452    {
2453        return Err(PlannerError::invalid_expression(
2454            "window frame bounds are reversed",
2455        ));
2456    }
2457
2458    let has_offset = matches!(
2459        frame.start_bound,
2460        WindowFrameBound::Preceding(_) | WindowFrameBound::Following(_)
2461    ) || matches!(
2462        frame.end_bound,
2463        WindowFrameBound::Preceding(_) | WindowFrameBound::Following(_)
2464    );
2465    if frame.units == WindowFrameUnits::Range && has_offset {
2466        if order_by.len() != 1 {
2467            return Err(PlannerError::invalid_expression(
2468                "RANGE offset frames require exactly one ORDER BY expression",
2469            ));
2470        }
2471        if !is_numeric_type(&order_by[0].expr.resolved_type) {
2472            return Err(PlannerError::invalid_expression(format!(
2473                "RANGE offset ORDER BY expression must be numeric, found {:?}",
2474                order_by[0].expr.resolved_type
2475            )));
2476        }
2477    }
2478    Ok(())
2479}
2480
2481fn validate_offset_window_call(
2482    name: &str,
2483    arg_count: usize,
2484    distinct: bool,
2485    star: bool,
2486) -> Result<(), PlannerError> {
2487    let display_name = name.to_ascii_uppercase();
2488    if distinct {
2489        return Err(PlannerError::invalid_expression(format!(
2490            "{display_name}() window function does not accept DISTINCT"
2491        )));
2492    }
2493    if star {
2494        return Err(PlannerError::invalid_expression(format!(
2495            "{display_name}() window function does not accept a star argument"
2496        )));
2497    }
2498    if !(1..=3).contains(&arg_count) {
2499        return Err(PlannerError::invalid_expression(format!(
2500            "{display_name}() window function expects 1 to 3 arguments"
2501        )));
2502    }
2503    Ok(())
2504}
2505
2506fn coerce_compatible_result(expr: &mut TypedExpr, target: &ResolvedType) {
2507    if expr.resolved_type == *target || matches!(expr.resolved_type, ResolvedType::Null) {
2508        return;
2509    }
2510    let span = expr.span;
2511    *expr = TypedExpr::cast(expr.clone(), target.clone(), span);
2512}
2513
2514fn coerce_case_result(expr: &mut TypedExpr, target: &ResolvedType) {
2515    if expr.resolved_type == *target || matches!(expr.resolved_type, ResolvedType::Null) {
2516        return;
2517    }
2518    let span = expr.span;
2519    *expr = TypedExpr::cast(expr.clone(), target.clone(), span);
2520}
2521
2522#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2523struct AggregateSignature {
2524    name: String,
2525    distinct: bool,
2526    star: bool,
2527    arg_key: Option<String>,
2528    separator: Option<String>,
2529}
2530
2531fn is_aggregate_name(name: &str) -> bool {
2532    matches!(
2533        name.to_ascii_lowercase().as_str(),
2534        "count" | "sum" | "total" | "avg" | "min" | "max" | "group_concat" | "string_agg"
2535    )
2536}
2537
2538fn aggregate_signature_from_expr(expr: &AggregateExpr) -> AggregateSignature {
2539    let (name, separator, star, arg) = match &expr.function {
2540        AggregateFunction::Count => (
2541            "count".to_string(),
2542            None,
2543            expr.arg.is_none(),
2544            expr.arg.as_ref(),
2545        ),
2546        AggregateFunction::Sum => ("sum".to_string(), None, false, expr.arg.as_ref()),
2547        AggregateFunction::Total => ("total".to_string(), None, false, expr.arg.as_ref()),
2548        AggregateFunction::Avg => ("avg".to_string(), None, false, expr.arg.as_ref()),
2549        AggregateFunction::Min => ("min".to_string(), None, false, expr.arg.as_ref()),
2550        AggregateFunction::Max => ("max".to_string(), None, false, expr.arg.as_ref()),
2551        AggregateFunction::GroupConcat { separator } => (
2552            "group_concat".to_string(),
2553            separator.clone(),
2554            false,
2555            expr.arg.as_ref(),
2556        ),
2557        AggregateFunction::StringAgg { separator } => (
2558            "string_agg".to_string(),
2559            separator.clone(),
2560            false,
2561            expr.arg.as_ref(),
2562        ),
2563    };
2564    AggregateSignature {
2565        name,
2566        distinct: expr.distinct,
2567        star,
2568        arg_key: arg.map(typed_expr_signature),
2569        separator,
2570    }
2571}
2572
2573fn aggregate_signature_from_call(
2574    name: &str,
2575    args: &[TypedExpr],
2576    distinct: bool,
2577    star: bool,
2578) -> Result<AggregateSignature, PlannerError> {
2579    let separator = if name.eq_ignore_ascii_case("group_concat") && args.len() == 2 {
2580        if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
2581            Some(value.clone())
2582        } else {
2583            return Err(PlannerError::invalid_expression(
2584                "GROUP_CONCAT separator must be a string literal".to_string(),
2585            ));
2586        }
2587    } else if name.eq_ignore_ascii_case("string_agg") && args.len() == 2 {
2588        if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
2589            Some(value.clone())
2590        } else {
2591            return Err(PlannerError::invalid_expression(
2592                "STRING_AGG separator must be a string literal".to_string(),
2593            ));
2594        }
2595    } else {
2596        None
2597    };
2598    Ok(AggregateSignature {
2599        name: name.to_ascii_lowercase(),
2600        distinct,
2601        star,
2602        arg_key: args.first().map(typed_expr_signature),
2603        separator,
2604    })
2605}
2606
2607fn typed_expr_signature(expr: &TypedExpr) -> String {
2608    format!("{:?}", expr.kind)
2609}
2610
2611fn single_column_type(schema: &[ColumnMetadata], span: Span) -> Result<ResolvedType, PlannerError> {
2612    match schema {
2613        [column] => Ok(column.data_type.clone()),
2614        [] => Err(PlannerError::type_mismatch(
2615            "one-column subquery",
2616            "zero-column subquery",
2617            span,
2618        )),
2619        _ => Err(PlannerError::type_mismatch(
2620            "one-column subquery",
2621            format!("{} columns", schema.len()),
2622            span,
2623        )),
2624    }
2625}
2626
2627// Tests are in type_checker/tests.rs
2628#[cfg(test)]
2629#[path = "type_checker/tests.rs"]
2630mod tests;