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