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, TruthValue,
12    UnaryOp, WindowFrame, WindowFrameBound, WindowFrameUnits, WindowSpec,
13};
14use crate::ast::expr::{
15    INTERNAL_ROW_BETWEEN, INTERNAL_ROW_DISTINCT, INTERNAL_ROW_EQ, INTERNAL_ROW_GT,
16    INTERNAL_ROW_GTEQ, INTERNAL_ROW_IN, INTERNAL_ROW_LT, INTERNAL_ROW_LTEQ, INTERNAL_ROW_NEQ,
17    INTERNAL_TRUTH_FALSE, INTERNAL_TRUTH_TRUE, INTERNAL_TRUTH_UNKNOWN,
18};
19use crate::catalog::{Catalog, ColumnMetadata, TableMetadata};
20use crate::planner::aggregate_expr::{AggregateExpr, AggregateFunction};
21use crate::planner::error::PlannerError;
22use crate::planner::logical_plan::LogicalPlan;
23use crate::planner::typed_expr::{
24    Quantifier, SortExpr, TypedCaseWhen, TypedExpr, TypedExprKind, TypedWindowSpec,
25};
26use crate::planner::types::ResolvedType;
27use std::collections::{BTreeSet, HashMap, HashSet};
28use std::sync::Arc;
29
30/// A table visible to expression name resolution.
31///
32/// The metadata is shared rather than owned: every enclosing scope is copied
33/// into each nested scope, and copying whole schemas there made resolution cost
34/// grow with the square of the nesting depth.
35#[derive(Debug, Clone)]
36pub struct ScopedTable {
37    pub table: Arc<TableMetadata>,
38    pub start_index: usize,
39    /// Lexical nesting level; zero is the current SELECT and larger values
40    /// are successively enclosing SELECT scopes.
41    pub scope_level: usize,
42    /// Columns coalesced by a JOIN ... USING or NATURAL JOIN. They remain
43    /// addressable by a qualified right-hand reference, but are not candidates
44    /// for an unqualified reference because the merged output column owns
45    /// the name.
46    pub hidden_unqualified_columns: HashSet<String>,
47    /// For a column merged by USING or NATURAL, the output indexes of every
48    /// other side. An unqualified reference to a merged name resolves to
49    /// `COALESCE(left, right, ...)` so that RIGHT and FULL joins report the key
50    /// from whichever joined input is present.
51    pub merged_column_partners: HashMap<String, Vec<usize>>,
52    /// Column name to position in `table.columns`, built once when the table
53    /// enters scope. Resolution looks a name up once per reference, so scanning
54    /// the column list made a wide projection cost the square of its width.
55    /// Shared alongside the metadata it indexes so the two cannot drift apart.
56    ///
57    /// `None` for narrow tables, where building the map costs more than the
58    /// scans it saves; see [`COLUMN_INDEX_THRESHOLD`].
59    column_index: Option<Arc<HashMap<String, usize>>>,
60}
61
62/// Column count above which a scoped table gets a hash index.
63///
64/// Below this a linear scan of the column list wins: the map allocation is paid
65/// once per table per scope, and measurement showed narrow tables getting 10-15%
66/// slower when every table was indexed unconditionally.
67const COLUMN_INDEX_THRESHOLD: usize = 32;
68
69impl ScopedTable {
70    pub fn new(table: impl Into<Arc<TableMetadata>>, start_index: usize) -> Self {
71        let table = table.into();
72        let column_index = (table.columns.len() > COLUMN_INDEX_THRESHOLD).then(|| {
73            // On a duplicate name the first position wins, matching the linear
74            // scan this replaces.
75            let mut index = HashMap::with_capacity(table.columns.len());
76            for (position, column) in table.columns.iter().enumerate() {
77                index.entry(column.name.clone()).or_insert(position);
78            }
79            Arc::new(index)
80        });
81        Self {
82            table,
83            start_index,
84            scope_level: 0,
85            hidden_unqualified_columns: HashSet::new(),
86            merged_column_partners: HashMap::new(),
87            column_index,
88        }
89    }
90
91    /// Position of `column` in this table, or `None` if it has no such column.
92    pub fn column_position(&self, column: &str) -> Option<usize> {
93        match &self.column_index {
94            Some(index) => index.get(column).copied(),
95            None => self.table.get_column_index(column),
96        }
97    }
98
99    pub fn hide_unqualified_columns(&mut self, columns: &[String]) {
100        self.hidden_unqualified_columns
101            .extend(columns.iter().cloned());
102    }
103
104    /// Record that `column` is merged with the output column at `partner_index`.
105    pub fn merge_column_with(&mut self, column: &str, partner_index: usize) {
106        let partners = self
107            .merged_column_partners
108            .entry(column.to_string())
109            .or_default();
110        if !partners.contains(&partner_index) {
111            partners.push(partner_index);
112        }
113    }
114}
115
116pub type SubqueryPlanner<'p> = dyn Fn(&Statement, &[ScopedTable]) -> Result<(LogicalPlan, Vec<ColumnMetadata>), PlannerError>
117    + 'p;
118
119/// Type checker for SQL expressions.
120///
121/// Performs type inference and validation for expressions, ensuring that
122/// operations are valid for the types involved and that constraints are met.
123///
124/// # Examples
125///
126/// ```
127/// use alopex_sql::catalog::MemoryCatalog;
128/// use alopex_sql::planner::type_checker::TypeChecker;
129///
130/// let catalog = MemoryCatalog::new();
131/// let type_checker = TypeChecker::new(&catalog);
132/// ```
133pub struct TypeChecker<'a, C: Catalog + ?Sized> {
134    catalog: &'a C,
135}
136
137impl<'a, C: Catalog + ?Sized> TypeChecker<'a, C> {
138    /// Create a new TypeChecker with the given catalog.
139    pub fn new(catalog: &'a C) -> Self {
140        Self { catalog }
141    }
142
143    /// Get a reference to the catalog.
144    pub fn catalog(&self) -> &'a C {
145        self.catalog
146    }
147
148    /// Infer the type of an expression within a table context.
149    ///
150    /// Recursively analyzes the expression to determine its type, resolving
151    /// column references against the provided table metadata.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if:
156    /// - A column reference cannot be resolved
157    /// - A binary operation is invalid for the operand types
158    /// - A function call has invalid arguments
159    pub fn infer_type(
160        &self,
161        expr: &Expr,
162        table: &TableMetadata,
163    ) -> Result<TypedExpr, PlannerError> {
164        let scope = [ScopedTable::new(table.clone(), 0)];
165        self.infer_type_with_scope(expr, &scope, &|stmt, _outer| {
166            let planner = crate::planner::Planner::new(self.catalog);
167            let plan = planner.plan(stmt)?;
168            Ok((plan, Vec::new()))
169        })
170    }
171
172    pub fn infer_type_with_scope(
173        &self,
174        expr: &Expr,
175        scope: &[ScopedTable],
176        plan_subquery: &SubqueryPlanner<'_>,
177    ) -> Result<TypedExpr, PlannerError> {
178        let span = expr.span;
179        match &expr.kind {
180            ExprKind::Literal { literal: lit } => self.infer_literal_type(lit, span),
181
182            ExprKind::ColumnRef {
183                table: table_qualifier,
184                column,
185            } => self.infer_column_ref_type_with_scope(
186                scope,
187                table_qualifier.as_deref(),
188                column,
189                span,
190            ),
191
192            ExprKind::BinaryOp { left, op, right } => {
193                self.infer_binary_op_type_with_scope(left, *op, right, scope, plan_subquery, span)
194            }
195
196            ExprKind::UnaryOp { op, operand } => {
197                self.infer_unary_op_type_with_scope(*op, operand, scope, plan_subquery, span)
198            }
199
200            ExprKind::Case {
201                operand,
202                branches,
203                else_expr,
204            } => self.infer_case_type_with_scope(
205                operand.as_deref(),
206                branches,
207                else_expr.as_deref(),
208                scope,
209                plan_subquery,
210                span,
211            ),
212
213            ExprKind::FunctionCall {
214                name,
215                args,
216                distinct,
217                star,
218                order_by,
219                within_group,
220                filter,
221                over,
222            } => self.infer_function_call_type_with_scope(
223                name,
224                args,
225                *distinct,
226                *star,
227                order_by,
228                within_group,
229                filter.as_deref(),
230                over.as_ref(),
231                scope,
232                plan_subquery,
233                span,
234            ),
235
236            ExprKind::Cast { expr, target_type } => {
237                let typed_expr = self.infer_type_with_scope(expr, scope, plan_subquery)?;
238                Ok(TypedExpr::cast(
239                    typed_expr,
240                    ResolvedType::from_ast(target_type),
241                    span,
242                ))
243            }
244
245            ExprKind::TryCast { expr, target_type } => {
246                let typed_expr = self.infer_type_with_scope(expr, scope, plan_subquery)?;
247                Ok(TypedExpr::try_cast(
248                    typed_expr,
249                    ResolvedType::from_ast(target_type),
250                    span,
251                ))
252            }
253
254            ExprKind::Between {
255                expr,
256                low,
257                high,
258                negated,
259            } => self.infer_between_type_with_scope(
260                expr,
261                low,
262                high,
263                *negated,
264                scope,
265                plan_subquery,
266                span,
267            ),
268
269            ExprKind::Like {
270                expr,
271                pattern,
272                escape,
273                negated,
274                kind,
275            } => self.infer_like_type_with_scope(
276                expr,
277                pattern,
278                escape.as_deref(),
279                *negated,
280                *kind,
281                scope,
282                plan_subquery,
283                span,
284            ),
285
286            ExprKind::InList {
287                expr,
288                list,
289                negated,
290            } => {
291                self.infer_in_list_type_with_scope(expr, list, *negated, scope, plan_subquery, span)
292            }
293
294            ExprKind::IsNull { expr, negated } => {
295                self.infer_is_null_type_with_scope(expr, *negated, scope, plan_subquery, span)
296            }
297
298            ExprKind::Row { .. } => Err(PlannerError::unsupported_feature(
299                "standalone row constructor",
300                "v0.8.8 predicate context",
301                span,
302            )),
303
304            ExprKind::TruthPredicate {
305                expr,
306                value,
307                negated,
308            } => self.infer_truth_predicate_with_scope(
309                expr,
310                *value,
311                *negated,
312                scope,
313                plan_subquery,
314                span,
315            ),
316
317            ExprKind::IsDistinctFrom {
318                left,
319                right,
320                negated,
321            } => self.infer_distinct_predicate_with_scope(
322                left,
323                right,
324                *negated,
325                scope,
326                plan_subquery,
327                span,
328            ),
329
330            ExprKind::VectorLiteral { values } => self.infer_vector_literal_type(values, span),
331
332            ExprKind::ScalarSubquery { subquery } => {
333                let (plan, schema) = plan_subquery(subquery, scope)?;
334                let value_type = single_column_type(&schema, span)?;
335                Ok(TypedExpr {
336                    kind: TypedExprKind::ScalarSubquery(Box::new(plan)),
337                    resolved_type: value_type,
338                    span,
339                })
340            }
341            ExprKind::InSubquery {
342                expr,
343                subquery,
344                negated,
345            } => {
346                let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
347                let (plan, schema) = plan_subquery(subquery, scope)?;
348                let value_type = single_column_type(&schema, span)?;
349                self.check_comparison_op(&expr_typed.resolved_type, &value_type, span)?;
350                Ok(TypedExpr {
351                    kind: TypedExprKind::InSubquery {
352                        expr: Box::new(expr_typed),
353                        subquery: Box::new(plan),
354                        negated: *negated,
355                    },
356                    resolved_type: ResolvedType::Boolean,
357                    span,
358                })
359            }
360            ExprKind::Exists { subquery, negated } => {
361                let (plan, _schema) = plan_subquery(subquery, scope)?;
362                Ok(TypedExpr {
363                    kind: TypedExprKind::Exists {
364                        subquery: Box::new(plan),
365                        negated: *negated,
366                    },
367                    resolved_type: ResolvedType::Boolean,
368                    span,
369                })
370            }
371            ExprKind::Quantified {
372                expr,
373                op,
374                quantifier,
375                subquery,
376            } => {
377                let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
378                let (plan, schema) = plan_subquery(subquery, scope)?;
379                let value_type = single_column_type(&schema, span)?;
380                self.check_binary_op(*op, &expr_typed.resolved_type, &value_type, span)?;
381                Ok(TypedExpr {
382                    kind: TypedExprKind::Quantified {
383                        expr: Box::new(expr_typed),
384                        op: *op,
385                        quantifier: match quantifier {
386                            AstQuantifier::Any => Quantifier::Any,
387                            AstQuantifier::All => Quantifier::All,
388                        },
389                        subquery: Box::new(plan),
390                    },
391                    resolved_type: ResolvedType::Boolean,
392                    span,
393                })
394            }
395        }
396    }
397
398    /// Infer the type of a literal value.
399    fn infer_literal_type(&self, lit: &Literal, span: Span) -> Result<TypedExpr, PlannerError> {
400        let (kind, resolved_type) = match lit {
401            Literal::Number(s) => {
402                // Determine if it's integer or floating point
403                let resolved_type = if s.contains('.') || s.contains('e') || s.contains('E') {
404                    ResolvedType::Double
405                } else {
406                    // Check if it fits in i32 or needs i64
407                    if s.parse::<i32>().is_ok() {
408                        ResolvedType::Integer
409                    } else {
410                        ResolvedType::BigInt
411                    }
412                };
413                (TypedExprKind::Literal(lit.clone()), resolved_type)
414            }
415            Literal::String(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Text),
416            Literal::Interval(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Interval),
417            Literal::Boolean(_) => (TypedExprKind::Literal(lit.clone()), ResolvedType::Boolean),
418            Literal::Null => (TypedExprKind::Literal(lit.clone()), ResolvedType::Null),
419        };
420
421        Ok(TypedExpr {
422            kind,
423            resolved_type,
424            span,
425        })
426    }
427
428    /// Infer the type of a column reference.
429    #[allow(dead_code)]
430    fn infer_column_ref_type(
431        &self,
432        table: &TableMetadata,
433        column_name: &str,
434        span: Span,
435    ) -> Result<TypedExpr, PlannerError> {
436        // Find the column in the table
437        let (column_index, column) = table
438            .columns
439            .iter()
440            .enumerate()
441            .find(|(_, c)| c.name == column_name)
442            .ok_or_else(|| PlannerError::ColumnNotFound {
443                column: column_name.to_string(),
444                table: table.name.clone(),
445                line: span.start.line,
446                col: span.start.column,
447            })?;
448
449        Ok(TypedExpr {
450            kind: TypedExprKind::ColumnRef {
451                table: table.name.clone(),
452                column: column_name.to_string(),
453                column_index,
454            },
455            resolved_type: column.data_type.clone(),
456            span,
457        })
458    }
459
460    fn infer_column_ref_type_with_scope(
461        &self,
462        scope: &[ScopedTable],
463        table_qualifier: Option<&str>,
464        column_name: &str,
465        span: Span,
466    ) -> Result<TypedExpr, PlannerError> {
467        let levels = scope
468            .iter()
469            .map(|table| table.scope_level)
470            .collect::<BTreeSet<_>>();
471        let mut qualifier_found = false;
472
473        for level in levels {
474            let candidates = scope
475                .iter()
476                .filter(|table| table.scope_level == level)
477                .filter(|table| {
478                    table_qualifier.is_some()
479                        || !table.hidden_unqualified_columns.contains(column_name)
480                })
481                .collect::<Vec<_>>();
482            if candidates.is_empty() {
483                continue;
484            }
485            if let Some(qualifier) = table_qualifier {
486                let qualified = candidates
487                    .iter()
488                    .filter(|table| table.table.name == qualifier)
489                    .collect::<Vec<_>>();
490                match qualified.len() {
491                    0 => continue,
492                    1 => qualifier_found = true,
493                    _ => {
494                        return Err(PlannerError::ambiguous_column(
495                            column_name,
496                            qualified
497                                .iter()
498                                .map(|table| table.table.name.clone())
499                                .collect(),
500                            span,
501                        ));
502                    }
503                }
504            }
505
506            // Resolution happens through each table's own column index rather
507            // than by scanning its column list, because this runs once per
508            // column reference and the scan made a wide projection quadratic.
509            let mut matches = candidates.iter().filter(|table| {
510                table_qualifier.is_none_or(|qualifier| table.table.name == qualifier)
511                    && table.column_position(column_name).is_some()
512            });
513            let found = matches.next();
514            let second = matches.next();
515
516            match (found, second) {
517                (Some(_), Some(_)) => {
518                    return Err(PlannerError::ambiguous_column(
519                        column_name,
520                        candidates
521                            .iter()
522                            .filter(|table| table.column_position(column_name).is_some())
523                            .map(|table| table.table.name.clone())
524                            .collect(),
525                        span,
526                    ));
527                }
528                (None, _) => {
529                    if table_qualifier.is_some() {
530                        // A qualified name that the named table does not have is
531                        // an error here; it cannot be a correlated reference.
532                        return Err(PlannerError::column_not_found(
533                            column_name,
534                            candidates
535                                .first()
536                                .map(|table| table.table.name.as_str())
537                                .unwrap_or("unknown"),
538                            span,
539                        ));
540                    }
541                    // A missing local name may be a correlated reference. Only
542                    // this case falls back to the next enclosing scope.
543                    continue;
544                }
545                (Some(scoped), None) => {
546                    let column_index = scoped
547                        .column_position(column_name)
548                        .expect("filtered on the column being present");
549                    let column = &scoped.table.columns[column_index];
550                    let own_ref = TypedExpr {
551                        kind: TypedExprKind::ColumnRef {
552                            table: scoped.table.name.clone(),
553                            column: column_name.to_string(),
554                            column_index: scoped.start_index + column_index,
555                        },
556                        resolved_type: column.data_type.clone(),
557                        span,
558                    };
559
560                    // A USING/NATURAL common column is one output column formed
561                    // from both inputs. An unqualified reference must see the
562                    // merged value, otherwise a RIGHT or FULL join reports the
563                    // left side's NULL for rows that only exist on the right.
564                    if table_qualifier.is_none()
565                        && let Some(partner_indices) =
566                            scoped.merged_column_partners.get(column_name)
567                    {
568                        let mut args = Vec::with_capacity(partner_indices.len() + 1);
569                        args.push(own_ref);
570                        args.extend(partner_indices.iter().map(|&partner_index| TypedExpr {
571                            kind: TypedExprKind::ColumnRef {
572                                table: scoped.table.name.clone(),
573                                column: column_name.to_string(),
574                                column_index: partner_index,
575                            },
576                            resolved_type: column.data_type.clone(),
577                            span,
578                        }));
579                        return Ok(TypedExpr {
580                            kind: TypedExprKind::FunctionCall {
581                                name: "coalesce".to_string(),
582                                args,
583                                distinct: false,
584                                star: false,
585                                filter: None,
586                                order_by: Vec::new(),
587                                over: None,
588                            },
589                            resolved_type: column.data_type.clone(),
590                            span,
591                        });
592                    }
593
594                    return Ok(own_ref);
595                }
596            }
597        }
598
599        let table = scope
600            .iter()
601            .min_by_key(|table| table.scope_level)
602            .map(|table| table.table.name.clone())
603            .unwrap_or_else(|| "unknown".to_string());
604        if let Some(qualifier) = table_qualifier
605            && !qualifier_found
606        {
607            return Err(PlannerError::table_not_found(qualifier, span));
608        }
609        Err(PlannerError::column_not_found(column_name, table, span))
610    }
611
612    /// Infer the type of a binary operation.
613    #[allow(dead_code)]
614    fn infer_binary_op_type(
615        &self,
616        left: &Expr,
617        op: BinaryOp,
618        right: &Expr,
619        table: &TableMetadata,
620        span: Span,
621    ) -> Result<TypedExpr, PlannerError> {
622        let left_typed = self.infer_type(left, table)?;
623        let right_typed = self.infer_type(right, table)?;
624
625        let result_type = self.check_binary_op(
626            op,
627            &left_typed.resolved_type,
628            &right_typed.resolved_type,
629            span,
630        )?;
631
632        if let Some(folded) =
633            fold_integral_binary(&left_typed, op, &right_typed, &result_type, span)
634        {
635            return Ok(folded);
636        }
637
638        Ok(TypedExpr {
639            kind: TypedExprKind::BinaryOp {
640                left: Box::new(left_typed),
641                op,
642                right: Box::new(right_typed),
643            },
644            resolved_type: result_type,
645            span,
646        })
647    }
648
649    fn infer_binary_op_type_with_scope(
650        &self,
651        left: &Expr,
652        op: BinaryOp,
653        right: &Expr,
654        scope: &[ScopedTable],
655        plan_subquery: &SubqueryPlanner<'_>,
656        span: Span,
657    ) -> Result<TypedExpr, PlannerError> {
658        if row_items(left).is_some() || row_items(right).is_some() {
659            let internal = match op {
660                BinaryOp::Eq => INTERNAL_ROW_EQ,
661                BinaryOp::Neq => INTERNAL_ROW_NEQ,
662                BinaryOp::Lt => INTERNAL_ROW_LT,
663                BinaryOp::LtEq => INTERNAL_ROW_LTEQ,
664                BinaryOp::Gt => INTERNAL_ROW_GT,
665                BinaryOp::GtEq => INTERNAL_ROW_GTEQ,
666                _ => {
667                    return Err(PlannerError::invalid_operator(
668                        format!("{op:?}"),
669                        "Row",
670                        span,
671                    ));
672                }
673            };
674            let (mut left, right, width) =
675                self.infer_row_pair_with_scope(left, right, scope, plan_subquery, span)?;
676            left.extend(right);
677            return Ok(internal_predicate(
678                format!("{internal}:{width}"),
679                left,
680                span,
681            ));
682        }
683
684        let left_typed = self.infer_type_with_scope(left, scope, plan_subquery)?;
685        let right_typed = self.infer_type_with_scope(right, scope, plan_subquery)?;
686
687        let result_type = self.check_binary_op(
688            op,
689            &left_typed.resolved_type,
690            &right_typed.resolved_type,
691            span,
692        )?;
693
694        if let Some(folded) =
695            fold_integral_binary(&left_typed, op, &right_typed, &result_type, span)
696        {
697            return Ok(folded);
698        }
699
700        Ok(TypedExpr {
701            kind: TypedExprKind::BinaryOp {
702                left: Box::new(left_typed),
703                op,
704                right: Box::new(right_typed),
705            },
706            resolved_type: result_type,
707            span,
708        })
709    }
710
711    fn infer_case_type_with_scope(
712        &self,
713        operand: Option<&Expr>,
714        branches: &[crate::ast::expr::CaseWhen],
715        else_expr: Option<&Expr>,
716        scope: &[ScopedTable],
717        plan_subquery: &SubqueryPlanner<'_>,
718        span: Span,
719    ) -> Result<TypedExpr, PlannerError> {
720        if branches.is_empty() {
721            return Err(PlannerError::invalid_expression(
722                "CASE expression requires at least one WHEN branch",
723            ));
724        }
725        let typed_operand = operand
726            .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
727            .transpose()?;
728        let mut typed_branches = Vec::with_capacity(branches.len());
729        let mut result_type = ResolvedType::Null;
730
731        for branch in branches {
732            let condition = self.infer_type_with_scope(&branch.when, scope, plan_subquery)?;
733            if let Some(operand) = &typed_operand {
734                self.check_comparison_op(
735                    &operand.resolved_type,
736                    &condition.resolved_type,
737                    condition.span,
738                )?;
739            } else if !matches!(
740                condition.resolved_type,
741                ResolvedType::Boolean | ResolvedType::Null
742            ) {
743                return Err(PlannerError::type_mismatch(
744                    "Boolean",
745                    condition.resolved_type.type_name(),
746                    condition.span,
747                ));
748            }
749
750            let result = self.infer_type_with_scope(&branch.then, scope, plan_subquery)?;
751            result_type =
752                self.common_case_result_type(&result_type, &result.resolved_type, result.span)?;
753            typed_branches.push(TypedCaseWhen {
754                when: condition,
755                then: result,
756            });
757        }
758
759        let mut typed_else = else_expr
760            .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
761            .transpose()?;
762        if let Some(else_expr) = &typed_else {
763            result_type = self.common_case_result_type(
764                &result_type,
765                &else_expr.resolved_type,
766                else_expr.span,
767            )?;
768        }
769
770        for branch in &mut typed_branches {
771            coerce_case_result(&mut branch.then, &result_type);
772        }
773        if let Some(else_expr) = &mut typed_else {
774            coerce_case_result(else_expr, &result_type);
775        }
776
777        Ok(TypedExpr {
778            kind: TypedExprKind::Case {
779                operand: typed_operand.map(Box::new),
780                branches: typed_branches,
781                else_expr: typed_else.map(Box::new),
782            },
783            resolved_type: result_type,
784            span,
785        })
786    }
787
788    fn common_case_result_type(
789        &self,
790        current: &ResolvedType,
791        next: &ResolvedType,
792        span: Span,
793    ) -> Result<ResolvedType, PlannerError> {
794        if matches!(current, ResolvedType::Null) {
795            return Ok(next.clone());
796        }
797        if matches!(next, ResolvedType::Null) || current == next {
798            return Ok(current.clone());
799        }
800        if is_numeric_type(current) && is_numeric_type(next) {
801            return self.check_arithmetic_op(current, next, span);
802        }
803        Err(PlannerError::type_mismatch(
804            current.type_name(),
805            next.type_name(),
806            span,
807        ))
808    }
809
810    /// Check binary operation and return the result type.
811    ///
812    /// Validates that the operator is valid for the given operand types
813    /// and returns the result type.
814    ///
815    /// # Type Rules
816    ///
817    /// - Arithmetic operators (+, -, *, /, %): Require numeric operands
818    /// - Comparison operators (=, <>, <, >, <=, >=): Require compatible types
819    /// - Logical operators (AND, OR): Require boolean operands
820    /// - String concatenation (||): Requires text operands
821    pub fn check_binary_op(
822        &self,
823        op: BinaryOp,
824        left: &ResolvedType,
825        right: &ResolvedType,
826        span: Span,
827    ) -> Result<ResolvedType, PlannerError> {
828        use BinaryOp::*;
829        use ResolvedType::*;
830
831        match op {
832            // Arithmetic operators: require numeric types
833            Add | Sub | Mul | Div => {
834                if let Some(result) = Self::temporal_arithmetic_type(op, left, right) {
835                    return Ok(result);
836                }
837                if let Some(result) = decimal_arithmetic_type(op, left, right) {
838                    return Ok(result);
839                }
840                let result = self.check_arithmetic_op(left, right, span)?;
841                Ok(result)
842            }
843
844            // Remainder is defined only for integral operands.
845            Mod => self.check_modulo_op(left, right, span),
846
847            BitAnd | BitOr | BitXor | ShiftLeft | ShiftRight => {
848                self.check_integral_op(left, right, span)
849            }
850
851            // Comparison operators: require compatible types, return boolean
852            Eq | Neq | Lt | Gt | LtEq | GtEq => {
853                self.check_comparison_op(left, right, span)?;
854                Ok(Boolean)
855            }
856
857            // Logical operators: require boolean types
858            And | Or => {
859                self.check_logical_op(left, right, span)?;
860                Ok(Boolean)
861            }
862
863            // String concatenation: requires text types
864            StringConcat => {
865                self.check_string_concat_op(left, right, span)?;
866                Ok(Text)
867            }
868        }
869    }
870
871    /// Check arithmetic operation and return the result type.
872    fn check_arithmetic_op(
873        &self,
874        left: &ResolvedType,
875        right: &ResolvedType,
876        span: Span,
877    ) -> Result<ResolvedType, PlannerError> {
878        use ResolvedType::*;
879
880        // Handle NULL propagation
881        if matches!(left, Null) || matches!(right, Null) {
882            return Ok(Null);
883        }
884
885        // Determine result type based on numeric type hierarchy
886        match (left, right) {
887            // Integer operations
888            (Integer, Integer) => Ok(Integer),
889            (Integer, BigInt) | (BigInt, Integer) | (BigInt, BigInt) => Ok(BigInt),
890            (Float, Float) => Ok(Float),
891            // f32 has 24 bits of mantissa and cannot hold the whole i32 range,
892            // so an INTEGER mixed with FLOAT widens to DOUBLE.
893            (Integer, Float)
894            | (Float, Integer)
895            | (Integer, Double)
896            | (Double, Integer)
897            | (BigInt, Float)
898            | (Float, BigInt)
899            | (BigInt, Double)
900            | (Double, BigInt)
901            | (Float, Double)
902            | (Double, Float)
903            | (Double, Double) => Ok(Double),
904
905            _ => Err(PlannerError::InvalidOperator {
906                op: "arithmetic".to_string(),
907                type_name: format!("{} and {}", left.type_name(), right.type_name()),
908                line: span.start.line,
909                column: span.start.column,
910            }),
911        }
912    }
913
914    fn temporal_arithmetic_type(
915        op: BinaryOp,
916        left: &ResolvedType,
917        right: &ResolvedType,
918    ) -> Option<ResolvedType> {
919        use ResolvedType::*;
920        match (op, left, right) {
921            (BinaryOp::Add | BinaryOp::Sub, Date, Interval) => Some(Date),
922            (BinaryOp::Add | BinaryOp::Sub, Timestamp, Interval) => Some(Timestamp),
923            (BinaryOp::Add | BinaryOp::Sub, Time, Interval) => Some(Time),
924            (BinaryOp::Add, Interval, Date) => Some(Date),
925            (BinaryOp::Add, Interval, Timestamp) => Some(Timestamp),
926            (BinaryOp::Add, Interval, Time) => Some(Time),
927            (BinaryOp::Sub, Date, Date)
928            | (BinaryOp::Sub, Timestamp, Timestamp)
929            | (BinaryOp::Sub, Time, Time)
930            | (BinaryOp::Add | BinaryOp::Sub, Interval, Interval) => Some(Interval),
931            _ => None,
932        }
933    }
934
935    /// Check remainder operands and return the integral result type.
936    fn check_modulo_op(
937        &self,
938        left: &ResolvedType,
939        right: &ResolvedType,
940        span: Span,
941    ) -> Result<ResolvedType, PlannerError> {
942        use ResolvedType::*;
943
944        if matches!(left, Null) || matches!(right, Null) {
945            return Ok(Null);
946        }
947
948        match (left, right) {
949            (Integer, Integer) => Ok(Integer),
950            (Integer, BigInt) | (BigInt, Integer) | (BigInt, BigInt) => Ok(BigInt),
951            _ => Err(PlannerError::InvalidOperator {
952                op: "modulo".to_string(),
953                type_name: format!("{} and {}", left.type_name(), right.type_name()),
954                line: span.start.line,
955                column: span.start.column,
956            }),
957        }
958    }
959
960    fn check_integral_op(
961        &self,
962        left: &ResolvedType,
963        right: &ResolvedType,
964        span: Span,
965    ) -> Result<ResolvedType, PlannerError> {
966        use ResolvedType::*;
967        if matches!(left, Null) || matches!(right, Null) {
968            return Ok(Null);
969        }
970        match (left, right) {
971            (Integer, Integer) => Ok(Integer),
972            (Integer | BigInt, Integer | BigInt) => Ok(BigInt),
973            _ => Err(PlannerError::invalid_operator(
974                "bitwise",
975                format!("{} and {}", left.type_name(), right.type_name()),
976                span,
977            )),
978        }
979    }
980
981    /// Check comparison operation for compatible types.
982    pub(crate) fn check_comparison_op(
983        &self,
984        left: &ResolvedType,
985        right: &ResolvedType,
986        span: Span,
987    ) -> Result<(), PlannerError> {
988        use ResolvedType::*;
989
990        // NULL can be compared with anything
991        if matches!(left, Null) || matches!(right, Null) {
992            return Ok(());
993        }
994
995        // Check type compatibility
996        let compatible = match (left, right) {
997            // Same types are always comparable
998            (a, b) if a == b => true,
999
1000            // Numeric types are comparable with each other
1001            (
1002                Integer | BigInt | Float | Double | Decimal { .. },
1003                Integer | BigInt | Float | Double | Decimal { .. },
1004            ) => true,
1005
1006            // Text types
1007            (Text, Text) => true,
1008
1009            // Boolean types
1010            (Boolean, Boolean) => true,
1011
1012            // Timestamp types
1013            (Timestamp, Timestamp) => true,
1014            (Date, Date) | (Time, Time) | (Interval, Interval) => true,
1015
1016            // Vector types (for equality only, dimension must match)
1017            (Vector { dimension: d1, .. }, Vector { dimension: d2, .. }) => d1 == d2,
1018
1019            _ => false,
1020        };
1021
1022        if compatible {
1023            Ok(())
1024        } else {
1025            Err(PlannerError::TypeMismatch {
1026                expected: left.type_name().to_string(),
1027                found: right.type_name().to_string(),
1028                line: span.start.line,
1029                column: span.start.column,
1030            })
1031        }
1032    }
1033
1034    /// Check logical operation for boolean types.
1035    fn check_logical_op(
1036        &self,
1037        left: &ResolvedType,
1038        right: &ResolvedType,
1039        span: Span,
1040    ) -> Result<(), PlannerError> {
1041        use ResolvedType::*;
1042
1043        // NULL is allowed (three-valued logic)
1044        let left_ok = matches!(left, Boolean | Null);
1045        let right_ok = matches!(right, Boolean | Null);
1046
1047        if !left_ok {
1048            return Err(PlannerError::TypeMismatch {
1049                expected: "Boolean".to_string(),
1050                found: left.type_name().to_string(),
1051                line: span.start.line,
1052                column: span.start.column,
1053            });
1054        }
1055
1056        if !right_ok {
1057            return Err(PlannerError::TypeMismatch {
1058                expected: "Boolean".to_string(),
1059                found: right.type_name().to_string(),
1060                line: span.start.line,
1061                column: span.start.column,
1062            });
1063        }
1064
1065        Ok(())
1066    }
1067
1068    /// Check string concatenation operation.
1069    fn check_string_concat_op(
1070        &self,
1071        left: &ResolvedType,
1072        right: &ResolvedType,
1073        span: Span,
1074    ) -> Result<(), PlannerError> {
1075        use ResolvedType::*;
1076
1077        // NULL is allowed
1078        let left_ok = matches!(left, Text | Null);
1079        let right_ok = matches!(right, Text | Null);
1080
1081        if !left_ok {
1082            return Err(PlannerError::TypeMismatch {
1083                expected: "Text".to_string(),
1084                found: left.type_name().to_string(),
1085                line: span.start.line,
1086                column: span.start.column,
1087            });
1088        }
1089
1090        if !right_ok {
1091            return Err(PlannerError::TypeMismatch {
1092                expected: "Text".to_string(),
1093                found: right.type_name().to_string(),
1094                line: span.start.line,
1095                column: span.start.column,
1096            });
1097        }
1098
1099        Ok(())
1100    }
1101
1102    /// Infer the type of a unary operation.
1103    #[allow(dead_code)]
1104    fn infer_unary_op_type(
1105        &self,
1106        op: UnaryOp,
1107        operand: &Expr,
1108        table: &TableMetadata,
1109        span: Span,
1110    ) -> Result<TypedExpr, PlannerError> {
1111        let operand_typed = self.infer_type(operand, table)?;
1112
1113        let result_type = match op {
1114            UnaryOp::Not => {
1115                // NOT requires boolean operand
1116                if !matches!(
1117                    operand_typed.resolved_type,
1118                    ResolvedType::Boolean | ResolvedType::Null
1119                ) {
1120                    return Err(PlannerError::TypeMismatch {
1121                        expected: "Boolean".to_string(),
1122                        found: operand_typed.resolved_type.type_name().to_string(),
1123                        line: span.start.line,
1124                        column: span.start.column,
1125                    });
1126                }
1127                ResolvedType::Boolean
1128            }
1129            UnaryOp::Minus => {
1130                // Unary minus requires numeric operand
1131                match &operand_typed.resolved_type {
1132                    ResolvedType::Integer => ResolvedType::Integer,
1133                    ResolvedType::BigInt => ResolvedType::BigInt,
1134                    ResolvedType::Float => ResolvedType::Float,
1135                    ResolvedType::Double => ResolvedType::Double,
1136                    ResolvedType::Decimal { precision, scale } => ResolvedType::Decimal {
1137                        precision: *precision,
1138                        scale: *scale,
1139                    },
1140                    ResolvedType::Null => ResolvedType::Null,
1141                    other => {
1142                        return Err(PlannerError::InvalidOperator {
1143                            op: "unary minus".to_string(),
1144                            type_name: other.type_name().to_string(),
1145                            line: span.start.line,
1146                            column: span.start.column,
1147                        });
1148                    }
1149                }
1150            }
1151            UnaryOp::BitNot => match &operand_typed.resolved_type {
1152                ResolvedType::Integer => ResolvedType::Integer,
1153                ResolvedType::BigInt => ResolvedType::BigInt,
1154                ResolvedType::Null => ResolvedType::Null,
1155                other => {
1156                    return Err(PlannerError::InvalidOperator {
1157                        op: "bitwise not".to_string(),
1158                        type_name: other.type_name().to_string(),
1159                        line: span.start.line,
1160                        column: span.start.column,
1161                    });
1162                }
1163            },
1164        };
1165
1166        Ok(TypedExpr {
1167            kind: TypedExprKind::UnaryOp {
1168                op,
1169                operand: Box::new(operand_typed),
1170            },
1171            resolved_type: result_type,
1172            span,
1173        })
1174    }
1175
1176    fn infer_unary_op_type_with_scope(
1177        &self,
1178        op: UnaryOp,
1179        operand: &Expr,
1180        scope: &[ScopedTable],
1181        plan_subquery: &SubqueryPlanner<'_>,
1182        span: Span,
1183    ) -> Result<TypedExpr, PlannerError> {
1184        let operand_typed = self.infer_type_with_scope(operand, scope, plan_subquery)?;
1185
1186        let result_type = match op {
1187            UnaryOp::Not => {
1188                if !matches!(
1189                    operand_typed.resolved_type,
1190                    ResolvedType::Boolean | ResolvedType::Null
1191                ) {
1192                    return Err(PlannerError::TypeMismatch {
1193                        expected: "Boolean".to_string(),
1194                        found: operand_typed.resolved_type.type_name().to_string(),
1195                        line: span.start.line,
1196                        column: span.start.column,
1197                    });
1198                }
1199                ResolvedType::Boolean
1200            }
1201            UnaryOp::Minus => match &operand_typed.resolved_type {
1202                ResolvedType::Integer => ResolvedType::Integer,
1203                ResolvedType::BigInt => ResolvedType::BigInt,
1204                ResolvedType::Float => ResolvedType::Float,
1205                ResolvedType::Double => ResolvedType::Double,
1206                ResolvedType::Decimal { precision, scale } => ResolvedType::Decimal {
1207                    precision: *precision,
1208                    scale: *scale,
1209                },
1210                ResolvedType::Null => ResolvedType::Null,
1211                other => {
1212                    return Err(PlannerError::InvalidOperator {
1213                        op: "unary minus".to_string(),
1214                        type_name: other.type_name().to_string(),
1215                        line: span.start.line,
1216                        column: span.start.column,
1217                    });
1218                }
1219            },
1220            UnaryOp::BitNot => match &operand_typed.resolved_type {
1221                ResolvedType::Integer => ResolvedType::Integer,
1222                ResolvedType::BigInt => ResolvedType::BigInt,
1223                ResolvedType::Null => ResolvedType::Null,
1224                other => {
1225                    return Err(PlannerError::InvalidOperator {
1226                        op: "bitwise not".to_string(),
1227                        type_name: other.type_name().to_string(),
1228                        line: span.start.line,
1229                        column: span.start.column,
1230                    });
1231                }
1232            },
1233        };
1234
1235        Ok(TypedExpr {
1236            kind: TypedExprKind::UnaryOp {
1237                op,
1238                operand: Box::new(operand_typed),
1239            },
1240            resolved_type: result_type,
1241            span,
1242        })
1243    }
1244
1245    /// Infer the type of a function call.
1246    #[allow(dead_code)]
1247    fn infer_function_call_type(
1248        &self,
1249        name: &str,
1250        args: &[Expr],
1251        distinct: bool,
1252        star: bool,
1253        table: &TableMetadata,
1254        span: Span,
1255    ) -> Result<TypedExpr, PlannerError> {
1256        // Type-check all arguments first
1257        let typed_args: Vec<TypedExpr> = args
1258            .iter()
1259            .map(|arg| self.infer_type(arg, table))
1260            .collect::<Result<Vec<_>, _>>()?;
1261
1262        // Delegate to check_function_call for validation and return type
1263        let result_type = self.check_function_call(name, &typed_args, distinct, star, span)?;
1264
1265        Ok(TypedExpr {
1266            kind: TypedExprKind::FunctionCall {
1267                name: name.to_string(),
1268                args: typed_args,
1269                distinct,
1270                star,
1271                filter: None,
1272                order_by: Vec::new(),
1273                over: None,
1274            },
1275            resolved_type: result_type,
1276            span,
1277        })
1278    }
1279
1280    #[allow(clippy::too_many_arguments)]
1281    fn infer_function_call_type_with_scope(
1282        &self,
1283        name: &str,
1284        args: &[Expr],
1285        distinct: bool,
1286        star: bool,
1287        order_by: &[crate::ast::dml::OrderByExpr],
1288        within_group: &[crate::ast::dml::OrderByExpr],
1289        filter: Option<&Expr>,
1290        over: Option<&WindowSpec>,
1291        scope: &[ScopedTable],
1292        plan_subquery: &SubqueryPlanner<'_>,
1293        span: Span,
1294    ) -> Result<TypedExpr, PlannerError> {
1295        let lower_name = name.to_ascii_lowercase();
1296        self.validate_aggregate_clause_placement(
1297            &lower_name,
1298            distinct,
1299            order_by,
1300            within_group,
1301            filter,
1302            over.is_some(),
1303            span,
1304        )?;
1305        if over.is_some() {
1306            match lower_name.as_str() {
1307                "lag" | "lead" => {
1308                    validate_offset_window_call(name, args.len(), distinct, star)?;
1309                }
1310                "first_value" | "last_value" | "ntile" => {
1311                    validate_exact_window_call(name, args.len(), 1, distinct, star)?;
1312                }
1313                "nth_value" => {
1314                    validate_exact_window_call(name, args.len(), 2, distinct, star)?;
1315                }
1316                "percent_rank" | "cume_dist" => {
1317                    validate_exact_window_call(name, args.len(), 0, distinct, star)?;
1318                }
1319                _ => {}
1320            }
1321        }
1322
1323        let mut typed_args: Vec<TypedExpr> = args
1324            .iter()
1325            .map(|arg| self.infer_type_with_scope(arg, scope, plan_subquery))
1326            .collect::<Result<Vec<_>, _>>()?;
1327
1328        // WITHIN GROUP normalizes onto the same aggregate-local ordering as an
1329        // in-argument ORDER BY; the parser rejects supplying both at once.
1330        let order_by_source = if within_group.is_empty() {
1331            order_by
1332        } else {
1333            within_group
1334        };
1335        let typed_order_by = order_by_source
1336            .iter()
1337            .map(|order| {
1338                let expr = self.infer_type_with_scope(&order.expr, scope, plan_subquery)?;
1339                if super::typed_expr_contains_aggregate(&expr) {
1340                    return Err(PlannerError::invalid_expression(
1341                        "aggregate functions are not allowed in aggregate ORDER BY".to_string(),
1342                    ));
1343                }
1344                if super::typed_expr_contains_window(&expr) {
1345                    return Err(PlannerError::invalid_expression(
1346                        "window functions are not allowed in aggregate ORDER BY".to_string(),
1347                    ));
1348                }
1349                Ok(SortExpr::new(
1350                    expr,
1351                    order.asc.unwrap_or(true),
1352                    order.nulls_first.unwrap_or(false),
1353                ))
1354            })
1355            .collect::<Result<Vec<_>, PlannerError>>()?;
1356
1357        let typed_filter = filter
1358            .map(|predicate| {
1359                let typed = self.infer_type_with_scope(predicate, scope, plan_subquery)?;
1360                if super::typed_expr_contains_aggregate(&typed) {
1361                    return Err(PlannerError::invalid_expression(
1362                        "aggregate functions are not allowed in FILTER".to_string(),
1363                    ));
1364                }
1365                if super::typed_expr_contains_window(&typed) {
1366                    return Err(PlannerError::invalid_expression(
1367                        "window functions are not allowed in FILTER".to_string(),
1368                    ));
1369                }
1370                if !matches!(
1371                    typed.resolved_type,
1372                    ResolvedType::Boolean | ResolvedType::Null
1373                ) {
1374                    return Err(PlannerError::type_mismatch(
1375                        "BOOLEAN FILTER predicate",
1376                        typed.resolved_type.type_name(),
1377                        typed.span,
1378                    ));
1379                }
1380                Ok(Box::new(typed))
1381            })
1382            .transpose()?;
1383
1384        // D4 (PostgreSQL rule): with DISTINCT, every aggregate ORDER BY
1385        // expression must appear in the argument list, otherwise the sort key
1386        // is undefined after deduplication.
1387        if distinct && !typed_order_by.is_empty() {
1388            for sort in &typed_order_by {
1389                let key = super::distinct_on_expr_signature(&sort.expr);
1390                let appears = typed_args
1391                    .iter()
1392                    .any(|arg| super::distinct_on_expr_signature(arg) == key);
1393                if !appears {
1394                    return Err(PlannerError::invalid_expression(
1395                        "in an aggregate with DISTINCT, ORDER BY expressions must appear in \
1396                         the argument list"
1397                            .to_string(),
1398                    ));
1399                }
1400            }
1401        }
1402
1403        let result_type = if over.is_some() {
1404            match lower_name.as_str() {
1405                "lag" | "lead" => self.infer_offset_window_result_type(name, &mut typed_args)?,
1406                "first_value" | "last_value" => typed_args[0].resolved_type.clone(),
1407                "nth_value" => {
1408                    validate_positive_integer_argument(name, &typed_args[1])?;
1409                    typed_args[0].resolved_type.clone()
1410                }
1411                "ntile" => {
1412                    validate_positive_integer_argument(name, &typed_args[0])?;
1413                    ResolvedType::BigInt
1414                }
1415                "percent_rank" | "cume_dist" => ResolvedType::Double,
1416                "row_number" | "rank" | "dense_rank" => {
1417                    if !typed_args.is_empty() || distinct || star {
1418                        return Err(PlannerError::invalid_expression(format!(
1419                            "{}() window function takes no arguments",
1420                            name.to_ascii_uppercase()
1421                        )));
1422                    }
1423                    ResolvedType::BigInt
1424                }
1425                name if is_aggregate_name(name) => {
1426                    self.check_function_call(name, &typed_args, distinct, star, span)?
1427                }
1428                _ => {
1429                    return Err(PlannerError::unsupported_feature(
1430                        format!("function '{}' with OVER", name),
1431                        "future",
1432                        span,
1433                    ));
1434                }
1435            }
1436        } else if matches!(lower_name.as_str(), "percentile_disc" | "percentile_cont") {
1437            self.check_percentile(&lower_name, &typed_args, &typed_order_by, span)?
1438        } else if lower_name == "mode" && !within_group.is_empty() {
1439            if !typed_args.is_empty() || typed_order_by.len() != 1 {
1440                return Err(PlannerError::invalid_expression(
1441                    "MODE requires no arguments and exactly one WITHIN GROUP sort expression"
1442                        .to_string(),
1443                ));
1444            }
1445            typed_order_by[0].expr.resolved_type.clone()
1446        } else {
1447            self.check_function_call(name, &typed_args, distinct, star, span)?
1448        };
1449
1450        let typed_over = over
1451            .map(|window| {
1452                if let Some(base) = &window.base {
1453                    return Err(PlannerError::invalid_expression(format!(
1454                        "named window '{base}' was not resolved in its query block"
1455                    )));
1456                }
1457                let partition_by = window
1458                    .partition_by
1459                    .iter()
1460                    .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
1461                    .collect::<Result<Vec<_>, _>>()?;
1462                let order_by = window
1463                    .order_by
1464                    .iter()
1465                    .map(|order| {
1466                        let expr = self.infer_type_with_scope(&order.expr, scope, plan_subquery)?;
1467                        Ok(SortExpr::new(
1468                            expr,
1469                            order.asc.unwrap_or(true),
1470                            order.nulls_first.unwrap_or(false),
1471                        ))
1472                    })
1473                    .collect::<Result<Vec<_>, PlannerError>>()?;
1474                if let Some(frame) = &window.frame {
1475                    validate_window_frame(&lower_name, frame, &order_by)?;
1476                }
1477                Ok(TypedWindowSpec {
1478                    partition_by,
1479                    order_by,
1480                    frame: window.frame.clone(),
1481                })
1482            })
1483            .transpose()?;
1484
1485        Ok(TypedExpr {
1486            kind: TypedExprKind::FunctionCall {
1487                name: name.to_string(),
1488                args: typed_args,
1489                distinct,
1490                star,
1491                filter: typed_filter,
1492                order_by: typed_order_by,
1493                over: typed_over,
1494            },
1495            resolved_type: result_type,
1496            span,
1497        })
1498    }
1499
1500    /// Placement rules for FILTER / WITHIN GROUP / aggregate ORDER BY that do
1501    /// not require typed arguments (issue #148, D2/D6/D7).
1502    #[allow(clippy::too_many_arguments)]
1503    fn validate_aggregate_clause_placement(
1504        &self,
1505        lower_name: &str,
1506        distinct: bool,
1507        order_by: &[crate::ast::dml::OrderByExpr],
1508        within_group: &[crate::ast::dml::OrderByExpr],
1509        filter: Option<&Expr>,
1510        has_over: bool,
1511        span: Span,
1512    ) -> Result<(), PlannerError> {
1513        let is_ordered_set = is_ordered_set_aggregate_name(lower_name);
1514        let is_aggregate = is_aggregate_name(lower_name);
1515
1516        if let Some(filter) = filter {
1517            if has_over {
1518                // PostgreSQL allows FILTER on window-aggregates; the Alopex
1519                // window executor frame path does not implement it yet, so the
1520                // boundary is a stable explicit error (D2).
1521                return Err(PlannerError::unsupported_feature(
1522                    "FILTER on a window function call",
1523                    "future",
1524                    span,
1525                ));
1526            }
1527            if !is_aggregate {
1528                return Err(PlannerError::invalid_expression(format!(
1529                    "FILTER (WHERE ...) is only valid for aggregate functions, not '{lower_name}'"
1530                )));
1531            }
1532            if super::expr_contains_subquery(filter) {
1533                return Err(PlannerError::unsupported_feature(
1534                    "subquery in aggregate FILTER",
1535                    "future",
1536                    filter.span,
1537                ));
1538            }
1539        }
1540
1541        if !within_group.is_empty() {
1542            if has_over {
1543                // PostgreSQL: ordered-set aggregates cannot be window calls.
1544                return Err(PlannerError::invalid_expression(
1545                    "WITHIN GROUP cannot be combined with OVER".to_string(),
1546                ));
1547            }
1548            if !is_ordered_set {
1549                return Err(PlannerError::invalid_expression(format!(
1550                    "WITHIN GROUP is only valid for ordered-set aggregate functions, \
1551                     not '{lower_name}'"
1552                )));
1553            }
1554            if distinct {
1555                return Err(PlannerError::invalid_expression(
1556                    "DISTINCT is not supported with WITHIN GROUP".to_string(),
1557                ));
1558            }
1559            if within_group
1560                .iter()
1561                .any(|order| super::expr_contains_subquery(&order.expr))
1562            {
1563                return Err(PlannerError::unsupported_feature(
1564                    "subquery in aggregate ORDER BY",
1565                    "future",
1566                    span,
1567                ));
1568            }
1569        }
1570
1571        if !order_by.is_empty() {
1572            if has_over {
1573                // PostgreSQL: "aggregate ORDER BY is not implemented for
1574                // window functions".
1575                return Err(PlannerError::invalid_expression(
1576                    "aggregate ORDER BY cannot be combined with OVER".to_string(),
1577                ));
1578            }
1579            if !is_aggregate || is_ordered_set {
1580                return Err(PlannerError::invalid_expression(format!(
1581                    "ORDER BY in the argument list is only valid for aggregate functions, \
1582                     not '{lower_name}'"
1583                )));
1584            }
1585            if order_by
1586                .iter()
1587                .any(|order| super::expr_contains_subquery(&order.expr))
1588            {
1589                return Err(PlannerError::unsupported_feature(
1590                    "subquery in aggregate ORDER BY",
1591                    "future",
1592                    span,
1593                ));
1594            }
1595        }
1596
1597        if matches!(lower_name, "percentile_disc" | "percentile_cont") && within_group.is_empty() {
1598            return Err(PlannerError::invalid_expression(format!(
1599                "WITHIN GROUP (ORDER BY ...) is required for {}",
1600                lower_name.to_ascii_uppercase()
1601            )));
1602        }
1603
1604        Ok(())
1605    }
1606
1607    /// Argument and ordering rules for `PERCENTILE_DISC(fraction) WITHIN
1608    /// GROUP (ORDER BY sort_expr)` (issue #148, D5). The result type is the
1609    /// sort expression's type; PostgreSQL 16 behaves identically.
1610    fn check_percentile(
1611        &self,
1612        name: &str,
1613        args: &[TypedExpr],
1614        order_by: &[SortExpr],
1615        span: Span,
1616    ) -> Result<ResolvedType, PlannerError> {
1617        if args.len() != 1 {
1618            return Err(PlannerError::type_mismatch(
1619                "1 argument",
1620                format!("{} arguments", args.len()),
1621                span,
1622            ));
1623        }
1624        let _ = percentile_fraction_named(name, &args[0])?;
1625        if order_by.len() != 1 {
1626            return Err(PlannerError::invalid_expression(format!(
1627                "{} requires WITHIN GROUP (ORDER BY ...) with exactly one sort expression",
1628                name.to_ascii_uppercase()
1629            )));
1630        }
1631        Ok(order_by[0].expr.resolved_type.clone())
1632    }
1633
1634    fn infer_offset_window_result_type(
1635        &self,
1636        name: &str,
1637        args: &mut [TypedExpr],
1638    ) -> Result<ResolvedType, PlannerError> {
1639        if let Some(offset) = args.get(1)
1640            && !matches!(
1641                offset.resolved_type,
1642                ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Null
1643            )
1644        {
1645            return Err(PlannerError::type_mismatch(
1646                "INTEGER offset",
1647                offset.resolved_type.type_name(),
1648                offset.span,
1649            ));
1650        }
1651
1652        let value_type = args
1653            .first()
1654            .map(|arg| arg.resolved_type.clone())
1655            .ok_or_else(|| {
1656                PlannerError::invalid_expression(format!(
1657                    "{}() window function expects 1 to 3 arguments",
1658                    name.to_ascii_uppercase()
1659                ))
1660            })?;
1661        let result_type = if let Some(default) = args.get(2) {
1662            self.common_compatible_result_type(&value_type, &default.resolved_type, default.span)?
1663        } else {
1664            value_type
1665        };
1666
1667        coerce_compatible_result(&mut args[0], &result_type);
1668        if let Some(default) = args.get_mut(2) {
1669            coerce_compatible_result(default, &result_type);
1670        }
1671
1672        Ok(result_type)
1673    }
1674
1675    fn common_compatible_result_type(
1676        &self,
1677        current: &ResolvedType,
1678        next: &ResolvedType,
1679        span: Span,
1680    ) -> Result<ResolvedType, PlannerError> {
1681        if matches!(current, ResolvedType::Null) {
1682            return Ok(next.clone());
1683        }
1684        if matches!(next, ResolvedType::Null) || current == next {
1685            return Ok(current.clone());
1686        }
1687        if is_numeric_type(current) && is_numeric_type(next) {
1688            return self.check_arithmetic_op(current, next, span);
1689        }
1690        if next.can_cast_to(current) {
1691            return Ok(current.clone());
1692        }
1693        if current.can_cast_to(next) {
1694            return Ok(next.clone());
1695        }
1696        Err(PlannerError::type_mismatch(
1697            current.type_name(),
1698            next.type_name(),
1699            span,
1700        ))
1701    }
1702
1703    /// Infer the type of a BETWEEN expression.
1704    #[allow(dead_code)]
1705    fn infer_between_type(
1706        &self,
1707        expr: &Expr,
1708        low: &Expr,
1709        high: &Expr,
1710        negated: bool,
1711        table: &TableMetadata,
1712        span: Span,
1713    ) -> Result<TypedExpr, PlannerError> {
1714        let expr_typed = self.infer_type(expr, table)?;
1715        let low_typed = self.infer_type(low, table)?;
1716        let high_typed = self.infer_type(high, table)?;
1717
1718        // Check that all three expressions have compatible types
1719        self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
1720        self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
1721
1722        Ok(TypedExpr {
1723            kind: TypedExprKind::Between {
1724                expr: Box::new(expr_typed),
1725                low: Box::new(low_typed),
1726                high: Box::new(high_typed),
1727                negated,
1728            },
1729            resolved_type: ResolvedType::Boolean,
1730            span,
1731        })
1732    }
1733
1734    #[allow(clippy::too_many_arguments)]
1735    fn infer_between_type_with_scope(
1736        &self,
1737        expr: &Expr,
1738        low: &Expr,
1739        high: &Expr,
1740        negated: bool,
1741        scope: &[ScopedTable],
1742        plan_subquery: &SubqueryPlanner<'_>,
1743        span: Span,
1744    ) -> Result<TypedExpr, PlannerError> {
1745        if row_items(expr).is_some() || row_items(low).is_some() || row_items(high).is_some() {
1746            let expr_typed = self.infer_row_operand_with_scope(expr, scope, plan_subquery)?;
1747            let low_typed = self.infer_row_operand_with_scope(low, scope, plan_subquery)?;
1748            let high_typed = self.infer_row_operand_with_scope(high, scope, plan_subquery)?;
1749            let width = expr_typed.len();
1750            self.check_row_arity(width, low_typed.len(), span)?;
1751            self.check_row_arity(width, high_typed.len(), span)?;
1752            self.check_row_types(&expr_typed, &low_typed, span)?;
1753            self.check_row_types(&expr_typed, &high_typed, span)?;
1754            let mut args = expr_typed;
1755            args.extend(low_typed);
1756            args.extend(high_typed);
1757            return Ok(internal_predicate(
1758                format!("{INTERNAL_ROW_BETWEEN}:{width}:{}", u8::from(negated)),
1759                args,
1760                span,
1761            ));
1762        }
1763
1764        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1765        let low_typed = self.infer_type_with_scope(low, scope, plan_subquery)?;
1766        let high_typed = self.infer_type_with_scope(high, scope, plan_subquery)?;
1767        self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
1768        self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
1769
1770        Ok(TypedExpr {
1771            kind: TypedExprKind::Between {
1772                expr: Box::new(expr_typed),
1773                low: Box::new(low_typed),
1774                high: Box::new(high_typed),
1775                negated,
1776            },
1777            resolved_type: ResolvedType::Boolean,
1778            span,
1779        })
1780    }
1781
1782    /// Infer the type of a LIKE expression.
1783    #[allow(dead_code)]
1784    #[allow(clippy::too_many_arguments)]
1785    fn infer_like_type(
1786        &self,
1787        expr: &Expr,
1788        pattern: &Expr,
1789        escape: Option<&Expr>,
1790        negated: bool,
1791        kind: PatternMatchKind,
1792        table: &TableMetadata,
1793        span: Span,
1794    ) -> Result<TypedExpr, PlannerError> {
1795        let expr_typed = self.infer_type(expr, table)?;
1796        let pattern_typed = self.infer_type(pattern, table)?;
1797
1798        // Expression must be text
1799        if !matches!(
1800            expr_typed.resolved_type,
1801            ResolvedType::Text | ResolvedType::Null
1802        ) {
1803            return Err(PlannerError::TypeMismatch {
1804                expected: "Text".to_string(),
1805                found: expr_typed.resolved_type.type_name().to_string(),
1806                line: expr.span.start.line,
1807                column: expr.span.start.column,
1808            });
1809        }
1810
1811        // Pattern must be text
1812        if !matches!(
1813            pattern_typed.resolved_type,
1814            ResolvedType::Text | ResolvedType::Null
1815        ) {
1816            return Err(PlannerError::TypeMismatch {
1817                expected: "Text".to_string(),
1818                found: pattern_typed.resolved_type.type_name().to_string(),
1819                line: pattern.span.start.line,
1820                column: pattern.span.start.column,
1821            });
1822        }
1823
1824        let escape_typed = if let Some(esc) = escape {
1825            let typed = self.infer_type(esc, table)?;
1826            if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
1827                return Err(PlannerError::TypeMismatch {
1828                    expected: "Text".to_string(),
1829                    found: typed.resolved_type.type_name().to_string(),
1830                    line: esc.span.start.line,
1831                    column: esc.span.start.column,
1832                });
1833            }
1834            Some(Box::new(typed))
1835        } else {
1836            None
1837        };
1838
1839        Ok(TypedExpr {
1840            kind: TypedExprKind::Like {
1841                expr: Box::new(expr_typed),
1842                pattern: Box::new(pattern_typed),
1843                escape: escape_typed,
1844                negated,
1845                kind,
1846            },
1847            resolved_type: ResolvedType::Boolean,
1848            span,
1849        })
1850    }
1851
1852    #[allow(clippy::too_many_arguments)]
1853    #[allow(clippy::too_many_arguments)]
1854    fn infer_like_type_with_scope(
1855        &self,
1856        expr: &Expr,
1857        pattern: &Expr,
1858        escape: Option<&Expr>,
1859        negated: bool,
1860        kind: PatternMatchKind,
1861        scope: &[ScopedTable],
1862        plan_subquery: &SubqueryPlanner<'_>,
1863        span: Span,
1864    ) -> Result<TypedExpr, PlannerError> {
1865        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1866        let pattern_typed = self.infer_type_with_scope(pattern, scope, plan_subquery)?;
1867
1868        if !matches!(
1869            expr_typed.resolved_type,
1870            ResolvedType::Text | ResolvedType::Null
1871        ) {
1872            return Err(PlannerError::TypeMismatch {
1873                expected: "Text".to_string(),
1874                found: expr_typed.resolved_type.type_name().to_string(),
1875                line: expr.span.start.line,
1876                column: expr.span.start.column,
1877            });
1878        }
1879
1880        if !matches!(
1881            pattern_typed.resolved_type,
1882            ResolvedType::Text | ResolvedType::Null
1883        ) {
1884            return Err(PlannerError::TypeMismatch {
1885                expected: "Text".to_string(),
1886                found: pattern_typed.resolved_type.type_name().to_string(),
1887                line: pattern.span.start.line,
1888                column: pattern.span.start.column,
1889            });
1890        }
1891
1892        let escape_typed = if let Some(esc) = escape {
1893            let typed = self.infer_type_with_scope(esc, scope, plan_subquery)?;
1894            if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
1895                return Err(PlannerError::TypeMismatch {
1896                    expected: "Text".to_string(),
1897                    found: typed.resolved_type.type_name().to_string(),
1898                    line: esc.span.start.line,
1899                    column: esc.span.start.column,
1900                });
1901            }
1902            Some(Box::new(typed))
1903        } else {
1904            None
1905        };
1906
1907        Ok(TypedExpr {
1908            kind: TypedExprKind::Like {
1909                expr: Box::new(expr_typed),
1910                pattern: Box::new(pattern_typed),
1911                escape: escape_typed,
1912                negated,
1913                kind,
1914            },
1915            resolved_type: ResolvedType::Boolean,
1916            span,
1917        })
1918    }
1919
1920    /// Infer the type of an IN list expression.
1921    #[allow(dead_code)]
1922    fn infer_in_list_type(
1923        &self,
1924        expr: &Expr,
1925        list: &[Expr],
1926        negated: bool,
1927        table: &TableMetadata,
1928        span: Span,
1929    ) -> Result<TypedExpr, PlannerError> {
1930        let expr_typed = self.infer_type(expr, table)?;
1931
1932        let typed_list: Vec<TypedExpr> = list
1933            .iter()
1934            .map(|item| {
1935                let typed = self.infer_type(item, table)?;
1936                // Check each item is compatible with the expression
1937                self.check_comparison_op(
1938                    &expr_typed.resolved_type,
1939                    &typed.resolved_type,
1940                    item.span,
1941                )?;
1942                Ok(typed)
1943            })
1944            .collect::<Result<Vec<_>, PlannerError>>()?;
1945
1946        Ok(TypedExpr {
1947            kind: TypedExprKind::InList {
1948                expr: Box::new(expr_typed),
1949                list: typed_list,
1950                negated,
1951            },
1952            resolved_type: ResolvedType::Boolean,
1953            span,
1954        })
1955    }
1956
1957    fn infer_in_list_type_with_scope(
1958        &self,
1959        expr: &Expr,
1960        list: &[Expr],
1961        negated: bool,
1962        scope: &[ScopedTable],
1963        plan_subquery: &SubqueryPlanner<'_>,
1964        span: Span,
1965    ) -> Result<TypedExpr, PlannerError> {
1966        if row_items(expr).is_some() || list.iter().any(|item| row_items(item).is_some()) {
1967            let mut args = self.infer_row_operand_with_scope(expr, scope, plan_subquery)?;
1968            let width = args.len();
1969            for item in list {
1970                let typed = self.infer_row_operand_with_scope(item, scope, plan_subquery)?;
1971                self.check_row_arity(width, typed.len(), item.span)?;
1972                self.check_row_types(&args[..width], &typed, item.span)?;
1973                args.extend(typed);
1974            }
1975            return Ok(internal_predicate(
1976                format!("{INTERNAL_ROW_IN}:{width}:{}", u8::from(negated)),
1977                args,
1978                span,
1979            ));
1980        }
1981
1982        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1983
1984        let typed_list: Vec<TypedExpr> = list
1985            .iter()
1986            .map(|item| {
1987                let typed = self.infer_type_with_scope(item, scope, plan_subquery)?;
1988                self.check_comparison_op(
1989                    &expr_typed.resolved_type,
1990                    &typed.resolved_type,
1991                    item.span,
1992                )?;
1993                Ok(typed)
1994            })
1995            .collect::<Result<Vec<_>, PlannerError>>()?;
1996
1997        Ok(TypedExpr {
1998            kind: TypedExprKind::InList {
1999                expr: Box::new(expr_typed),
2000                list: typed_list,
2001                negated,
2002            },
2003            resolved_type: ResolvedType::Boolean,
2004            span,
2005        })
2006    }
2007
2008    fn infer_truth_predicate_with_scope(
2009        &self,
2010        expr: &Expr,
2011        value: TruthValue,
2012        negated: bool,
2013        scope: &[ScopedTable],
2014        plan_subquery: &SubqueryPlanner<'_>,
2015        span: Span,
2016    ) -> Result<TypedExpr, PlannerError> {
2017        let typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
2018        if !matches!(
2019            typed.resolved_type,
2020            ResolvedType::Boolean | ResolvedType::Null
2021        ) {
2022            return Err(PlannerError::type_mismatch(
2023                "Boolean",
2024                typed.resolved_type.type_name(),
2025                expr.span,
2026            ));
2027        }
2028        let name = match value {
2029            TruthValue::True => INTERNAL_TRUTH_TRUE,
2030            TruthValue::False => INTERNAL_TRUTH_FALSE,
2031            TruthValue::Unknown => INTERNAL_TRUTH_UNKNOWN,
2032        };
2033        Ok(internal_predicate(
2034            format!("{name}:{}", u8::from(negated)),
2035            vec![typed],
2036            span,
2037        ))
2038    }
2039
2040    fn infer_distinct_predicate_with_scope(
2041        &self,
2042        left: &Expr,
2043        right: &Expr,
2044        negated: bool,
2045        scope: &[ScopedTable],
2046        plan_subquery: &SubqueryPlanner<'_>,
2047        span: Span,
2048    ) -> Result<TypedExpr, PlannerError> {
2049        let (mut left, right, width) =
2050            self.infer_row_pair_with_scope(left, right, scope, plan_subquery, span)?;
2051        left.extend(right);
2052        Ok(internal_predicate(
2053            format!("{INTERNAL_ROW_DISTINCT}:{width}:{}", u8::from(negated)),
2054            left,
2055            span,
2056        ))
2057    }
2058
2059    fn infer_row_pair_with_scope(
2060        &self,
2061        left: &Expr,
2062        right: &Expr,
2063        scope: &[ScopedTable],
2064        plan_subquery: &SubqueryPlanner<'_>,
2065        span: Span,
2066    ) -> Result<(Vec<TypedExpr>, Vec<TypedExpr>, usize), PlannerError> {
2067        let left = self.infer_row_operand_with_scope(left, scope, plan_subquery)?;
2068        let right = self.infer_row_operand_with_scope(right, scope, plan_subquery)?;
2069        let width = left.len();
2070        self.check_row_arity(width, right.len(), span)?;
2071        self.check_row_types(&left, &right, span)?;
2072        Ok((left, right, width))
2073    }
2074
2075    fn infer_row_operand_with_scope(
2076        &self,
2077        expr: &Expr,
2078        scope: &[ScopedTable],
2079        plan_subquery: &SubqueryPlanner<'_>,
2080    ) -> Result<Vec<TypedExpr>, PlannerError> {
2081        match row_items(expr) {
2082            Some(items) => items
2083                .iter()
2084                .map(|item| self.infer_type_with_scope(item, scope, plan_subquery))
2085                .collect(),
2086            None => Ok(vec![self.infer_type_with_scope(
2087                expr,
2088                scope,
2089                plan_subquery,
2090            )?]),
2091        }
2092    }
2093
2094    fn check_row_arity(
2095        &self,
2096        expected: usize,
2097        actual: usize,
2098        span: Span,
2099    ) -> Result<(), PlannerError> {
2100        if expected == actual {
2101            Ok(())
2102        } else {
2103            Err(PlannerError::RowArityMismatch {
2104                expected,
2105                actual,
2106                line: span.start.line,
2107                column: span.start.column,
2108            })
2109        }
2110    }
2111
2112    fn check_row_types(
2113        &self,
2114        left: &[TypedExpr],
2115        right: &[TypedExpr],
2116        span: Span,
2117    ) -> Result<(), PlannerError> {
2118        for (left, right) in left.iter().zip(right) {
2119            self.check_comparison_op(&left.resolved_type, &right.resolved_type, span)?;
2120        }
2121        Ok(())
2122    }
2123
2124    /// Infer the type of an IS NULL expression.
2125    #[allow(dead_code)]
2126    fn infer_is_null_type(
2127        &self,
2128        expr: &Expr,
2129        negated: bool,
2130        table: &TableMetadata,
2131        span: Span,
2132    ) -> Result<TypedExpr, PlannerError> {
2133        let expr_typed = self.infer_type(expr, table)?;
2134
2135        Ok(TypedExpr {
2136            kind: TypedExprKind::IsNull {
2137                expr: Box::new(expr_typed),
2138                negated,
2139            },
2140            resolved_type: ResolvedType::Boolean,
2141            span,
2142        })
2143    }
2144
2145    fn infer_is_null_type_with_scope(
2146        &self,
2147        expr: &Expr,
2148        negated: bool,
2149        scope: &[ScopedTable],
2150        plan_subquery: &SubqueryPlanner<'_>,
2151        span: Span,
2152    ) -> Result<TypedExpr, PlannerError> {
2153        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
2154
2155        Ok(TypedExpr {
2156            kind: TypedExprKind::IsNull {
2157                expr: Box::new(expr_typed),
2158                negated,
2159            },
2160            resolved_type: ResolvedType::Boolean,
2161            span,
2162        })
2163    }
2164
2165    /// Infer the type of a vector literal.
2166    fn infer_vector_literal_type(
2167        &self,
2168        values: &[f64],
2169        span: Span,
2170    ) -> Result<TypedExpr, PlannerError> {
2171        Ok(TypedExpr {
2172            kind: TypedExprKind::VectorLiteral(values.to_vec()),
2173            resolved_type: ResolvedType::Vector {
2174                dimension: values.len() as u32,
2175                metric: VectorMetric::Cosine, // Default metric for literals
2176            },
2177            span,
2178        })
2179    }
2180
2181    /// Normalize a metric string to VectorMetric enum (case-insensitive).
2182    ///
2183    /// # Valid Values
2184    ///
2185    /// - "cosine" (case-insensitive) → `VectorMetric::Cosine`
2186    /// - "l2" (case-insensitive) → `VectorMetric::L2`
2187    /// - "inner" (case-insensitive) → `VectorMetric::Inner`
2188    ///
2189    /// # Errors
2190    ///
2191    /// Returns `PlannerError::InvalidMetric` if the value is not recognized.
2192    pub fn normalize_metric(&self, metric: &str, span: Span) -> Result<VectorMetric, PlannerError> {
2193        match metric.to_lowercase().as_str() {
2194            "cosine" => Ok(VectorMetric::Cosine),
2195            "l2" => Ok(VectorMetric::L2),
2196            "inner" => Ok(VectorMetric::Inner),
2197            _ => Err(PlannerError::InvalidMetric {
2198                value: metric.to_string(),
2199                line: span.start.line,
2200                column: span.start.column,
2201            }),
2202        }
2203    }
2204
2205    /// Check function call and return the result type.
2206    ///
2207    /// Validates that the function arguments have correct types and returns
2208    /// the result type.
2209    pub fn check_function_call(
2210        &self,
2211        name: &str,
2212        args: &[TypedExpr],
2213        distinct: bool,
2214        star: bool,
2215        span: Span,
2216    ) -> Result<ResolvedType, PlannerError> {
2217        let lower_name = name.to_ascii_lowercase();
2218
2219        match lower_name.as_str() {
2220            "count" => self.check_count(args, distinct, star, span),
2221            "sum" => self.check_sum(args, distinct, star, span),
2222            "total" => self.check_total(args, distinct, star, span),
2223            "avg" => self.check_avg(args, distinct, star, span),
2224            "min" => self.check_min_max(args, distinct, star, span),
2225            "max" => self.check_min_max(args, distinct, star, span),
2226            "group_concat" => self.check_group_concat(args, distinct, star, span),
2227            "string_agg" => self.check_string_agg(args, distinct, star, span),
2228            "json_group_array" => self.check_json_group_array(args, star, span),
2229            "array_agg" => self
2230                .check_json_group_array(args, star, span)
2231                .map(|_| ResolvedType::Array(Box::new(args[0].resolved_type.clone()))),
2232            "json_group_object" => self.check_json_group_object(args, star, span),
2233            "jsonb_agg" => self
2234                .check_json_group_array(args, star, span)
2235                .map(|_| ResolvedType::Json),
2236            "jsonb_object_agg" => self
2237                .check_json_group_object(args, star, span)
2238                .map(|_| ResolvedType::Json),
2239            name if is_portable_aggregate_name(name) => {
2240                check_portable_aggregate(name, args, distinct, star, span)
2241            }
2242            // GROUPING/GROUPING_ID distinguish grouping-set placeholder NULLs
2243            // from data NULLs (issue #149, D4). Placement and argument
2244            // validation happen in the planner; the result is a BIGINT
2245            // bitmask, so at most 63 arguments are accepted.
2246            "grouping" | "grouping_id" => {
2247                if distinct || star {
2248                    return Err(PlannerError::invalid_expression(
2249                        "GROUPING does not support DISTINCT or *".to_string(),
2250                    ));
2251                }
2252                if args.is_empty() {
2253                    return Err(PlannerError::invalid_expression(
2254                        "GROUPING requires at least one argument".to_string(),
2255                    ));
2256                }
2257                if args.len() > 63 {
2258                    return Err(PlannerError::invalid_expression(
2259                        "GROUPING accepts at most 63 arguments".to_string(),
2260                    ));
2261                }
2262                Ok(ResolvedType::BigInt)
2263            }
2264            _ => {
2265                let Some(signature) = crate::scalar::signature(&lower_name) else {
2266                    return Err(PlannerError::unsupported_feature(
2267                        format!("function '{name}'"),
2268                        "future",
2269                        span,
2270                    ));
2271                };
2272                if distinct || star {
2273                    return Err(PlannerError::invalid_expression(format!(
2274                        "scalar function '{name}' does not support DISTINCT or *"
2275                    )));
2276                }
2277                signature.arity.validate(name, args.len(), span)?;
2278                (signature.check)(args)?;
2279                let types: Vec<_> = args.iter().map(|arg| arg.resolved_type.clone()).collect();
2280                match &signature.ret {
2281                    crate::scalar::ReturnRule::Fixed(ty) => Ok(ty.clone()),
2282                    crate::scalar::ReturnRule::FromArgs(rule) => rule(&types),
2283                    crate::scalar::ReturnRule::FromTypedArgs(rule) => rule(args),
2284                }
2285            }
2286        }
2287    }
2288
2289    pub fn validate_having_expr(
2290        &self,
2291        expr: &TypedExpr,
2292        group_keys: &[TypedExpr],
2293        aggregates: &[AggregateExpr],
2294    ) -> Result<(), PlannerError> {
2295        use std::collections::HashSet;
2296
2297        let group_key_indices: HashSet<usize> = group_keys
2298            .iter()
2299            .filter_map(|expr| match &expr.kind {
2300                TypedExprKind::ColumnRef { column_index, .. } => Some(*column_index),
2301                _ => None,
2302            })
2303            .collect();
2304
2305        let aggregate_signatures: HashSet<AggregateSignature> = aggregates
2306            .iter()
2307            .map(aggregate_signature_from_expr)
2308            .collect();
2309
2310        fn walk(
2311            expr: &TypedExpr,
2312            group_key_indices: &HashSet<usize>,
2313            aggregate_signatures: &HashSet<AggregateSignature>,
2314        ) -> Result<(), PlannerError> {
2315            match &expr.kind {
2316                TypedExprKind::ColumnRef { column_index, .. } => {
2317                    if group_key_indices.contains(column_index) {
2318                        Ok(())
2319                    } else {
2320                        Err(PlannerError::invalid_expression(
2321                            "column in HAVING must be in GROUP BY or be aggregated".to_string(),
2322                        ))
2323                    }
2324                }
2325                TypedExprKind::FunctionCall { name, args, .. }
2326                    if name.eq_ignore_ascii_case("grouping")
2327                        || name.eq_ignore_ascii_case("grouping_id") =>
2328                {
2329                    // GROUPING in HAVING is valid when every argument is a
2330                    // grouping expression (issue #149, D5); the planner
2331                    // rewrites the call onto __grouping_id afterwards.
2332                    for arg in args {
2333                        match &arg.kind {
2334                            TypedExprKind::ColumnRef { column_index, .. }
2335                                if group_key_indices.contains(column_index) => {}
2336                            _ => {
2337                                return Err(PlannerError::invalid_expression(
2338                                    "arguments to GROUPING must be grouping expressions \
2339                                     of the query"
2340                                        .to_string(),
2341                                ));
2342                            }
2343                        }
2344                    }
2345                    Ok(())
2346                }
2347                TypedExprKind::FunctionCall {
2348                    name,
2349                    args,
2350                    distinct,
2351                    star,
2352                    filter,
2353                    order_by,
2354                    over: _,
2355                } if is_aggregate_name(name) => {
2356                    let signature = aggregate_signature_from_call(
2357                        name,
2358                        args,
2359                        *distinct,
2360                        *star,
2361                        filter.as_deref(),
2362                        order_by,
2363                    )?;
2364                    if aggregate_signatures.contains(&signature) {
2365                        Ok(())
2366                    } else {
2367                        Err(PlannerError::invalid_expression(
2368                            "aggregate in HAVING must appear in plan".to_string(),
2369                        ))
2370                    }
2371                }
2372                TypedExprKind::BinaryOp { left, right, .. } => {
2373                    walk(left, group_key_indices, aggregate_signatures)?;
2374                    walk(right, group_key_indices, aggregate_signatures)
2375                }
2376                TypedExprKind::UnaryOp { operand, .. } => {
2377                    walk(operand, group_key_indices, aggregate_signatures)
2378                }
2379                TypedExprKind::Case {
2380                    operand,
2381                    branches,
2382                    else_expr,
2383                } => {
2384                    if let Some(operand) = operand {
2385                        walk(operand, group_key_indices, aggregate_signatures)?;
2386                    }
2387                    for branch in branches {
2388                        walk(&branch.when, group_key_indices, aggregate_signatures)?;
2389                        walk(&branch.then, group_key_indices, aggregate_signatures)?;
2390                    }
2391                    if let Some(else_expr) = else_expr {
2392                        walk(else_expr, group_key_indices, aggregate_signatures)?;
2393                    }
2394                    Ok(())
2395                }
2396                TypedExprKind::FunctionCall { args, .. } => {
2397                    for arg in args {
2398                        walk(arg, group_key_indices, aggregate_signatures)?;
2399                    }
2400                    Ok(())
2401                }
2402                TypedExprKind::Between {
2403                    expr, low, high, ..
2404                } => {
2405                    walk(expr, group_key_indices, aggregate_signatures)?;
2406                    walk(low, group_key_indices, aggregate_signatures)?;
2407                    walk(high, group_key_indices, aggregate_signatures)
2408                }
2409                TypedExprKind::Like {
2410                    expr,
2411                    pattern,
2412                    escape,
2413                    ..
2414                } => {
2415                    walk(expr, group_key_indices, aggregate_signatures)?;
2416                    walk(pattern, group_key_indices, aggregate_signatures)?;
2417                    if let Some(esc) = escape {
2418                        walk(esc, group_key_indices, aggregate_signatures)?;
2419                    }
2420                    Ok(())
2421                }
2422                TypedExprKind::InList { expr, list, .. } => {
2423                    walk(expr, group_key_indices, aggregate_signatures)?;
2424                    for item in list {
2425                        walk(item, group_key_indices, aggregate_signatures)?;
2426                    }
2427                    Ok(())
2428                }
2429                TypedExprKind::IsNull { expr, .. } => {
2430                    walk(expr, group_key_indices, aggregate_signatures)
2431                }
2432                _ => Ok(()),
2433            }
2434        }
2435
2436        walk(expr, &group_key_indices, &aggregate_signatures)
2437    }
2438
2439    fn check_count(
2440        &self,
2441        args: &[TypedExpr],
2442        distinct: bool,
2443        star: bool,
2444        span: Span,
2445    ) -> Result<ResolvedType, PlannerError> {
2446        if star {
2447            if distinct {
2448                return Err(PlannerError::unsupported_feature(
2449                    "COUNT(DISTINCT *)",
2450                    "future",
2451                    span,
2452                ));
2453            }
2454            if !args.is_empty() {
2455                return Err(PlannerError::type_mismatch(
2456                    "no arguments with COUNT(*)",
2457                    format!("{} arguments", args.len()),
2458                    span,
2459                ));
2460            }
2461            return Ok(ResolvedType::BigInt);
2462        }
2463
2464        if args.len() != 1 {
2465            return Err(PlannerError::type_mismatch(
2466                "1 argument",
2467                format!("{} arguments", args.len()),
2468                span,
2469            ));
2470        }
2471
2472        if distinct {
2473            return Ok(ResolvedType::BigInt);
2474        }
2475
2476        Ok(ResolvedType::BigInt)
2477    }
2478
2479    fn check_sum(
2480        &self,
2481        args: &[TypedExpr],
2482        _distinct: bool,
2483        star: bool,
2484        span: Span,
2485    ) -> Result<ResolvedType, PlannerError> {
2486        if star {
2487            return Err(PlannerError::type_mismatch(
2488                "numeric argument",
2489                "COUNT(*) style",
2490                span,
2491            ));
2492        }
2493        let arg = self.require_single_arg(args, span)?;
2494        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
2495            return Err(PlannerError::type_mismatch(
2496                "numeric",
2497                arg.resolved_type.type_name().to_string(),
2498                arg.span,
2499            ));
2500        }
2501        Ok(crate::planner::aggregate_expr::sum_result_type(
2502            &arg.resolved_type,
2503        ))
2504    }
2505
2506    fn check_total(
2507        &self,
2508        args: &[TypedExpr],
2509        distinct: bool,
2510        star: bool,
2511        span: Span,
2512    ) -> Result<ResolvedType, PlannerError> {
2513        if star {
2514            return Err(PlannerError::type_mismatch(
2515                "numeric argument",
2516                "COUNT(*) style",
2517                span,
2518            ));
2519        }
2520        if distinct {
2521            return Err(PlannerError::unsupported_feature(
2522                "TOTAL(DISTINCT ...)",
2523                "future",
2524                span,
2525            ));
2526        }
2527        let arg = self.require_single_arg(args, span)?;
2528        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
2529            return Err(PlannerError::type_mismatch(
2530                "numeric",
2531                arg.resolved_type.type_name().to_string(),
2532                arg.span,
2533            ));
2534        }
2535        Ok(ResolvedType::Double)
2536    }
2537
2538    fn check_avg(
2539        &self,
2540        args: &[TypedExpr],
2541        _distinct: bool,
2542        star: bool,
2543        span: Span,
2544    ) -> Result<ResolvedType, PlannerError> {
2545        if star {
2546            return Err(PlannerError::type_mismatch(
2547                "numeric argument",
2548                "COUNT(*) style",
2549                span,
2550            ));
2551        }
2552        let arg = self.require_single_arg(args, span)?;
2553        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
2554            return Err(PlannerError::type_mismatch(
2555                "numeric",
2556                arg.resolved_type.type_name().to_string(),
2557                arg.span,
2558            ));
2559        }
2560        Ok(ResolvedType::Double)
2561    }
2562
2563    fn check_min_max(
2564        &self,
2565        args: &[TypedExpr],
2566        _distinct: bool,
2567        star: bool,
2568        span: Span,
2569    ) -> Result<ResolvedType, PlannerError> {
2570        if star {
2571            return Err(PlannerError::type_mismatch(
2572                "argument",
2573                "COUNT(*) style",
2574                span,
2575            ));
2576        }
2577        let arg = self.require_single_arg(args, span)?;
2578        if matches!(arg.resolved_type, ResolvedType::Vector { .. }) {
2579            return Err(PlannerError::type_mismatch(
2580                "comparable",
2581                arg.resolved_type.type_name().to_string(),
2582                arg.span,
2583            ));
2584        }
2585        Ok(arg.resolved_type.clone())
2586    }
2587
2588    fn check_group_concat(
2589        &self,
2590        args: &[TypedExpr],
2591        _distinct: bool,
2592        star: bool,
2593        span: Span,
2594    ) -> Result<ResolvedType, PlannerError> {
2595        if star {
2596            return Err(PlannerError::type_mismatch(
2597                "text argument",
2598                "COUNT(*) style",
2599                span,
2600            ));
2601        }
2602        if args.is_empty() || args.len() > 2 {
2603            return Err(PlannerError::type_mismatch(
2604                "1 or 2 arguments",
2605                format!("{} arguments", args.len()),
2606                span,
2607            ));
2608        }
2609        if !matches!(
2610            args[0].resolved_type,
2611            ResolvedType::Text | ResolvedType::Null
2612        ) {
2613            return Err(PlannerError::type_mismatch(
2614                "Text",
2615                args[0].resolved_type.type_name().to_string(),
2616                args[0].span,
2617            ));
2618        }
2619        if args.len() == 2
2620            && !matches!(
2621                args[1].resolved_type,
2622                ResolvedType::Text | ResolvedType::Null
2623            )
2624        {
2625            return Err(PlannerError::type_mismatch(
2626                "Text",
2627                args[1].resolved_type.type_name().to_string(),
2628                args[1].span,
2629            ));
2630        }
2631        Ok(ResolvedType::Text)
2632    }
2633
2634    fn check_string_agg(
2635        &self,
2636        args: &[TypedExpr],
2637        _distinct: bool,
2638        star: bool,
2639        span: Span,
2640    ) -> Result<ResolvedType, PlannerError> {
2641        if star {
2642            return Err(PlannerError::type_mismatch(
2643                "text argument",
2644                "COUNT(*) style",
2645                span,
2646            ));
2647        }
2648        if args.len() != 2 {
2649            return Err(PlannerError::type_mismatch(
2650                "2 arguments",
2651                format!("{} arguments", args.len()),
2652                span,
2653            ));
2654        }
2655        if !matches!(
2656            args[0].resolved_type,
2657            ResolvedType::Text | ResolvedType::Null
2658        ) {
2659            return Err(PlannerError::type_mismatch(
2660                "Text",
2661                args[0].resolved_type.type_name().to_string(),
2662                args[0].span,
2663            ));
2664        }
2665        if !matches!(
2666            args[1].resolved_type,
2667            ResolvedType::Text | ResolvedType::Null
2668        ) {
2669            return Err(PlannerError::type_mismatch(
2670                "Text",
2671                args[1].resolved_type.type_name().to_string(),
2672                args[1].span,
2673            ));
2674        }
2675        Ok(ResolvedType::Text)
2676    }
2677
2678    fn check_json_group_array(
2679        &self,
2680        args: &[TypedExpr],
2681        star: bool,
2682        span: Span,
2683    ) -> Result<ResolvedType, PlannerError> {
2684        if star || args.len() != 1 {
2685            return Err(PlannerError::type_mismatch(
2686                "1 argument",
2687                format!("{} arguments", args.len()),
2688                span,
2689            ));
2690        }
2691        Ok(ResolvedType::Text)
2692    }
2693
2694    fn check_json_group_object(
2695        &self,
2696        args: &[TypedExpr],
2697        star: bool,
2698        span: Span,
2699    ) -> Result<ResolvedType, PlannerError> {
2700        if star || args.len() != 2 {
2701            return Err(PlannerError::type_mismatch(
2702                "2 arguments",
2703                format!("{} arguments", args.len()),
2704                span,
2705            ));
2706        }
2707        if !matches!(
2708            args[0].resolved_type,
2709            ResolvedType::Text | ResolvedType::Null
2710        ) {
2711            return Err(PlannerError::type_mismatch(
2712                "Text",
2713                args[0].resolved_type.type_name(),
2714                args[0].span,
2715            ));
2716        }
2717        Ok(ResolvedType::Text)
2718    }
2719
2720    fn require_single_arg<'b>(
2721        &self,
2722        args: &'b [TypedExpr],
2723        span: Span,
2724    ) -> Result<&'b TypedExpr, PlannerError> {
2725        if args.len() != 1 {
2726            return Err(PlannerError::type_mismatch(
2727                "1 argument",
2728                format!("{} arguments", args.len()),
2729                span,
2730            ));
2731        }
2732        Ok(&args[0])
2733    }
2734
2735    /// Check vector_distance function arguments.
2736    ///
2737    /// Signature: `vector_distance(column: Vector, vector: Vector, metric: Text) -> Double`
2738    ///
2739    /// # Requirements
2740    ///
2741    /// - First argument must be a Vector type (column reference)
2742    /// - Second argument must be a Vector type (vector literal)
2743    /// - Third argument must be a Text type (metric string)
2744    /// - Vector dimensions must match
2745    pub fn check_vector_distance(
2746        &self,
2747        args: &[TypedExpr],
2748        span: Span,
2749    ) -> Result<ResolvedType, PlannerError> {
2750        if args.len() != 3 {
2751            return Err(PlannerError::TypeMismatch {
2752                expected: "3 arguments".to_string(),
2753                found: format!("{} arguments", args.len()),
2754                line: span.start.line,
2755                column: span.start.column,
2756            });
2757        }
2758
2759        // First argument: Vector column
2760        let col_dim = match &args[0].resolved_type {
2761            ResolvedType::Vector { dimension, .. } => *dimension,
2762            other => {
2763                return Err(PlannerError::TypeMismatch {
2764                    expected: "Vector".to_string(),
2765                    found: other.type_name().to_string(),
2766                    line: args[0].span.start.line,
2767                    column: args[0].span.start.column,
2768                });
2769            }
2770        };
2771
2772        // Second argument: Vector literal
2773        let vec_dim = match &args[1].resolved_type {
2774            ResolvedType::Vector { dimension, .. } => *dimension,
2775            other => {
2776                return Err(PlannerError::TypeMismatch {
2777                    expected: "Vector".to_string(),
2778                    found: other.type_name().to_string(),
2779                    line: args[1].span.start.line,
2780                    column: args[1].span.start.column,
2781                });
2782            }
2783        };
2784
2785        // Check dimension match
2786        self.check_vector_dimension(col_dim, vec_dim, args[1].span)?;
2787
2788        // Third argument: Metric string
2789        match &args[2].resolved_type {
2790            ResolvedType::Text => {
2791                // Validate metric value if it's a literal
2792                if let TypedExprKind::Literal(Literal::String(s)) = &args[2].kind {
2793                    self.normalize_metric(s, args[2].span)?;
2794                }
2795            }
2796            ResolvedType::Null => {
2797                // NULL metric is not allowed
2798                return Err(PlannerError::TypeMismatch {
2799                    expected: "Text (metric)".to_string(),
2800                    found: "Null".to_string(),
2801                    line: args[2].span.start.line,
2802                    column: args[2].span.start.column,
2803                });
2804            }
2805            other => {
2806                return Err(PlannerError::TypeMismatch {
2807                    expected: "Text (metric)".to_string(),
2808                    found: other.type_name().to_string(),
2809                    line: args[2].span.start.line,
2810                    column: args[2].span.start.column,
2811                });
2812            }
2813        }
2814
2815        Ok(ResolvedType::Double)
2816    }
2817
2818    /// Check vector_similarity function arguments.
2819    ///
2820    /// Signature: `vector_similarity(column: Vector, vector: Vector, metric: Text) -> Double`
2821    ///
2822    /// Same validation rules as vector_distance.
2823    pub fn check_vector_similarity(
2824        &self,
2825        args: &[TypedExpr],
2826        span: Span,
2827    ) -> Result<ResolvedType, PlannerError> {
2828        // Same validation as vector_distance
2829        self.check_vector_distance(args, span)
2830    }
2831
2832    /// Check that two vector dimensions match.
2833    ///
2834    /// # Errors
2835    ///
2836    /// Returns `PlannerError::VectorDimensionMismatch` if dimensions don't match.
2837    pub fn check_vector_dimension(
2838        &self,
2839        expected: u32,
2840        found: u32,
2841        span: Span,
2842    ) -> Result<(), PlannerError> {
2843        if expected != found {
2844            Err(PlannerError::VectorDimensionMismatch {
2845                expected,
2846                found,
2847                line: span.start.line,
2848                column: span.start.column,
2849            })
2850        } else {
2851            Ok(())
2852        }
2853    }
2854
2855    // ============================================================
2856    // INSERT/UPDATE Type Checking Methods (Task 13)
2857    // ============================================================
2858
2859    /// Check INSERT values against table columns.
2860    ///
2861    /// Validates that:
2862    /// - The number of values matches the number of columns
2863    /// - Each value's type is compatible with the column type
2864    /// - NOT NULL constraints are satisfied
2865    /// - Vector dimensions match for vector columns
2866    ///
2867    /// # Column Order
2868    ///
2869    /// If `columns` is empty, uses `TableMetadata.column_names()` order (definition order).
2870    ///
2871    /// # Errors
2872    ///
2873    /// - `ColumnValueCountMismatch`: Number of values doesn't match columns
2874    /// - `TypeMismatch`: Value type incompatible with column type
2875    /// - `NullConstraintViolation`: NULL value for NOT NULL column
2876    /// - `VectorDimensionMismatch`: Vector dimension mismatch
2877    pub fn check_insert_values(
2878        &self,
2879        table: &TableMetadata,
2880        columns: &[String],
2881        values: &[Vec<Expr>],
2882        span: Span,
2883    ) -> Result<Vec<Vec<TypedExpr>>, PlannerError> {
2884        // Determine the target columns
2885        let target_columns: Vec<&str> = if columns.is_empty() {
2886            table.column_names()
2887        } else {
2888            columns.iter().map(|s| s.as_str()).collect()
2889        };
2890
2891        let mut typed_rows = Vec::with_capacity(values.len());
2892
2893        for row in values {
2894            // Check value count matches column count
2895            if row.len() != target_columns.len() {
2896                return Err(PlannerError::ColumnValueCountMismatch {
2897                    columns: target_columns.len(),
2898                    values: row.len(),
2899                    line: span.start.line,
2900                    column: span.start.column,
2901                });
2902            }
2903
2904            let mut typed_values = Vec::with_capacity(row.len());
2905
2906            for (value, col_name) in row.iter().zip(target_columns.iter()) {
2907                // Get column metadata
2908                let col_meta =
2909                    table
2910                        .get_column(col_name)
2911                        .ok_or_else(|| PlannerError::ColumnNotFound {
2912                            column: col_name.to_string(),
2913                            table: table.name.clone(),
2914                            line: span.start.line,
2915                            col: span.start.column,
2916                        })?;
2917
2918                // Type-check the value expression
2919                let typed_value = self.infer_type(value, table)?;
2920
2921                // Check NOT NULL constraint
2922                self.check_null_constraint(col_meta, &typed_value, value.span)?;
2923
2924                // Check type compatibility
2925                self.check_type_compatibility(
2926                    &col_meta.data_type,
2927                    &typed_value.resolved_type,
2928                    value.span,
2929                )?;
2930
2931                let typed_value =
2932                    self.coerce_column_value(&col_meta.data_type, typed_value, value.span);
2933
2934                // For vector types, also check dimension
2935                if let (
2936                    ResolvedType::Vector {
2937                        dimension: expected_dim,
2938                        ..
2939                    },
2940                    ResolvedType::Vector {
2941                        dimension: actual_dim,
2942                        ..
2943                    },
2944                ) = (&col_meta.data_type, &typed_value.resolved_type)
2945                {
2946                    self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
2947                }
2948
2949                typed_values.push(typed_value);
2950            }
2951
2952            typed_rows.push(typed_values);
2953        }
2954
2955        Ok(typed_rows)
2956    }
2957
2958    /// Check UPDATE assignment type compatibility.
2959    ///
2960    /// Validates that the value's type is compatible with the column type.
2961    ///
2962    /// # Errors
2963    ///
2964    /// - `ColumnNotFound`: Column doesn't exist
2965    /// - `TypeMismatch`: Value type incompatible with column type
2966    /// - `NullConstraintViolation`: NULL value for NOT NULL column
2967    /// - `VectorDimensionMismatch`: Vector dimension mismatch
2968    pub fn check_assignment(
2969        &self,
2970        table: &TableMetadata,
2971        column: &str,
2972        value: &Expr,
2973        span: Span,
2974    ) -> Result<TypedExpr, PlannerError> {
2975        // Get column metadata
2976        let col_meta = table
2977            .get_column(column)
2978            .ok_or_else(|| PlannerError::ColumnNotFound {
2979                column: column.to_string(),
2980                table: table.name.clone(),
2981                line: span.start.line,
2982                col: span.start.column,
2983            })?;
2984
2985        // Type-check the value expression
2986        let typed_value = self.infer_type(value, table)?;
2987
2988        // Check NOT NULL constraint
2989        self.check_null_constraint(col_meta, &typed_value, value.span)?;
2990
2991        // Check type compatibility
2992        self.check_type_compatibility(&col_meta.data_type, &typed_value.resolved_type, value.span)?;
2993
2994        let typed_value = self.coerce_column_value(&col_meta.data_type, typed_value, value.span);
2995
2996        // For vector types, also check dimension
2997        if let (
2998            ResolvedType::Vector {
2999                dimension: expected_dim,
3000                ..
3001            },
3002            ResolvedType::Vector {
3003                dimension: actual_dim,
3004                ..
3005            },
3006        ) = (&col_meta.data_type, &typed_value.resolved_type)
3007        {
3008            self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
3009        }
3010
3011        Ok(typed_value)
3012    }
3013
3014    /// Check NOT NULL constraint for a value.
3015    ///
3016    /// # Errors
3017    ///
3018    /// Returns `PlannerError::NullConstraintViolation` if the column has NOT NULL
3019    /// constraint and the value is NULL.
3020    pub fn check_null_constraint(
3021        &self,
3022        column: &crate::catalog::ColumnMetadata,
3023        value: &TypedExpr,
3024        span: Span,
3025    ) -> Result<(), PlannerError> {
3026        if column.not_null && matches!(value.resolved_type, ResolvedType::Null) {
3027            Err(PlannerError::NullConstraintViolation {
3028                column: column.name.clone(),
3029                line: span.start.line,
3030                col: span.start.column,
3031            })
3032        } else {
3033            Ok(())
3034        }
3035    }
3036
3037    /// Check type compatibility between expected and actual types.
3038    ///
3039    /// Uses implicit type conversion rules defined in `ResolvedType::can_cast_to`.
3040    ///
3041    /// # Errors
3042    ///
3043    /// Returns `PlannerError::TypeMismatch` if types are incompatible.
3044    fn check_type_compatibility(
3045        &self,
3046        expected: &ResolvedType,
3047        actual: &ResolvedType,
3048        span: Span,
3049    ) -> Result<(), PlannerError> {
3050        // Same type is always compatible
3051        if expected == actual {
3052            return Ok(());
3053        }
3054
3055        // Check if implicit cast is allowed
3056        if actual.can_cast_to(expected) {
3057            return Ok(());
3058        }
3059
3060        // Special case: Vector types with same dimension but different metric are compatible
3061        // (the column's metric is used)
3062        if let (
3063            ResolvedType::Vector {
3064                dimension: d1,
3065                metric: _,
3066            },
3067            ResolvedType::Vector {
3068                dimension: d2,
3069                metric: _,
3070            },
3071        ) = (expected, actual)
3072        {
3073            // Dimensions must match for vector compatibility
3074            if *d1 == *d2 {
3075                return Ok(());
3076            }
3077            // Different dimensions will fall through to TypeMismatch error
3078        }
3079
3080        Err(PlannerError::TypeMismatch {
3081            expected: expected.type_name().to_string(),
3082            found: actual.type_name().to_string(),
3083            line: span.start.line,
3084            column: span.start.column,
3085        })
3086    }
3087
3088    /// Insert an execution-time coercion where a column accepts a value whose
3089    /// source representation differs from its storage representation.
3090    fn coerce_column_value(
3091        &self,
3092        expected: &ResolvedType,
3093        value: TypedExpr,
3094        span: Span,
3095    ) -> TypedExpr {
3096        if value.resolved_type != *expected
3097            && value.resolved_type != ResolvedType::Null
3098            && matches!(
3099                expected,
3100                ResolvedType::Integer
3101                    | ResolvedType::BigInt
3102                    | ResolvedType::Float
3103                    | ResolvedType::Double
3104                    | ResolvedType::Timestamp
3105                    | ResolvedType::Date
3106                    | ResolvedType::Time
3107                    | ResolvedType::Interval
3108                    | ResolvedType::Decimal { .. }
3109            )
3110        {
3111            TypedExpr::cast(value, expected.clone(), span)
3112        } else {
3113            value
3114        }
3115    }
3116}
3117
3118fn fold_integral_binary(
3119    left: &TypedExpr,
3120    op: BinaryOp,
3121    right: &TypedExpr,
3122    result_type: &ResolvedType,
3123    span: Span,
3124) -> Option<TypedExpr> {
3125    let value = |expr: &TypedExpr| match &expr.kind {
3126        TypedExprKind::Literal(Literal::Number(value)) => value.parse::<i64>().ok(),
3127        _ => None,
3128    };
3129    let left = value(left)?;
3130    let right = value(right)?;
3131    let folded = match op {
3132        BinaryOp::BitAnd => left & right,
3133        BinaryOp::BitOr => left | right,
3134        BinaryOp::BitXor => left ^ right,
3135        BinaryOp::ShiftLeft | BinaryOp::ShiftRight => {
3136            let width = if matches!(result_type, ResolvedType::BigInt) {
3137                64
3138            } else {
3139                32
3140            };
3141            if !(0..width).contains(&right) {
3142                return None;
3143            }
3144            if op == BinaryOp::ShiftRight {
3145                left >> right as u32
3146            } else {
3147                let shifted = i128::from(left) * (1_i128 << right as u32);
3148                if matches!(result_type, ResolvedType::BigInt) {
3149                    i64::try_from(shifted).ok()?
3150                } else {
3151                    i64::from(i32::try_from(shifted).ok()?)
3152                }
3153            }
3154        }
3155        _ => return None,
3156    };
3157    Some(TypedExpr::literal(
3158        Literal::Number(folded.to_string()),
3159        result_type.clone(),
3160        span,
3161    ))
3162}
3163
3164fn is_numeric_type(ty: &ResolvedType) -> bool {
3165    matches!(
3166        ty,
3167        ResolvedType::Integer
3168            | ResolvedType::BigInt
3169            | ResolvedType::Float
3170            | ResolvedType::Double
3171            | ResolvedType::Decimal { .. }
3172    )
3173}
3174
3175fn decimal_arithmetic_type(
3176    op: BinaryOp,
3177    left: &ResolvedType,
3178    right: &ResolvedType,
3179) -> Option<ResolvedType> {
3180    let parts = |ty: &ResolvedType| match ty {
3181        ResolvedType::Decimal { precision, scale } => Some((*precision, *scale)),
3182        ResolvedType::Integer => Some((10, 0)),
3183        ResolvedType::BigInt => Some((19, 0)),
3184        _ => None,
3185    };
3186    if !matches!(left, ResolvedType::Decimal { .. })
3187        && !matches!(right, ResolvedType::Decimal { .. })
3188    {
3189        return None;
3190    }
3191    let (lp, ls) = parts(left)?;
3192    let (rp, rs) = parts(right)?;
3193    let (raw_precision, raw_scale) = match op {
3194        BinaryOp::Add | BinaryOp::Sub => ((lp - ls).max(rp - rs) + ls.max(rs) + 1, ls.max(rs)),
3195        BinaryOp::Mul => (
3196            lp.saturating_add(rp).saturating_add(1),
3197            ls.saturating_add(rs),
3198        ),
3199        BinaryOp::Div => {
3200            let scale = 6_u8.max(ls.saturating_add(rp).saturating_add(1));
3201            ((lp - ls).saturating_add(rs).saturating_add(scale), scale)
3202        }
3203        _ => return None,
3204    };
3205    let reduction = raw_precision.saturating_sub(38);
3206    Some(ResolvedType::Decimal {
3207        precision: raw_precision.min(38),
3208        scale: raw_scale.saturating_sub(reduction),
3209    })
3210}
3211
3212fn validate_window_frame(
3213    function_name: &str,
3214    frame: &WindowFrame,
3215    order_by: &[SortExpr],
3216) -> Result<(), PlannerError> {
3217    if !is_aggregate_name(function_name)
3218        && !matches!(function_name, "first_value" | "last_value" | "nth_value")
3219    {
3220        return Err(PlannerError::invalid_expression(format!(
3221            "explicit window frames are only supported for aggregate functions and \
3222             FIRST_VALUE/LAST_VALUE/NTH_VALUE, not {}()",
3223            function_name.to_ascii_uppercase()
3224        )));
3225    }
3226    if order_by.is_empty() {
3227        return Err(PlannerError::invalid_expression(
3228            "explicit ROWS/RANGE window frames require ORDER BY for deterministic evaluation",
3229        ));
3230    }
3231    if matches!(frame.start_bound, WindowFrameBound::UnboundedFollowing) {
3232        return Err(PlannerError::invalid_expression(
3233            "window frame start cannot be UNBOUNDED FOLLOWING",
3234        ));
3235    }
3236    if matches!(frame.end_bound, WindowFrameBound::UnboundedPreceding) {
3237        return Err(PlannerError::invalid_expression(
3238            "window frame end cannot be UNBOUNDED PRECEDING",
3239        ));
3240    }
3241    if (matches!(frame.start_bound, WindowFrameBound::CurrentRow)
3242        && matches!(frame.end_bound, WindowFrameBound::Preceding(_)))
3243        || (matches!(frame.start_bound, WindowFrameBound::Following(_))
3244            && matches!(
3245                frame.end_bound,
3246                WindowFrameBound::Preceding(_) | WindowFrameBound::CurrentRow
3247            ))
3248    {
3249        return Err(PlannerError::invalid_expression(
3250            "window frame bounds are reversed",
3251        ));
3252    }
3253
3254    let has_offset = matches!(
3255        frame.start_bound,
3256        WindowFrameBound::Preceding(_) | WindowFrameBound::Following(_)
3257    ) || matches!(
3258        frame.end_bound,
3259        WindowFrameBound::Preceding(_) | WindowFrameBound::Following(_)
3260    );
3261    if frame.units == WindowFrameUnits::Range && has_offset {
3262        if order_by.len() != 1 {
3263            return Err(PlannerError::invalid_expression(
3264                "RANGE offset frames require exactly one ORDER BY expression",
3265            ));
3266        }
3267        if !is_numeric_type(&order_by[0].expr.resolved_type) {
3268            return Err(PlannerError::invalid_expression(format!(
3269                "RANGE offset ORDER BY expression must be numeric, found {:?}",
3270                order_by[0].expr.resolved_type
3271            )));
3272        }
3273    }
3274    Ok(())
3275}
3276
3277fn validate_offset_window_call(
3278    name: &str,
3279    arg_count: usize,
3280    distinct: bool,
3281    star: bool,
3282) -> Result<(), PlannerError> {
3283    let display_name = name.to_ascii_uppercase();
3284    if distinct {
3285        return Err(PlannerError::invalid_expression(format!(
3286            "{display_name}() window function does not accept DISTINCT"
3287        )));
3288    }
3289    if star {
3290        return Err(PlannerError::invalid_expression(format!(
3291            "{display_name}() window function does not accept a star argument"
3292        )));
3293    }
3294    if !(1..=3).contains(&arg_count) {
3295        return Err(PlannerError::invalid_expression(format!(
3296            "{display_name}() window function expects 1 to 3 arguments"
3297        )));
3298    }
3299    Ok(())
3300}
3301
3302fn validate_exact_window_call(
3303    name: &str,
3304    arg_count: usize,
3305    expected: usize,
3306    distinct: bool,
3307    star: bool,
3308) -> Result<(), PlannerError> {
3309    let display_name = name.to_ascii_uppercase();
3310    if distinct {
3311        return Err(PlannerError::invalid_expression(format!(
3312            "{display_name}() window function does not support DISTINCT"
3313        )));
3314    }
3315    if star {
3316        return Err(PlannerError::invalid_expression(format!(
3317            "{display_name}() window function does not support a star argument"
3318        )));
3319    }
3320    if arg_count != expected {
3321        let signature = match expected {
3322            0 => "no arguments",
3323            1 => "one argument",
3324            2 => "two arguments",
3325            _ => unreachable!("window signatures are bounded above"),
3326        };
3327        return Err(PlannerError::invalid_expression(format!(
3328            "{display_name}() window function takes {signature}"
3329        )));
3330    }
3331    Ok(())
3332}
3333
3334fn validate_positive_integer_argument(
3335    name: &str,
3336    argument: &TypedExpr,
3337) -> Result<(), PlannerError> {
3338    if matches!(
3339        argument.resolved_type,
3340        ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Null
3341    ) {
3342        return Ok(());
3343    }
3344    Err(PlannerError::type_mismatch(
3345        format!("positive INTEGER {} argument", name.to_ascii_uppercase()),
3346        argument.resolved_type.type_name(),
3347        argument.span,
3348    ))
3349}
3350
3351fn coerce_compatible_result(expr: &mut TypedExpr, target: &ResolvedType) {
3352    if expr.resolved_type == *target || matches!(expr.resolved_type, ResolvedType::Null) {
3353        return;
3354    }
3355    let span = expr.span;
3356    *expr = TypedExpr::cast(expr.clone(), target.clone(), span);
3357}
3358
3359fn coerce_case_result(expr: &mut TypedExpr, target: &ResolvedType) {
3360    if expr.resolved_type == *target || matches!(expr.resolved_type, ResolvedType::Null) {
3361        return;
3362    }
3363    let span = expr.span;
3364    *expr = TypedExpr::cast(expr.clone(), target.clone(), span);
3365}
3366
3367#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3368struct AggregateSignature {
3369    name: String,
3370    distinct: bool,
3371    star: bool,
3372    arg_key: Option<String>,
3373    extra_arg_keys: Vec<String>,
3374    separator: Option<String>,
3375    /// FILTER (WHERE ...) predicate identity; aggregates that differ only in
3376    /// their filter are distinct physical aggregates (issue #148, D10).
3377    filter_key: Option<String>,
3378    /// Aggregate-local ordering identity. Populated only for order-sensitive
3379    /// aggregates so that a validated-then-discarded ORDER BY (D3) still
3380    /// deduplicates with the unordered call.
3381    order_key: Option<String>,
3382}
3383
3384pub(crate) fn is_portable_aggregate_name(name: &str) -> bool {
3385    matches!(
3386        name,
3387        "variance"
3388            | "var_samp"
3389            | "var_pop"
3390            | "stddev"
3391            | "stddev_samp"
3392            | "stddev_pop"
3393            | "covar_samp"
3394            | "covar_pop"
3395            | "corr"
3396            | "median"
3397            | "mode"
3398            | "quantile_cont"
3399            | "regr_count"
3400            | "regr_avgx"
3401            | "regr_avgy"
3402            | "regr_sxx"
3403            | "regr_syy"
3404            | "regr_sxy"
3405            | "regr_slope"
3406            | "regr_intercept"
3407            | "regr_r2"
3408            | "any_value"
3409            | "first"
3410            | "last"
3411            | "arg_min"
3412            | "min_by"
3413            | "arg_max"
3414            | "max_by"
3415            | "bit_and"
3416            | "bit_or"
3417            | "bit_xor"
3418            | "bool_and"
3419            | "bool_or"
3420    )
3421}
3422
3423fn canonical_aggregate_name(name: &str) -> String {
3424    match name.to_ascii_lowercase().as_str() {
3425        "variance" | "var_samp" => "var_samp".into(),
3426        "stddev" | "stddev_samp" => "stddev_samp".into(),
3427        "min_by" => "arg_min".into(),
3428        "max_by" => "arg_max".into(),
3429        lower => lower.into(),
3430    }
3431}
3432
3433fn check_portable_aggregate(
3434    name: &str,
3435    args: &[TypedExpr],
3436    distinct: bool,
3437    star: bool,
3438    span: Span,
3439) -> Result<ResolvedType, PlannerError> {
3440    if distinct || star {
3441        return Err(PlannerError::invalid_expression(format!(
3442            "{} does not support DISTINCT or *",
3443            name.to_ascii_uppercase()
3444        )));
3445    }
3446    let expected = if matches!(
3447        name,
3448        "covar_samp"
3449            | "covar_pop"
3450            | "corr"
3451            | "quantile_cont"
3452            | "regr_count"
3453            | "regr_avgx"
3454            | "regr_avgy"
3455            | "regr_sxx"
3456            | "regr_syy"
3457            | "regr_sxy"
3458            | "regr_slope"
3459            | "regr_intercept"
3460            | "regr_r2"
3461            | "arg_min"
3462            | "min_by"
3463            | "arg_max"
3464            | "max_by"
3465    ) {
3466        2
3467    } else {
3468        1
3469    };
3470    if args.len() != expected {
3471        return Err(PlannerError::type_mismatch(
3472            format!("{expected} argument(s)"),
3473            format!("{} arguments", args.len()),
3474            span,
3475        ));
3476    }
3477
3478    let numeric = |arg: &TypedExpr| {
3479        matches!(
3480            arg.resolved_type,
3481            ResolvedType::Integer
3482                | ResolvedType::BigInt
3483                | ResolvedType::Float
3484                | ResolvedType::Double
3485                | ResolvedType::Null
3486        )
3487    };
3488    if matches!(
3489        name,
3490        "variance"
3491            | "var_samp"
3492            | "var_pop"
3493            | "stddev"
3494            | "stddev_samp"
3495            | "stddev_pop"
3496            | "median"
3497            | "quantile_cont"
3498            | "covar_samp"
3499            | "covar_pop"
3500            | "corr"
3501            | "regr_count"
3502            | "regr_avgx"
3503            | "regr_avgy"
3504            | "regr_sxx"
3505            | "regr_syy"
3506            | "regr_sxy"
3507            | "regr_slope"
3508            | "regr_intercept"
3509            | "regr_r2"
3510    ) && !args.iter().all(numeric)
3511    {
3512        return Err(PlannerError::type_mismatch(
3513            "numeric aggregate argument",
3514            args.iter()
3515                .find(|arg| !numeric(arg))
3516                .expect("non-numeric argument")
3517                .resolved_type
3518                .type_name(),
3519            span,
3520        ));
3521    }
3522    if name == "quantile_cont" {
3523        let _ = percentile_fraction_named(name, &args[1])?;
3524    }
3525    if matches!(name, "bit_and" | "bit_or" | "bit_xor")
3526        && !matches!(
3527            args[0].resolved_type,
3528            ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Null
3529        )
3530    {
3531        return Err(PlannerError::type_mismatch(
3532            "INTEGER or BIGINT",
3533            args[0].resolved_type.type_name(),
3534            span,
3535        ));
3536    }
3537    if matches!(name, "bool_and" | "bool_or")
3538        && !matches!(
3539            args[0].resolved_type,
3540            ResolvedType::Boolean | ResolvedType::Null
3541        )
3542    {
3543        return Err(PlannerError::type_mismatch(
3544            "BOOLEAN",
3545            args[0].resolved_type.type_name(),
3546            span,
3547        ));
3548    }
3549
3550    Ok(match name {
3551        "regr_count" => ResolvedType::BigInt,
3552        "any_value" | "first" | "last" | "arg_min" | "min_by" | "arg_max" | "max_by" | "mode" => {
3553            args[0].resolved_type.clone()
3554        }
3555        "bit_and" | "bit_or" | "bit_xor" => args[0].resolved_type.clone(),
3556        "bool_and" | "bool_or" => ResolvedType::Boolean,
3557        _ => ResolvedType::Double,
3558    })
3559}
3560
3561fn is_aggregate_name(name: &str) -> bool {
3562    matches!(
3563        name.to_ascii_lowercase().as_str(),
3564        "count"
3565            | "sum"
3566            | "total"
3567            | "avg"
3568            | "min"
3569            | "max"
3570            | "group_concat"
3571            | "string_agg"
3572            | "json_group_array"
3573            | "array_agg"
3574            | "json_group_object"
3575            | "jsonb_agg"
3576            | "jsonb_object_agg"
3577            | "percentile_disc"
3578            | "percentile_cont"
3579            | "variance"
3580            | "var_samp"
3581            | "var_pop"
3582            | "stddev"
3583            | "stddev_samp"
3584            | "stddev_pop"
3585            | "covar_samp"
3586            | "covar_pop"
3587            | "corr"
3588            | "median"
3589            | "mode"
3590            | "quantile_cont"
3591            | "regr_count"
3592            | "regr_avgx"
3593            | "regr_avgy"
3594            | "regr_sxx"
3595            | "regr_syy"
3596            | "regr_sxy"
3597            | "regr_slope"
3598            | "regr_intercept"
3599            | "regr_r2"
3600            | "any_value"
3601            | "first"
3602            | "last"
3603            | "arg_min"
3604            | "min_by"
3605            | "arg_max"
3606            | "max_by"
3607            | "bit_and"
3608            | "bit_or"
3609            | "bit_xor"
3610            | "bool_and"
3611            | "bool_or"
3612    )
3613}
3614
3615fn is_ordered_set_aggregate_name(name: &str) -> bool {
3616    matches!(name, "percentile_disc" | "percentile_cont" | "mode")
3617}
3618
3619/// Order identity participates in the signature only where ordering changes
3620/// the result (D3): order-insensitive aggregates discard their validated
3621/// ORDER BY, and their signature must match the unordered spelling.
3622fn is_order_sensitive_aggregate_name(name: &str) -> bool {
3623    matches!(
3624        name.to_ascii_lowercase().as_str(),
3625        "group_concat"
3626            | "string_agg"
3627            | "percentile_disc"
3628            | "percentile_cont"
3629            | "mode"
3630            | "first"
3631            | "last"
3632    )
3633}
3634
3635/// Extract and validate the `PERCENTILE_DISC` fraction literal (D5): a
3636/// numeric literal (optionally negated) inside `[0, 1]`.
3637pub(crate) fn percentile_fraction(arg: &TypedExpr) -> Result<f64, PlannerError> {
3638    percentile_fraction_named("percentile_disc", arg)
3639}
3640
3641pub(crate) fn percentile_fraction_named(name: &str, arg: &TypedExpr) -> Result<f64, PlannerError> {
3642    let literal = match &arg.kind {
3643        TypedExprKind::Literal(Literal::Number(text)) => text.parse::<f64>().ok(),
3644        TypedExprKind::UnaryOp {
3645            op: crate::ast::expr::UnaryOp::Minus,
3646            operand,
3647        } => match &operand.kind {
3648            TypedExprKind::Literal(Literal::Number(text)) => {
3649                text.parse::<f64>().ok().map(|value| -value)
3650            }
3651            _ => None,
3652        },
3653        _ => None,
3654    };
3655    let Some(value) = literal else {
3656        return Err(PlannerError::invalid_expression(format!(
3657            "{} fraction must be a numeric literal",
3658            name.to_ascii_uppercase()
3659        )));
3660    };
3661    if !(0.0..=1.0).contains(&value) {
3662        return Err(PlannerError::invalid_expression(format!(
3663            "{} fraction must be between 0 and 1",
3664            name.to_ascii_uppercase()
3665        )));
3666    }
3667    Ok(value)
3668}
3669
3670fn typed_sort_signature(order_by: &[SortExpr]) -> Option<String> {
3671    if order_by.is_empty() {
3672        return None;
3673    }
3674    Some(
3675        order_by
3676            .iter()
3677            .map(|sort| {
3678                format!(
3679                    "{}|{}|{}",
3680                    typed_expr_signature(&sort.expr),
3681                    sort.asc,
3682                    sort.nulls_first
3683                )
3684            })
3685            .collect::<Vec<_>>()
3686            .join(","),
3687    )
3688}
3689
3690fn aggregate_signature_from_expr(expr: &AggregateExpr) -> AggregateSignature {
3691    let (name, separator, star, arg) = match &expr.function {
3692        AggregateFunction::Count => (
3693            "count".to_string(),
3694            None,
3695            expr.arg.is_none(),
3696            expr.arg.as_ref(),
3697        ),
3698        AggregateFunction::Sum => ("sum".to_string(), None, false, expr.arg.as_ref()),
3699        AggregateFunction::Total => ("total".to_string(), None, false, expr.arg.as_ref()),
3700        AggregateFunction::Avg => ("avg".to_string(), None, false, expr.arg.as_ref()),
3701        AggregateFunction::Min => ("min".to_string(), None, false, expr.arg.as_ref()),
3702        AggregateFunction::Max => ("max".to_string(), None, false, expr.arg.as_ref()),
3703        AggregateFunction::GroupConcat { separator } => (
3704            "group_concat".to_string(),
3705            separator.clone(),
3706            false,
3707            expr.arg.as_ref(),
3708        ),
3709        AggregateFunction::StringAgg { separator } => (
3710            "string_agg".to_string(),
3711            separator.clone(),
3712            false,
3713            expr.arg.as_ref(),
3714        ),
3715        AggregateFunction::ArrayAgg => ("array_agg".to_string(), None, false, expr.arg.as_ref()),
3716        AggregateFunction::JsonGroupArray => (
3717            "json_group_array".to_string(),
3718            None,
3719            false,
3720            expr.arg.as_ref(),
3721        ),
3722        AggregateFunction::JsonGroupObject => (
3723            "json_group_object".to_string(),
3724            None,
3725            false,
3726            expr.arg.as_ref(),
3727        ),
3728        AggregateFunction::JsonbAgg => ("jsonb_agg".to_string(), None, false, expr.arg.as_ref()),
3729        AggregateFunction::JsonbObjectAgg => (
3730            "jsonb_object_agg".to_string(),
3731            None,
3732            false,
3733            expr.arg.as_ref(),
3734        ),
3735        // The sort value lives in `order_key`; the fraction rides the
3736        // separator slot so both signature constructions stay symmetric.
3737        AggregateFunction::PercentileDisc { fraction } => (
3738            "percentile_disc".to_string(),
3739            Some(format!("{fraction:?}")),
3740            false,
3741            None,
3742        ),
3743        AggregateFunction::PercentileCont { fraction } => (
3744            "percentile_cont".to_string(),
3745            Some(format!("{fraction:?}")),
3746            false,
3747            None,
3748        ),
3749        AggregateFunction::QuantileCont { fraction } => (
3750            "quantile_cont".to_string(),
3751            Some(format!("{fraction:?}")),
3752            false,
3753            expr.arg.as_ref(),
3754        ),
3755        AggregateFunction::Variance { sample } => (
3756            if *sample { "var_samp" } else { "var_pop" }.to_string(),
3757            None,
3758            false,
3759            expr.arg.as_ref(),
3760        ),
3761        AggregateFunction::Stddev { sample } => (
3762            if *sample { "stddev_samp" } else { "stddev_pop" }.to_string(),
3763            None,
3764            false,
3765            expr.arg.as_ref(),
3766        ),
3767        AggregateFunction::Covariance { sample } => (
3768            if *sample { "covar_samp" } else { "covar_pop" }.to_string(),
3769            None,
3770            false,
3771            expr.arg.as_ref(),
3772        ),
3773        AggregateFunction::Corr => ("corr".into(), None, false, expr.arg.as_ref()),
3774        AggregateFunction::Median => ("median".into(), None, false, expr.arg.as_ref()),
3775        AggregateFunction::Mode => (
3776            "mode".into(),
3777            None,
3778            false,
3779            expr.order_by
3780                .is_empty()
3781                .then_some(())
3782                .and(expr.arg.as_ref()),
3783        ),
3784        AggregateFunction::RegrCount => ("regr_count".into(), None, false, expr.arg.as_ref()),
3785        AggregateFunction::RegrAvgX => ("regr_avgx".into(), None, false, expr.arg.as_ref()),
3786        AggregateFunction::RegrAvgY => ("regr_avgy".into(), None, false, expr.arg.as_ref()),
3787        AggregateFunction::RegrSxx => ("regr_sxx".into(), None, false, expr.arg.as_ref()),
3788        AggregateFunction::RegrSyy => ("regr_syy".into(), None, false, expr.arg.as_ref()),
3789        AggregateFunction::RegrSxy => ("regr_sxy".into(), None, false, expr.arg.as_ref()),
3790        AggregateFunction::RegrSlope => ("regr_slope".into(), None, false, expr.arg.as_ref()),
3791        AggregateFunction::RegrIntercept => {
3792            ("regr_intercept".into(), None, false, expr.arg.as_ref())
3793        }
3794        AggregateFunction::RegrR2 => ("regr_r2".into(), None, false, expr.arg.as_ref()),
3795        AggregateFunction::AnyValue => ("any_value".into(), None, false, expr.arg.as_ref()),
3796        AggregateFunction::First => ("first".into(), None, false, expr.arg.as_ref()),
3797        AggregateFunction::Last => ("last".into(), None, false, expr.arg.as_ref()),
3798        AggregateFunction::ArgMin => ("arg_min".into(), None, false, expr.arg.as_ref()),
3799        AggregateFunction::ArgMax => ("arg_max".into(), None, false, expr.arg.as_ref()),
3800        AggregateFunction::BitAnd => ("bit_and".into(), None, false, expr.arg.as_ref()),
3801        AggregateFunction::BitOr => ("bit_or".into(), None, false, expr.arg.as_ref()),
3802        AggregateFunction::BitXor => ("bit_xor".into(), None, false, expr.arg.as_ref()),
3803        AggregateFunction::BoolAnd => ("bool_and".into(), None, false, expr.arg.as_ref()),
3804        AggregateFunction::BoolOr => ("bool_or".into(), None, false, expr.arg.as_ref()),
3805    };
3806    AggregateSignature {
3807        name,
3808        distinct: expr.distinct,
3809        star,
3810        arg_key: arg.map(typed_expr_signature),
3811        extra_arg_keys: expr.extra_args.iter().map(typed_expr_signature).collect(),
3812        separator,
3813        filter_key: expr.filter.as_ref().map(typed_expr_signature),
3814        order_key: typed_sort_signature(&expr.order_by),
3815    }
3816}
3817
3818fn aggregate_signature_from_call(
3819    name: &str,
3820    args: &[TypedExpr],
3821    distinct: bool,
3822    star: bool,
3823    filter: Option<&TypedExpr>,
3824    order_by: &[SortExpr],
3825) -> Result<AggregateSignature, PlannerError> {
3826    let lower = name.to_ascii_lowercase();
3827    let is_percentile = matches!(lower.as_str(), "percentile_disc" | "percentile_cont");
3828    let separator = if name.eq_ignore_ascii_case("group_concat") && args.len() == 2 {
3829        if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
3830            Some(value.clone())
3831        } else {
3832            return Err(PlannerError::invalid_expression(
3833                "GROUP_CONCAT separator must be a string literal".to_string(),
3834            ));
3835        }
3836    } else if name.eq_ignore_ascii_case("string_agg") && args.len() == 2 {
3837        if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
3838            Some(value.clone())
3839        } else {
3840            return Err(PlannerError::invalid_expression(
3841                "STRING_AGG separator must be a string literal".to_string(),
3842            ));
3843        }
3844    } else if is_percentile && args.len() == 1 {
3845        Some(format!(
3846            "{:?}",
3847            percentile_fraction_named(&lower, &args[0])?
3848        ))
3849    } else if lower == "quantile_cont" && args.len() == 2 {
3850        Some(format!(
3851            "{:?}",
3852            percentile_fraction_named(&lower, &args[1])?
3853        ))
3854    } else {
3855        None
3856    };
3857    Ok(AggregateSignature {
3858        name: canonical_aggregate_name(name),
3859        distinct,
3860        star,
3861        arg_key: if is_percentile {
3862            None
3863        } else {
3864            args.first().map(typed_expr_signature)
3865        },
3866        extra_arg_keys: if matches!(
3867            lower.as_str(),
3868            "group_concat" | "string_agg" | "percentile_disc" | "percentile_cont" | "quantile_cont"
3869        ) {
3870            Vec::new()
3871        } else {
3872            args.iter().skip(1).map(typed_expr_signature).collect()
3873        },
3874        separator,
3875        filter_key: filter.map(typed_expr_signature),
3876        order_key: if is_order_sensitive_aggregate_name(name) {
3877            typed_sort_signature(order_by)
3878        } else {
3879            None
3880        },
3881    })
3882}
3883
3884fn typed_expr_signature(expr: &TypedExpr) -> String {
3885    format!("{:?}", expr.kind)
3886}
3887
3888fn single_column_type(schema: &[ColumnMetadata], span: Span) -> Result<ResolvedType, PlannerError> {
3889    match schema {
3890        [column] => Ok(column.data_type.clone()),
3891        [] => Err(PlannerError::type_mismatch(
3892            "one-column subquery",
3893            "zero-column subquery",
3894            span,
3895        )),
3896        _ => Err(PlannerError::type_mismatch(
3897            "one-column subquery",
3898            format!("{} columns", schema.len()),
3899            span,
3900        )),
3901    }
3902}
3903
3904fn row_items(expr: &Expr) -> Option<&[Expr]> {
3905    match &expr.kind {
3906        ExprKind::Row { items } => Some(items),
3907        _ => None,
3908    }
3909}
3910
3911fn internal_predicate(name: String, args: Vec<TypedExpr>, span: Span) -> TypedExpr {
3912    TypedExpr::function_call(name, args, false, false, ResolvedType::Boolean, span)
3913}
3914
3915// Tests are in type_checker/tests.rs
3916#[cfg(test)]
3917#[path = "type_checker/tests.rs"]
3918mod tests;