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    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 typed_args: Vec<TypedExpr> = args
1115            .iter()
1116            .map(|arg| self.infer_type_with_scope(arg, scope, plan_subquery))
1117            .collect::<Result<Vec<_>, _>>()?;
1118        let lower_name = name.to_ascii_lowercase();
1119        let result_type = if over.is_some() {
1120            match lower_name.as_str() {
1121                "lag" | "lead" => {
1122                    return Err(PlannerError::unsupported_feature(
1123                        format!("{} window function", name.to_ascii_uppercase()),
1124                        "future",
1125                        span,
1126                    ));
1127                }
1128                "row_number" | "rank" | "dense_rank" => {
1129                    if !typed_args.is_empty() || distinct || star {
1130                        return Err(PlannerError::invalid_expression(format!(
1131                            "{}() window function takes no arguments",
1132                            name.to_ascii_uppercase()
1133                        )));
1134                    }
1135                    ResolvedType::BigInt
1136                }
1137                "sum" | "count" | "avg" | "min" | "max" => {
1138                    self.check_function_call(name, &typed_args, distinct, star, span)?
1139                }
1140                _ => {
1141                    return Err(PlannerError::unsupported_feature(
1142                        format!("function '{}' with OVER", name),
1143                        "future",
1144                        span,
1145                    ));
1146                }
1147            }
1148        } else {
1149            self.check_function_call(name, &typed_args, distinct, star, span)?
1150        };
1151
1152        let typed_over = over
1153            .map(|window| {
1154                let partition_by = window
1155                    .partition_by
1156                    .iter()
1157                    .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
1158                    .collect::<Result<Vec<_>, _>>()?;
1159                let order_by = window
1160                    .order_by
1161                    .iter()
1162                    .map(|order| {
1163                        let expr = self.infer_type_with_scope(&order.expr, scope, plan_subquery)?;
1164                        Ok(SortExpr::new(
1165                            expr,
1166                            order.asc.unwrap_or(true),
1167                            order.nulls_first.unwrap_or(false),
1168                        ))
1169                    })
1170                    .collect::<Result<Vec<_>, PlannerError>>()?;
1171                Ok(TypedWindowSpec {
1172                    partition_by,
1173                    order_by,
1174                })
1175            })
1176            .transpose()?;
1177
1178        Ok(TypedExpr {
1179            kind: TypedExprKind::FunctionCall {
1180                name: name.to_string(),
1181                args: typed_args,
1182                distinct,
1183                star,
1184                over: typed_over,
1185            },
1186            resolved_type: result_type,
1187            span,
1188        })
1189    }
1190
1191    /// Infer the type of a BETWEEN expression.
1192    #[allow(dead_code)]
1193    fn infer_between_type(
1194        &self,
1195        expr: &Expr,
1196        low: &Expr,
1197        high: &Expr,
1198        negated: bool,
1199        table: &TableMetadata,
1200        span: Span,
1201    ) -> Result<TypedExpr, PlannerError> {
1202        let expr_typed = self.infer_type(expr, table)?;
1203        let low_typed = self.infer_type(low, table)?;
1204        let high_typed = self.infer_type(high, table)?;
1205
1206        // Check that all three expressions have compatible types
1207        self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
1208        self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
1209
1210        Ok(TypedExpr {
1211            kind: TypedExprKind::Between {
1212                expr: Box::new(expr_typed),
1213                low: Box::new(low_typed),
1214                high: Box::new(high_typed),
1215                negated,
1216            },
1217            resolved_type: ResolvedType::Boolean,
1218            span,
1219        })
1220    }
1221
1222    #[allow(clippy::too_many_arguments)]
1223    fn infer_between_type_with_scope(
1224        &self,
1225        expr: &Expr,
1226        low: &Expr,
1227        high: &Expr,
1228        negated: bool,
1229        scope: &[ScopedTable],
1230        plan_subquery: &SubqueryPlanner<'_>,
1231        span: Span,
1232    ) -> Result<TypedExpr, PlannerError> {
1233        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1234        let low_typed = self.infer_type_with_scope(low, scope, plan_subquery)?;
1235        let high_typed = self.infer_type_with_scope(high, scope, plan_subquery)?;
1236        self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
1237        self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
1238
1239        Ok(TypedExpr {
1240            kind: TypedExprKind::Between {
1241                expr: Box::new(expr_typed),
1242                low: Box::new(low_typed),
1243                high: Box::new(high_typed),
1244                negated,
1245            },
1246            resolved_type: ResolvedType::Boolean,
1247            span,
1248        })
1249    }
1250
1251    /// Infer the type of a LIKE expression.
1252    #[allow(dead_code)]
1253    #[allow(clippy::too_many_arguments)]
1254    fn infer_like_type(
1255        &self,
1256        expr: &Expr,
1257        pattern: &Expr,
1258        escape: Option<&Expr>,
1259        negated: bool,
1260        kind: PatternMatchKind,
1261        table: &TableMetadata,
1262        span: Span,
1263    ) -> Result<TypedExpr, PlannerError> {
1264        let expr_typed = self.infer_type(expr, table)?;
1265        let pattern_typed = self.infer_type(pattern, table)?;
1266
1267        // Expression must be text
1268        if !matches!(
1269            expr_typed.resolved_type,
1270            ResolvedType::Text | ResolvedType::Null
1271        ) {
1272            return Err(PlannerError::TypeMismatch {
1273                expected: "Text".to_string(),
1274                found: expr_typed.resolved_type.type_name().to_string(),
1275                line: expr.span.start.line,
1276                column: expr.span.start.column,
1277            });
1278        }
1279
1280        // Pattern must be text
1281        if !matches!(
1282            pattern_typed.resolved_type,
1283            ResolvedType::Text | ResolvedType::Null
1284        ) {
1285            return Err(PlannerError::TypeMismatch {
1286                expected: "Text".to_string(),
1287                found: pattern_typed.resolved_type.type_name().to_string(),
1288                line: pattern.span.start.line,
1289                column: pattern.span.start.column,
1290            });
1291        }
1292
1293        let escape_typed = if let Some(esc) = escape {
1294            let typed = self.infer_type(esc, table)?;
1295            if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
1296                return Err(PlannerError::TypeMismatch {
1297                    expected: "Text".to_string(),
1298                    found: typed.resolved_type.type_name().to_string(),
1299                    line: esc.span.start.line,
1300                    column: esc.span.start.column,
1301                });
1302            }
1303            Some(Box::new(typed))
1304        } else {
1305            None
1306        };
1307
1308        Ok(TypedExpr {
1309            kind: TypedExprKind::Like {
1310                expr: Box::new(expr_typed),
1311                pattern: Box::new(pattern_typed),
1312                escape: escape_typed,
1313                negated,
1314                kind,
1315            },
1316            resolved_type: ResolvedType::Boolean,
1317            span,
1318        })
1319    }
1320
1321    #[allow(clippy::too_many_arguments)]
1322    #[allow(clippy::too_many_arguments)]
1323    fn infer_like_type_with_scope(
1324        &self,
1325        expr: &Expr,
1326        pattern: &Expr,
1327        escape: Option<&Expr>,
1328        negated: bool,
1329        kind: PatternMatchKind,
1330        scope: &[ScopedTable],
1331        plan_subquery: &SubqueryPlanner<'_>,
1332        span: Span,
1333    ) -> Result<TypedExpr, PlannerError> {
1334        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1335        let pattern_typed = self.infer_type_with_scope(pattern, scope, plan_subquery)?;
1336
1337        if !matches!(
1338            expr_typed.resolved_type,
1339            ResolvedType::Text | ResolvedType::Null
1340        ) {
1341            return Err(PlannerError::TypeMismatch {
1342                expected: "Text".to_string(),
1343                found: expr_typed.resolved_type.type_name().to_string(),
1344                line: expr.span.start.line,
1345                column: expr.span.start.column,
1346            });
1347        }
1348
1349        if !matches!(
1350            pattern_typed.resolved_type,
1351            ResolvedType::Text | ResolvedType::Null
1352        ) {
1353            return Err(PlannerError::TypeMismatch {
1354                expected: "Text".to_string(),
1355                found: pattern_typed.resolved_type.type_name().to_string(),
1356                line: pattern.span.start.line,
1357                column: pattern.span.start.column,
1358            });
1359        }
1360
1361        let escape_typed = if let Some(esc) = escape {
1362            let typed = self.infer_type_with_scope(esc, scope, plan_subquery)?;
1363            if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
1364                return Err(PlannerError::TypeMismatch {
1365                    expected: "Text".to_string(),
1366                    found: typed.resolved_type.type_name().to_string(),
1367                    line: esc.span.start.line,
1368                    column: esc.span.start.column,
1369                });
1370            }
1371            Some(Box::new(typed))
1372        } else {
1373            None
1374        };
1375
1376        Ok(TypedExpr {
1377            kind: TypedExprKind::Like {
1378                expr: Box::new(expr_typed),
1379                pattern: Box::new(pattern_typed),
1380                escape: escape_typed,
1381                negated,
1382                kind,
1383            },
1384            resolved_type: ResolvedType::Boolean,
1385            span,
1386        })
1387    }
1388
1389    /// Infer the type of an IN list expression.
1390    #[allow(dead_code)]
1391    fn infer_in_list_type(
1392        &self,
1393        expr: &Expr,
1394        list: &[Expr],
1395        negated: bool,
1396        table: &TableMetadata,
1397        span: Span,
1398    ) -> Result<TypedExpr, PlannerError> {
1399        let expr_typed = self.infer_type(expr, table)?;
1400
1401        let typed_list: Vec<TypedExpr> = list
1402            .iter()
1403            .map(|item| {
1404                let typed = self.infer_type(item, table)?;
1405                // Check each item is compatible with the expression
1406                self.check_comparison_op(
1407                    &expr_typed.resolved_type,
1408                    &typed.resolved_type,
1409                    item.span,
1410                )?;
1411                Ok(typed)
1412            })
1413            .collect::<Result<Vec<_>, PlannerError>>()?;
1414
1415        Ok(TypedExpr {
1416            kind: TypedExprKind::InList {
1417                expr: Box::new(expr_typed),
1418                list: typed_list,
1419                negated,
1420            },
1421            resolved_type: ResolvedType::Boolean,
1422            span,
1423        })
1424    }
1425
1426    fn infer_in_list_type_with_scope(
1427        &self,
1428        expr: &Expr,
1429        list: &[Expr],
1430        negated: bool,
1431        scope: &[ScopedTable],
1432        plan_subquery: &SubqueryPlanner<'_>,
1433        span: Span,
1434    ) -> Result<TypedExpr, PlannerError> {
1435        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1436
1437        let typed_list: Vec<TypedExpr> = list
1438            .iter()
1439            .map(|item| {
1440                let typed = self.infer_type_with_scope(item, scope, plan_subquery)?;
1441                self.check_comparison_op(
1442                    &expr_typed.resolved_type,
1443                    &typed.resolved_type,
1444                    item.span,
1445                )?;
1446                Ok(typed)
1447            })
1448            .collect::<Result<Vec<_>, PlannerError>>()?;
1449
1450        Ok(TypedExpr {
1451            kind: TypedExprKind::InList {
1452                expr: Box::new(expr_typed),
1453                list: typed_list,
1454                negated,
1455            },
1456            resolved_type: ResolvedType::Boolean,
1457            span,
1458        })
1459    }
1460
1461    /// Infer the type of an IS NULL expression.
1462    #[allow(dead_code)]
1463    fn infer_is_null_type(
1464        &self,
1465        expr: &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        Ok(TypedExpr {
1473            kind: TypedExprKind::IsNull {
1474                expr: Box::new(expr_typed),
1475                negated,
1476            },
1477            resolved_type: ResolvedType::Boolean,
1478            span,
1479        })
1480    }
1481
1482    fn infer_is_null_type_with_scope(
1483        &self,
1484        expr: &Expr,
1485        negated: bool,
1486        scope: &[ScopedTable],
1487        plan_subquery: &SubqueryPlanner<'_>,
1488        span: Span,
1489    ) -> Result<TypedExpr, PlannerError> {
1490        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1491
1492        Ok(TypedExpr {
1493            kind: TypedExprKind::IsNull {
1494                expr: Box::new(expr_typed),
1495                negated,
1496            },
1497            resolved_type: ResolvedType::Boolean,
1498            span,
1499        })
1500    }
1501
1502    /// Infer the type of a vector literal.
1503    fn infer_vector_literal_type(
1504        &self,
1505        values: &[f64],
1506        span: Span,
1507    ) -> Result<TypedExpr, PlannerError> {
1508        Ok(TypedExpr {
1509            kind: TypedExprKind::VectorLiteral(values.to_vec()),
1510            resolved_type: ResolvedType::Vector {
1511                dimension: values.len() as u32,
1512                metric: VectorMetric::Cosine, // Default metric for literals
1513            },
1514            span,
1515        })
1516    }
1517
1518    /// Normalize a metric string to VectorMetric enum (case-insensitive).
1519    ///
1520    /// # Valid Values
1521    ///
1522    /// - "cosine" (case-insensitive) → `VectorMetric::Cosine`
1523    /// - "l2" (case-insensitive) → `VectorMetric::L2`
1524    /// - "inner" (case-insensitive) → `VectorMetric::Inner`
1525    ///
1526    /// # Errors
1527    ///
1528    /// Returns `PlannerError::InvalidMetric` if the value is not recognized.
1529    pub fn normalize_metric(&self, metric: &str, span: Span) -> Result<VectorMetric, PlannerError> {
1530        match metric.to_lowercase().as_str() {
1531            "cosine" => Ok(VectorMetric::Cosine),
1532            "l2" => Ok(VectorMetric::L2),
1533            "inner" => Ok(VectorMetric::Inner),
1534            _ => Err(PlannerError::InvalidMetric {
1535                value: metric.to_string(),
1536                line: span.start.line,
1537                column: span.start.column,
1538            }),
1539        }
1540    }
1541
1542    /// Check function call and return the result type.
1543    ///
1544    /// Validates that the function arguments have correct types and returns
1545    /// the result type.
1546    pub fn check_function_call(
1547        &self,
1548        name: &str,
1549        args: &[TypedExpr],
1550        distinct: bool,
1551        star: bool,
1552        span: Span,
1553    ) -> Result<ResolvedType, PlannerError> {
1554        let lower_name = name.to_ascii_lowercase();
1555
1556        match lower_name.as_str() {
1557            "count" => self.check_count(args, distinct, star, span),
1558            "sum" => self.check_sum(args, distinct, star, span),
1559            "total" => self.check_total(args, distinct, star, span),
1560            "avg" => self.check_avg(args, distinct, star, span),
1561            "min" => self.check_min_max(args, distinct, star, span),
1562            "max" => self.check_min_max(args, distinct, star, span),
1563            "group_concat" => self.check_group_concat(args, distinct, star, span),
1564            "string_agg" => self.check_string_agg(args, distinct, star, span),
1565            _ => {
1566                let Some(signature) = crate::scalar::signature(&lower_name) else {
1567                    return Err(PlannerError::unsupported_feature(
1568                        format!("function '{name}'"),
1569                        "future",
1570                        span,
1571                    ));
1572                };
1573                if distinct || star {
1574                    return Err(PlannerError::invalid_expression(format!(
1575                        "scalar function '{name}' does not support DISTINCT or *"
1576                    )));
1577                }
1578                signature.arity.validate(name, args.len(), span)?;
1579                (signature.check)(args)?;
1580                let types: Vec<_> = args.iter().map(|arg| arg.resolved_type.clone()).collect();
1581                match &signature.ret {
1582                    crate::scalar::ReturnRule::Fixed(ty) => Ok(ty.clone()),
1583                    crate::scalar::ReturnRule::FromArgs(rule) => rule(&types),
1584                }
1585            }
1586        }
1587    }
1588
1589    pub fn validate_having_expr(
1590        &self,
1591        expr: &TypedExpr,
1592        group_keys: &[TypedExpr],
1593        aggregates: &[AggregateExpr],
1594    ) -> Result<(), PlannerError> {
1595        use std::collections::HashSet;
1596
1597        let group_key_indices: HashSet<usize> = group_keys
1598            .iter()
1599            .filter_map(|expr| match &expr.kind {
1600                TypedExprKind::ColumnRef { column_index, .. } => Some(*column_index),
1601                _ => None,
1602            })
1603            .collect();
1604
1605        let aggregate_signatures: HashSet<AggregateSignature> = aggregates
1606            .iter()
1607            .map(aggregate_signature_from_expr)
1608            .collect();
1609
1610        fn walk(
1611            expr: &TypedExpr,
1612            group_key_indices: &HashSet<usize>,
1613            aggregate_signatures: &HashSet<AggregateSignature>,
1614        ) -> Result<(), PlannerError> {
1615            match &expr.kind {
1616                TypedExprKind::ColumnRef { column_index, .. } => {
1617                    if group_key_indices.contains(column_index) {
1618                        Ok(())
1619                    } else {
1620                        Err(PlannerError::invalid_expression(
1621                            "column in HAVING must be in GROUP BY or be aggregated".to_string(),
1622                        ))
1623                    }
1624                }
1625                TypedExprKind::FunctionCall {
1626                    name,
1627                    args,
1628                    distinct,
1629                    star,
1630                    over: _,
1631                } if is_aggregate_name(name) => {
1632                    let signature = aggregate_signature_from_call(name, args, *distinct, *star)?;
1633                    if aggregate_signatures.contains(&signature) {
1634                        Ok(())
1635                    } else {
1636                        Err(PlannerError::invalid_expression(
1637                            "aggregate in HAVING must appear in plan".to_string(),
1638                        ))
1639                    }
1640                }
1641                TypedExprKind::BinaryOp { left, right, .. } => {
1642                    walk(left, group_key_indices, aggregate_signatures)?;
1643                    walk(right, group_key_indices, aggregate_signatures)
1644                }
1645                TypedExprKind::UnaryOp { operand, .. } => {
1646                    walk(operand, group_key_indices, aggregate_signatures)
1647                }
1648                TypedExprKind::Case {
1649                    operand,
1650                    branches,
1651                    else_expr,
1652                } => {
1653                    if let Some(operand) = operand {
1654                        walk(operand, group_key_indices, aggregate_signatures)?;
1655                    }
1656                    for branch in branches {
1657                        walk(&branch.when, group_key_indices, aggregate_signatures)?;
1658                        walk(&branch.then, group_key_indices, aggregate_signatures)?;
1659                    }
1660                    if let Some(else_expr) = else_expr {
1661                        walk(else_expr, group_key_indices, aggregate_signatures)?;
1662                    }
1663                    Ok(())
1664                }
1665                TypedExprKind::FunctionCall { args, .. } => {
1666                    for arg in args {
1667                        walk(arg, group_key_indices, aggregate_signatures)?;
1668                    }
1669                    Ok(())
1670                }
1671                TypedExprKind::Between {
1672                    expr, low, high, ..
1673                } => {
1674                    walk(expr, group_key_indices, aggregate_signatures)?;
1675                    walk(low, group_key_indices, aggregate_signatures)?;
1676                    walk(high, group_key_indices, aggregate_signatures)
1677                }
1678                TypedExprKind::Like {
1679                    expr,
1680                    pattern,
1681                    escape,
1682                    ..
1683                } => {
1684                    walk(expr, group_key_indices, aggregate_signatures)?;
1685                    walk(pattern, group_key_indices, aggregate_signatures)?;
1686                    if let Some(esc) = escape {
1687                        walk(esc, group_key_indices, aggregate_signatures)?;
1688                    }
1689                    Ok(())
1690                }
1691                TypedExprKind::InList { expr, list, .. } => {
1692                    walk(expr, group_key_indices, aggregate_signatures)?;
1693                    for item in list {
1694                        walk(item, group_key_indices, aggregate_signatures)?;
1695                    }
1696                    Ok(())
1697                }
1698                TypedExprKind::IsNull { expr, .. } => {
1699                    walk(expr, group_key_indices, aggregate_signatures)
1700                }
1701                _ => Ok(()),
1702            }
1703        }
1704
1705        walk(expr, &group_key_indices, &aggregate_signatures)
1706    }
1707
1708    fn check_count(
1709        &self,
1710        args: &[TypedExpr],
1711        distinct: bool,
1712        star: bool,
1713        span: Span,
1714    ) -> Result<ResolvedType, PlannerError> {
1715        if star {
1716            if distinct {
1717                return Err(PlannerError::unsupported_feature(
1718                    "COUNT(DISTINCT *)",
1719                    "future",
1720                    span,
1721                ));
1722            }
1723            if !args.is_empty() {
1724                return Err(PlannerError::type_mismatch(
1725                    "no arguments with COUNT(*)",
1726                    format!("{} arguments", args.len()),
1727                    span,
1728                ));
1729            }
1730            return Ok(ResolvedType::BigInt);
1731        }
1732
1733        if args.len() != 1 {
1734            return Err(PlannerError::type_mismatch(
1735                "1 argument",
1736                format!("{} arguments", args.len()),
1737                span,
1738            ));
1739        }
1740
1741        if distinct {
1742            return Ok(ResolvedType::BigInt);
1743        }
1744
1745        Ok(ResolvedType::BigInt)
1746    }
1747
1748    fn check_sum(
1749        &self,
1750        args: &[TypedExpr],
1751        _distinct: bool,
1752        star: bool,
1753        span: Span,
1754    ) -> Result<ResolvedType, PlannerError> {
1755        if star {
1756            return Err(PlannerError::type_mismatch(
1757                "numeric argument",
1758                "COUNT(*) style",
1759                span,
1760            ));
1761        }
1762        let arg = self.require_single_arg(args, span)?;
1763        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
1764            return Err(PlannerError::type_mismatch(
1765                "numeric",
1766                arg.resolved_type.type_name().to_string(),
1767                arg.span,
1768            ));
1769        }
1770        Ok(crate::planner::aggregate_expr::sum_result_type(
1771            &arg.resolved_type,
1772        ))
1773    }
1774
1775    fn check_total(
1776        &self,
1777        args: &[TypedExpr],
1778        distinct: bool,
1779        star: bool,
1780        span: Span,
1781    ) -> Result<ResolvedType, PlannerError> {
1782        if star {
1783            return Err(PlannerError::type_mismatch(
1784                "numeric argument",
1785                "COUNT(*) style",
1786                span,
1787            ));
1788        }
1789        if distinct {
1790            return Err(PlannerError::unsupported_feature(
1791                "TOTAL(DISTINCT ...)",
1792                "future",
1793                span,
1794            ));
1795        }
1796        let arg = self.require_single_arg(args, span)?;
1797        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
1798            return Err(PlannerError::type_mismatch(
1799                "numeric",
1800                arg.resolved_type.type_name().to_string(),
1801                arg.span,
1802            ));
1803        }
1804        Ok(ResolvedType::Double)
1805    }
1806
1807    fn check_avg(
1808        &self,
1809        args: &[TypedExpr],
1810        _distinct: bool,
1811        star: bool,
1812        span: Span,
1813    ) -> Result<ResolvedType, PlannerError> {
1814        if star {
1815            return Err(PlannerError::type_mismatch(
1816                "numeric argument",
1817                "COUNT(*) style",
1818                span,
1819            ));
1820        }
1821        let arg = self.require_single_arg(args, span)?;
1822        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
1823            return Err(PlannerError::type_mismatch(
1824                "numeric",
1825                arg.resolved_type.type_name().to_string(),
1826                arg.span,
1827            ));
1828        }
1829        Ok(ResolvedType::Double)
1830    }
1831
1832    fn check_min_max(
1833        &self,
1834        args: &[TypedExpr],
1835        _distinct: bool,
1836        star: bool,
1837        span: Span,
1838    ) -> Result<ResolvedType, PlannerError> {
1839        if star {
1840            return Err(PlannerError::type_mismatch(
1841                "argument",
1842                "COUNT(*) style",
1843                span,
1844            ));
1845        }
1846        let arg = self.require_single_arg(args, span)?;
1847        if matches!(arg.resolved_type, ResolvedType::Vector { .. }) {
1848            return Err(PlannerError::type_mismatch(
1849                "comparable",
1850                arg.resolved_type.type_name().to_string(),
1851                arg.span,
1852            ));
1853        }
1854        Ok(arg.resolved_type.clone())
1855    }
1856
1857    fn check_group_concat(
1858        &self,
1859        args: &[TypedExpr],
1860        _distinct: bool,
1861        star: bool,
1862        span: Span,
1863    ) -> Result<ResolvedType, PlannerError> {
1864        if star {
1865            return Err(PlannerError::type_mismatch(
1866                "text argument",
1867                "COUNT(*) style",
1868                span,
1869            ));
1870        }
1871        if args.is_empty() || args.len() > 2 {
1872            return Err(PlannerError::type_mismatch(
1873                "1 or 2 arguments",
1874                format!("{} arguments", args.len()),
1875                span,
1876            ));
1877        }
1878        if !matches!(
1879            args[0].resolved_type,
1880            ResolvedType::Text | ResolvedType::Null
1881        ) {
1882            return Err(PlannerError::type_mismatch(
1883                "Text",
1884                args[0].resolved_type.type_name().to_string(),
1885                args[0].span,
1886            ));
1887        }
1888        if args.len() == 2
1889            && !matches!(
1890                args[1].resolved_type,
1891                ResolvedType::Text | ResolvedType::Null
1892            )
1893        {
1894            return Err(PlannerError::type_mismatch(
1895                "Text",
1896                args[1].resolved_type.type_name().to_string(),
1897                args[1].span,
1898            ));
1899        }
1900        Ok(ResolvedType::Text)
1901    }
1902
1903    fn check_string_agg(
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                "text argument",
1913                "COUNT(*) style",
1914                span,
1915            ));
1916        }
1917        if args.len() != 2 {
1918            return Err(PlannerError::type_mismatch(
1919                "2 arguments",
1920                format!("{} arguments", args.len()),
1921                span,
1922            ));
1923        }
1924        if !matches!(
1925            args[0].resolved_type,
1926            ResolvedType::Text | ResolvedType::Null
1927        ) {
1928            return Err(PlannerError::type_mismatch(
1929                "Text",
1930                args[0].resolved_type.type_name().to_string(),
1931                args[0].span,
1932            ));
1933        }
1934        if !matches!(
1935            args[1].resolved_type,
1936            ResolvedType::Text | ResolvedType::Null
1937        ) {
1938            return Err(PlannerError::type_mismatch(
1939                "Text",
1940                args[1].resolved_type.type_name().to_string(),
1941                args[1].span,
1942            ));
1943        }
1944        Ok(ResolvedType::Text)
1945    }
1946
1947    fn require_single_arg<'b>(
1948        &self,
1949        args: &'b [TypedExpr],
1950        span: Span,
1951    ) -> Result<&'b TypedExpr, PlannerError> {
1952        if args.len() != 1 {
1953            return Err(PlannerError::type_mismatch(
1954                "1 argument",
1955                format!("{} arguments", args.len()),
1956                span,
1957            ));
1958        }
1959        Ok(&args[0])
1960    }
1961
1962    /// Check vector_distance function arguments.
1963    ///
1964    /// Signature: `vector_distance(column: Vector, vector: Vector, metric: Text) -> Double`
1965    ///
1966    /// # Requirements
1967    ///
1968    /// - First argument must be a Vector type (column reference)
1969    /// - Second argument must be a Vector type (vector literal)
1970    /// - Third argument must be a Text type (metric string)
1971    /// - Vector dimensions must match
1972    pub fn check_vector_distance(
1973        &self,
1974        args: &[TypedExpr],
1975        span: Span,
1976    ) -> Result<ResolvedType, PlannerError> {
1977        if args.len() != 3 {
1978            return Err(PlannerError::TypeMismatch {
1979                expected: "3 arguments".to_string(),
1980                found: format!("{} arguments", args.len()),
1981                line: span.start.line,
1982                column: span.start.column,
1983            });
1984        }
1985
1986        // First argument: Vector column
1987        let col_dim = match &args[0].resolved_type {
1988            ResolvedType::Vector { dimension, .. } => *dimension,
1989            other => {
1990                return Err(PlannerError::TypeMismatch {
1991                    expected: "Vector".to_string(),
1992                    found: other.type_name().to_string(),
1993                    line: args[0].span.start.line,
1994                    column: args[0].span.start.column,
1995                });
1996            }
1997        };
1998
1999        // Second argument: Vector literal
2000        let vec_dim = match &args[1].resolved_type {
2001            ResolvedType::Vector { dimension, .. } => *dimension,
2002            other => {
2003                return Err(PlannerError::TypeMismatch {
2004                    expected: "Vector".to_string(),
2005                    found: other.type_name().to_string(),
2006                    line: args[1].span.start.line,
2007                    column: args[1].span.start.column,
2008                });
2009            }
2010        };
2011
2012        // Check dimension match
2013        self.check_vector_dimension(col_dim, vec_dim, args[1].span)?;
2014
2015        // Third argument: Metric string
2016        match &args[2].resolved_type {
2017            ResolvedType::Text => {
2018                // Validate metric value if it's a literal
2019                if let TypedExprKind::Literal(Literal::String(s)) = &args[2].kind {
2020                    self.normalize_metric(s, args[2].span)?;
2021                }
2022            }
2023            ResolvedType::Null => {
2024                // NULL metric is not allowed
2025                return Err(PlannerError::TypeMismatch {
2026                    expected: "Text (metric)".to_string(),
2027                    found: "Null".to_string(),
2028                    line: args[2].span.start.line,
2029                    column: args[2].span.start.column,
2030                });
2031            }
2032            other => {
2033                return Err(PlannerError::TypeMismatch {
2034                    expected: "Text (metric)".to_string(),
2035                    found: other.type_name().to_string(),
2036                    line: args[2].span.start.line,
2037                    column: args[2].span.start.column,
2038                });
2039            }
2040        }
2041
2042        Ok(ResolvedType::Double)
2043    }
2044
2045    /// Check vector_similarity function arguments.
2046    ///
2047    /// Signature: `vector_similarity(column: Vector, vector: Vector, metric: Text) -> Double`
2048    ///
2049    /// Same validation rules as vector_distance.
2050    pub fn check_vector_similarity(
2051        &self,
2052        args: &[TypedExpr],
2053        span: Span,
2054    ) -> Result<ResolvedType, PlannerError> {
2055        // Same validation as vector_distance
2056        self.check_vector_distance(args, span)
2057    }
2058
2059    /// Check that two vector dimensions match.
2060    ///
2061    /// # Errors
2062    ///
2063    /// Returns `PlannerError::VectorDimensionMismatch` if dimensions don't match.
2064    pub fn check_vector_dimension(
2065        &self,
2066        expected: u32,
2067        found: u32,
2068        span: Span,
2069    ) -> Result<(), PlannerError> {
2070        if expected != found {
2071            Err(PlannerError::VectorDimensionMismatch {
2072                expected,
2073                found,
2074                line: span.start.line,
2075                column: span.start.column,
2076            })
2077        } else {
2078            Ok(())
2079        }
2080    }
2081
2082    // ============================================================
2083    // INSERT/UPDATE Type Checking Methods (Task 13)
2084    // ============================================================
2085
2086    /// Check INSERT values against table columns.
2087    ///
2088    /// Validates that:
2089    /// - The number of values matches the number of columns
2090    /// - Each value's type is compatible with the column type
2091    /// - NOT NULL constraints are satisfied
2092    /// - Vector dimensions match for vector columns
2093    ///
2094    /// # Column Order
2095    ///
2096    /// If `columns` is empty, uses `TableMetadata.column_names()` order (definition order).
2097    ///
2098    /// # Errors
2099    ///
2100    /// - `ColumnValueCountMismatch`: Number of values doesn't match columns
2101    /// - `TypeMismatch`: Value type incompatible with column type
2102    /// - `NullConstraintViolation`: NULL value for NOT NULL column
2103    /// - `VectorDimensionMismatch`: Vector dimension mismatch
2104    pub fn check_insert_values(
2105        &self,
2106        table: &TableMetadata,
2107        columns: &[String],
2108        values: &[Vec<Expr>],
2109        span: Span,
2110    ) -> Result<Vec<Vec<TypedExpr>>, PlannerError> {
2111        // Determine the target columns
2112        let target_columns: Vec<&str> = if columns.is_empty() {
2113            table.column_names()
2114        } else {
2115            columns.iter().map(|s| s.as_str()).collect()
2116        };
2117
2118        let mut typed_rows = Vec::with_capacity(values.len());
2119
2120        for row in values {
2121            // Check value count matches column count
2122            if row.len() != target_columns.len() {
2123                return Err(PlannerError::ColumnValueCountMismatch {
2124                    columns: target_columns.len(),
2125                    values: row.len(),
2126                    line: span.start.line,
2127                    column: span.start.column,
2128                });
2129            }
2130
2131            let mut typed_values = Vec::with_capacity(row.len());
2132
2133            for (value, col_name) in row.iter().zip(target_columns.iter()) {
2134                // Get column metadata
2135                let col_meta =
2136                    table
2137                        .get_column(col_name)
2138                        .ok_or_else(|| PlannerError::ColumnNotFound {
2139                            column: col_name.to_string(),
2140                            table: table.name.clone(),
2141                            line: span.start.line,
2142                            col: span.start.column,
2143                        })?;
2144
2145                // Type-check the value expression
2146                let typed_value = self.infer_type(value, table)?;
2147
2148                // Check NOT NULL constraint
2149                self.check_null_constraint(col_meta, &typed_value, value.span)?;
2150
2151                // Check type compatibility
2152                self.check_type_compatibility(
2153                    &col_meta.data_type,
2154                    &typed_value.resolved_type,
2155                    value.span,
2156                )?;
2157
2158                let typed_value =
2159                    self.coerce_column_value(&col_meta.data_type, typed_value, value.span);
2160
2161                // For vector types, also check dimension
2162                if let (
2163                    ResolvedType::Vector {
2164                        dimension: expected_dim,
2165                        ..
2166                    },
2167                    ResolvedType::Vector {
2168                        dimension: actual_dim,
2169                        ..
2170                    },
2171                ) = (&col_meta.data_type, &typed_value.resolved_type)
2172                {
2173                    self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
2174                }
2175
2176                typed_values.push(typed_value);
2177            }
2178
2179            typed_rows.push(typed_values);
2180        }
2181
2182        Ok(typed_rows)
2183    }
2184
2185    /// Check UPDATE assignment type compatibility.
2186    ///
2187    /// Validates that the value's type is compatible with the column type.
2188    ///
2189    /// # Errors
2190    ///
2191    /// - `ColumnNotFound`: Column doesn't exist
2192    /// - `TypeMismatch`: Value type incompatible with column type
2193    /// - `NullConstraintViolation`: NULL value for NOT NULL column
2194    /// - `VectorDimensionMismatch`: Vector dimension mismatch
2195    pub fn check_assignment(
2196        &self,
2197        table: &TableMetadata,
2198        column: &str,
2199        value: &Expr,
2200        span: Span,
2201    ) -> Result<TypedExpr, PlannerError> {
2202        // Get column metadata
2203        let col_meta = table
2204            .get_column(column)
2205            .ok_or_else(|| PlannerError::ColumnNotFound {
2206                column: column.to_string(),
2207                table: table.name.clone(),
2208                line: span.start.line,
2209                col: span.start.column,
2210            })?;
2211
2212        // Type-check the value expression
2213        let typed_value = self.infer_type(value, table)?;
2214
2215        // Check NOT NULL constraint
2216        self.check_null_constraint(col_meta, &typed_value, value.span)?;
2217
2218        // Check type compatibility
2219        self.check_type_compatibility(&col_meta.data_type, &typed_value.resolved_type, value.span)?;
2220
2221        let typed_value = self.coerce_column_value(&col_meta.data_type, typed_value, value.span);
2222
2223        // For vector types, also check dimension
2224        if let (
2225            ResolvedType::Vector {
2226                dimension: expected_dim,
2227                ..
2228            },
2229            ResolvedType::Vector {
2230                dimension: actual_dim,
2231                ..
2232            },
2233        ) = (&col_meta.data_type, &typed_value.resolved_type)
2234        {
2235            self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
2236        }
2237
2238        Ok(typed_value)
2239    }
2240
2241    /// Check NOT NULL constraint for a value.
2242    ///
2243    /// # Errors
2244    ///
2245    /// Returns `PlannerError::NullConstraintViolation` if the column has NOT NULL
2246    /// constraint and the value is NULL.
2247    pub fn check_null_constraint(
2248        &self,
2249        column: &crate::catalog::ColumnMetadata,
2250        value: &TypedExpr,
2251        span: Span,
2252    ) -> Result<(), PlannerError> {
2253        if column.not_null && matches!(value.resolved_type, ResolvedType::Null) {
2254            Err(PlannerError::NullConstraintViolation {
2255                column: column.name.clone(),
2256                line: span.start.line,
2257                col: span.start.column,
2258            })
2259        } else {
2260            Ok(())
2261        }
2262    }
2263
2264    /// Check type compatibility between expected and actual types.
2265    ///
2266    /// Uses implicit type conversion rules defined in `ResolvedType::can_cast_to`.
2267    ///
2268    /// # Errors
2269    ///
2270    /// Returns `PlannerError::TypeMismatch` if types are incompatible.
2271    fn check_type_compatibility(
2272        &self,
2273        expected: &ResolvedType,
2274        actual: &ResolvedType,
2275        span: Span,
2276    ) -> Result<(), PlannerError> {
2277        // Same type is always compatible
2278        if expected == actual {
2279            return Ok(());
2280        }
2281
2282        // Check if implicit cast is allowed
2283        if actual.can_cast_to(expected) {
2284            return Ok(());
2285        }
2286
2287        // Special case: Vector types with same dimension but different metric are compatible
2288        // (the column's metric is used)
2289        if let (
2290            ResolvedType::Vector {
2291                dimension: d1,
2292                metric: _,
2293            },
2294            ResolvedType::Vector {
2295                dimension: d2,
2296                metric: _,
2297            },
2298        ) = (expected, actual)
2299        {
2300            // Dimensions must match for vector compatibility
2301            if *d1 == *d2 {
2302                return Ok(());
2303            }
2304            // Different dimensions will fall through to TypeMismatch error
2305        }
2306
2307        Err(PlannerError::TypeMismatch {
2308            expected: expected.type_name().to_string(),
2309            found: actual.type_name().to_string(),
2310            line: span.start.line,
2311            column: span.start.column,
2312        })
2313    }
2314
2315    /// Insert an execution-time coercion where a column accepts a value whose
2316    /// source representation differs from its storage representation.
2317    fn coerce_column_value(
2318        &self,
2319        expected: &ResolvedType,
2320        value: TypedExpr,
2321        span: Span,
2322    ) -> TypedExpr {
2323        if value.resolved_type != *expected
2324            && value.resolved_type != ResolvedType::Null
2325            && matches!(
2326                expected,
2327                ResolvedType::Integer
2328                    | ResolvedType::BigInt
2329                    | ResolvedType::Float
2330                    | ResolvedType::Double
2331                    | ResolvedType::Timestamp
2332            )
2333        {
2334            TypedExpr::cast(value, expected.clone(), span)
2335        } else {
2336            value
2337        }
2338    }
2339}
2340
2341fn is_numeric_type(ty: &ResolvedType) -> bool {
2342    matches!(
2343        ty,
2344        ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Float | ResolvedType::Double
2345    )
2346}
2347
2348fn coerce_case_result(expr: &mut TypedExpr, target: &ResolvedType) {
2349    if expr.resolved_type == *target || matches!(expr.resolved_type, ResolvedType::Null) {
2350        return;
2351    }
2352    let span = expr.span;
2353    *expr = TypedExpr::cast(expr.clone(), target.clone(), span);
2354}
2355
2356#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2357struct AggregateSignature {
2358    name: String,
2359    distinct: bool,
2360    star: bool,
2361    arg_key: Option<String>,
2362    separator: Option<String>,
2363}
2364
2365fn is_aggregate_name(name: &str) -> bool {
2366    matches!(
2367        name.to_ascii_lowercase().as_str(),
2368        "count" | "sum" | "total" | "avg" | "min" | "max" | "group_concat" | "string_agg"
2369    )
2370}
2371
2372fn aggregate_signature_from_expr(expr: &AggregateExpr) -> AggregateSignature {
2373    let (name, separator, star, arg) = match &expr.function {
2374        AggregateFunction::Count => (
2375            "count".to_string(),
2376            None,
2377            expr.arg.is_none(),
2378            expr.arg.as_ref(),
2379        ),
2380        AggregateFunction::Sum => ("sum".to_string(), None, false, expr.arg.as_ref()),
2381        AggregateFunction::Total => ("total".to_string(), None, false, expr.arg.as_ref()),
2382        AggregateFunction::Avg => ("avg".to_string(), None, false, expr.arg.as_ref()),
2383        AggregateFunction::Min => ("min".to_string(), None, false, expr.arg.as_ref()),
2384        AggregateFunction::Max => ("max".to_string(), None, false, expr.arg.as_ref()),
2385        AggregateFunction::GroupConcat { separator } => (
2386            "group_concat".to_string(),
2387            separator.clone(),
2388            false,
2389            expr.arg.as_ref(),
2390        ),
2391        AggregateFunction::StringAgg { separator } => (
2392            "string_agg".to_string(),
2393            separator.clone(),
2394            false,
2395            expr.arg.as_ref(),
2396        ),
2397    };
2398    AggregateSignature {
2399        name,
2400        distinct: expr.distinct,
2401        star,
2402        arg_key: arg.map(typed_expr_signature),
2403        separator,
2404    }
2405}
2406
2407fn aggregate_signature_from_call(
2408    name: &str,
2409    args: &[TypedExpr],
2410    distinct: bool,
2411    star: bool,
2412) -> Result<AggregateSignature, PlannerError> {
2413    let separator = if name.eq_ignore_ascii_case("group_concat") && args.len() == 2 {
2414        if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
2415            Some(value.clone())
2416        } else {
2417            return Err(PlannerError::invalid_expression(
2418                "GROUP_CONCAT separator must be a string literal".to_string(),
2419            ));
2420        }
2421    } else if name.eq_ignore_ascii_case("string_agg") && args.len() == 2 {
2422        if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
2423            Some(value.clone())
2424        } else {
2425            return Err(PlannerError::invalid_expression(
2426                "STRING_AGG separator must be a string literal".to_string(),
2427            ));
2428        }
2429    } else {
2430        None
2431    };
2432    Ok(AggregateSignature {
2433        name: name.to_ascii_lowercase(),
2434        distinct,
2435        star,
2436        arg_key: args.first().map(typed_expr_signature),
2437        separator,
2438    })
2439}
2440
2441fn typed_expr_signature(expr: &TypedExpr) -> String {
2442    format!("{:?}", expr.kind)
2443}
2444
2445fn single_column_type(schema: &[ColumnMetadata], span: Span) -> Result<ResolvedType, PlannerError> {
2446    match schema {
2447        [column] => Ok(column.data_type.clone()),
2448        [] => Err(PlannerError::type_mismatch(
2449            "one-column subquery",
2450            "zero-column subquery",
2451            span,
2452        )),
2453        _ => Err(PlannerError::type_mismatch(
2454            "one-column subquery",
2455            format!("{} columns", schema.len()),
2456            span,
2457        )),
2458    }
2459}
2460
2461// Tests are in type_checker/tests.rs
2462#[cfg(test)]
2463#[path = "type_checker/tests.rs"]
2464mod tests;