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