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