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        Ok(TypedExpr {
639            kind: TypedExprKind::BinaryOp {
640                left: Box::new(left_typed),
641                op,
642                right: Box::new(right_typed),
643            },
644            resolved_type: result_type,
645            span,
646        })
647    }
648
649    fn infer_binary_op_type_with_scope(
650        &self,
651        left: &Expr,
652        op: BinaryOp,
653        right: &Expr,
654        scope: &[ScopedTable],
655        plan_subquery: &SubqueryPlanner<'_>,
656        span: Span,
657    ) -> Result<TypedExpr, PlannerError> {
658        if row_items(left).is_some() || row_items(right).is_some() {
659            let internal = match op {
660                BinaryOp::Eq => INTERNAL_ROW_EQ,
661                BinaryOp::Neq => INTERNAL_ROW_NEQ,
662                BinaryOp::Lt => INTERNAL_ROW_LT,
663                BinaryOp::LtEq => INTERNAL_ROW_LTEQ,
664                BinaryOp::Gt => INTERNAL_ROW_GT,
665                BinaryOp::GtEq => INTERNAL_ROW_GTEQ,
666                _ => {
667                    return Err(PlannerError::invalid_operator(
668                        format!("{op:?}"),
669                        "Row",
670                        span,
671                    ));
672                }
673            };
674            let (mut left, right, width) =
675                self.infer_row_pair_with_scope(left, right, scope, plan_subquery, span)?;
676            left.extend(right);
677            return Ok(internal_predicate(
678                format!("{internal}:{width}"),
679                left,
680                span,
681            ));
682        }
683
684        let left_typed = self.infer_type_with_scope(left, scope, plan_subquery)?;
685        let right_typed = self.infer_type_with_scope(right, scope, plan_subquery)?;
686
687        let result_type = self.check_binary_op(
688            op,
689            &left_typed.resolved_type,
690            &right_typed.resolved_type,
691            span,
692        )?;
693
694        Ok(TypedExpr {
695            kind: TypedExprKind::BinaryOp {
696                left: Box::new(left_typed),
697                op,
698                right: Box::new(right_typed),
699            },
700            resolved_type: result_type,
701            span,
702        })
703    }
704
705    fn infer_case_type_with_scope(
706        &self,
707        operand: Option<&Expr>,
708        branches: &[crate::ast::expr::CaseWhen],
709        else_expr: Option<&Expr>,
710        scope: &[ScopedTable],
711        plan_subquery: &SubqueryPlanner<'_>,
712        span: Span,
713    ) -> Result<TypedExpr, PlannerError> {
714        if branches.is_empty() {
715            return Err(PlannerError::invalid_expression(
716                "CASE expression requires at least one WHEN branch",
717            ));
718        }
719        let typed_operand = operand
720            .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
721            .transpose()?;
722        let mut typed_branches = Vec::with_capacity(branches.len());
723        let mut result_type = ResolvedType::Null;
724
725        for branch in branches {
726            let condition = self.infer_type_with_scope(&branch.when, scope, plan_subquery)?;
727            if let Some(operand) = &typed_operand {
728                self.check_comparison_op(
729                    &operand.resolved_type,
730                    &condition.resolved_type,
731                    condition.span,
732                )?;
733            } else if !matches!(
734                condition.resolved_type,
735                ResolvedType::Boolean | ResolvedType::Null
736            ) {
737                return Err(PlannerError::type_mismatch(
738                    "Boolean",
739                    condition.resolved_type.type_name(),
740                    condition.span,
741                ));
742            }
743
744            let result = self.infer_type_with_scope(&branch.then, scope, plan_subquery)?;
745            result_type =
746                self.common_case_result_type(&result_type, &result.resolved_type, result.span)?;
747            typed_branches.push(TypedCaseWhen {
748                when: condition,
749                then: result,
750            });
751        }
752
753        let mut typed_else = else_expr
754            .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
755            .transpose()?;
756        if let Some(else_expr) = &typed_else {
757            result_type = self.common_case_result_type(
758                &result_type,
759                &else_expr.resolved_type,
760                else_expr.span,
761            )?;
762        }
763
764        for branch in &mut typed_branches {
765            coerce_case_result(&mut branch.then, &result_type);
766        }
767        if let Some(else_expr) = &mut typed_else {
768            coerce_case_result(else_expr, &result_type);
769        }
770
771        Ok(TypedExpr {
772            kind: TypedExprKind::Case {
773                operand: typed_operand.map(Box::new),
774                branches: typed_branches,
775                else_expr: typed_else.map(Box::new),
776            },
777            resolved_type: result_type,
778            span,
779        })
780    }
781
782    fn common_case_result_type(
783        &self,
784        current: &ResolvedType,
785        next: &ResolvedType,
786        span: Span,
787    ) -> Result<ResolvedType, PlannerError> {
788        if matches!(current, ResolvedType::Null) {
789            return Ok(next.clone());
790        }
791        if matches!(next, ResolvedType::Null) || current == next {
792            return Ok(current.clone());
793        }
794        if is_numeric_type(current) && is_numeric_type(next) {
795            return self.check_arithmetic_op(current, next, span);
796        }
797        Err(PlannerError::type_mismatch(
798            current.type_name(),
799            next.type_name(),
800            span,
801        ))
802    }
803
804    /// Check binary operation and return the result type.
805    ///
806    /// Validates that the operator is valid for the given operand types
807    /// and returns the result type.
808    ///
809    /// # Type Rules
810    ///
811    /// - Arithmetic operators (+, -, *, /, %): Require numeric operands
812    /// - Comparison operators (=, <>, <, >, <=, >=): Require compatible types
813    /// - Logical operators (AND, OR): Require boolean operands
814    /// - String concatenation (||): Requires text operands
815    pub fn check_binary_op(
816        &self,
817        op: BinaryOp,
818        left: &ResolvedType,
819        right: &ResolvedType,
820        span: Span,
821    ) -> Result<ResolvedType, PlannerError> {
822        use BinaryOp::*;
823        use ResolvedType::*;
824
825        match op {
826            // Arithmetic operators: require numeric types
827            Add | Sub | Mul | Div => {
828                let result = self.check_arithmetic_op(left, right, span)?;
829                Ok(result)
830            }
831
832            // Remainder is defined only for integral operands.
833            Mod => self.check_modulo_op(left, right, span),
834
835            // Comparison operators: require compatible types, return boolean
836            Eq | Neq | Lt | Gt | LtEq | GtEq => {
837                self.check_comparison_op(left, right, span)?;
838                Ok(Boolean)
839            }
840
841            // Logical operators: require boolean types
842            And | Or => {
843                self.check_logical_op(left, right, span)?;
844                Ok(Boolean)
845            }
846
847            // String concatenation: requires text types
848            StringConcat => {
849                self.check_string_concat_op(left, right, span)?;
850                Ok(Text)
851            }
852        }
853    }
854
855    /// Check arithmetic operation and return the result type.
856    fn check_arithmetic_op(
857        &self,
858        left: &ResolvedType,
859        right: &ResolvedType,
860        span: Span,
861    ) -> Result<ResolvedType, PlannerError> {
862        use ResolvedType::*;
863
864        // Handle NULL propagation
865        if matches!(left, Null) || matches!(right, Null) {
866            return Ok(Null);
867        }
868
869        // Determine result type based on numeric type hierarchy
870        match (left, right) {
871            // Integer operations
872            (Integer, Integer) => Ok(Integer),
873            (Integer, BigInt) | (BigInt, Integer) | (BigInt, BigInt) => Ok(BigInt),
874            (Float, Float) => Ok(Float),
875            // f32 has 24 bits of mantissa and cannot hold the whole i32 range,
876            // so an INTEGER mixed with FLOAT widens to DOUBLE.
877            (Integer, Float)
878            | (Float, Integer)
879            | (Integer, Double)
880            | (Double, Integer)
881            | (BigInt, Float)
882            | (Float, BigInt)
883            | (BigInt, Double)
884            | (Double, BigInt)
885            | (Float, Double)
886            | (Double, Float)
887            | (Double, Double) => Ok(Double),
888
889            _ => Err(PlannerError::InvalidOperator {
890                op: "arithmetic".to_string(),
891                type_name: format!("{} and {}", left.type_name(), right.type_name()),
892                line: span.start.line,
893                column: span.start.column,
894            }),
895        }
896    }
897
898    /// Check remainder operands and return the integral result type.
899    fn check_modulo_op(
900        &self,
901        left: &ResolvedType,
902        right: &ResolvedType,
903        span: Span,
904    ) -> Result<ResolvedType, PlannerError> {
905        use ResolvedType::*;
906
907        if matches!(left, Null) || matches!(right, Null) {
908            return Ok(Null);
909        }
910
911        match (left, right) {
912            (Integer, Integer) => Ok(Integer),
913            (Integer, BigInt) | (BigInt, Integer) | (BigInt, BigInt) => Ok(BigInt),
914            _ => Err(PlannerError::InvalidOperator {
915                op: "modulo".to_string(),
916                type_name: format!("{} and {}", left.type_name(), right.type_name()),
917                line: span.start.line,
918                column: span.start.column,
919            }),
920        }
921    }
922
923    /// Check comparison operation for compatible types.
924    pub(crate) fn check_comparison_op(
925        &self,
926        left: &ResolvedType,
927        right: &ResolvedType,
928        span: Span,
929    ) -> Result<(), PlannerError> {
930        use ResolvedType::*;
931
932        // NULL can be compared with anything
933        if matches!(left, Null) || matches!(right, Null) {
934            return Ok(());
935        }
936
937        // Check type compatibility
938        let compatible = match (left, right) {
939            // Same types are always comparable
940            (a, b) if a == b => true,
941
942            // Numeric types are comparable with each other
943            (Integer | BigInt | Float | Double, Integer | BigInt | Float | Double) => true,
944
945            // Text types
946            (Text, Text) => true,
947
948            // Boolean types
949            (Boolean, Boolean) => true,
950
951            // Timestamp types
952            (Timestamp, Timestamp) => true,
953
954            // Vector types (for equality only, dimension must match)
955            (Vector { dimension: d1, .. }, Vector { dimension: d2, .. }) => d1 == d2,
956
957            _ => false,
958        };
959
960        if compatible {
961            Ok(())
962        } else {
963            Err(PlannerError::TypeMismatch {
964                expected: left.type_name().to_string(),
965                found: right.type_name().to_string(),
966                line: span.start.line,
967                column: span.start.column,
968            })
969        }
970    }
971
972    /// Check logical operation for boolean types.
973    fn check_logical_op(
974        &self,
975        left: &ResolvedType,
976        right: &ResolvedType,
977        span: Span,
978    ) -> Result<(), PlannerError> {
979        use ResolvedType::*;
980
981        // NULL is allowed (three-valued logic)
982        let left_ok = matches!(left, Boolean | Null);
983        let right_ok = matches!(right, Boolean | Null);
984
985        if !left_ok {
986            return Err(PlannerError::TypeMismatch {
987                expected: "Boolean".to_string(),
988                found: left.type_name().to_string(),
989                line: span.start.line,
990                column: span.start.column,
991            });
992        }
993
994        if !right_ok {
995            return Err(PlannerError::TypeMismatch {
996                expected: "Boolean".to_string(),
997                found: right.type_name().to_string(),
998                line: span.start.line,
999                column: span.start.column,
1000            });
1001        }
1002
1003        Ok(())
1004    }
1005
1006    /// Check string concatenation operation.
1007    fn check_string_concat_op(
1008        &self,
1009        left: &ResolvedType,
1010        right: &ResolvedType,
1011        span: Span,
1012    ) -> Result<(), PlannerError> {
1013        use ResolvedType::*;
1014
1015        // NULL is allowed
1016        let left_ok = matches!(left, Text | Null);
1017        let right_ok = matches!(right, Text | Null);
1018
1019        if !left_ok {
1020            return Err(PlannerError::TypeMismatch {
1021                expected: "Text".to_string(),
1022                found: left.type_name().to_string(),
1023                line: span.start.line,
1024                column: span.start.column,
1025            });
1026        }
1027
1028        if !right_ok {
1029            return Err(PlannerError::TypeMismatch {
1030                expected: "Text".to_string(),
1031                found: right.type_name().to_string(),
1032                line: span.start.line,
1033                column: span.start.column,
1034            });
1035        }
1036
1037        Ok(())
1038    }
1039
1040    /// Infer the type of a unary operation.
1041    #[allow(dead_code)]
1042    fn infer_unary_op_type(
1043        &self,
1044        op: UnaryOp,
1045        operand: &Expr,
1046        table: &TableMetadata,
1047        span: Span,
1048    ) -> Result<TypedExpr, PlannerError> {
1049        let operand_typed = self.infer_type(operand, table)?;
1050
1051        let result_type = match op {
1052            UnaryOp::Not => {
1053                // NOT requires boolean operand
1054                if !matches!(
1055                    operand_typed.resolved_type,
1056                    ResolvedType::Boolean | ResolvedType::Null
1057                ) {
1058                    return Err(PlannerError::TypeMismatch {
1059                        expected: "Boolean".to_string(),
1060                        found: operand_typed.resolved_type.type_name().to_string(),
1061                        line: span.start.line,
1062                        column: span.start.column,
1063                    });
1064                }
1065                ResolvedType::Boolean
1066            }
1067            UnaryOp::Minus => {
1068                // Unary minus requires numeric operand
1069                match &operand_typed.resolved_type {
1070                    ResolvedType::Integer => ResolvedType::Integer,
1071                    ResolvedType::BigInt => ResolvedType::BigInt,
1072                    ResolvedType::Float => ResolvedType::Float,
1073                    ResolvedType::Double => ResolvedType::Double,
1074                    ResolvedType::Null => ResolvedType::Null,
1075                    other => {
1076                        return Err(PlannerError::InvalidOperator {
1077                            op: "unary minus".to_string(),
1078                            type_name: other.type_name().to_string(),
1079                            line: span.start.line,
1080                            column: span.start.column,
1081                        });
1082                    }
1083                }
1084            }
1085        };
1086
1087        Ok(TypedExpr {
1088            kind: TypedExprKind::UnaryOp {
1089                op,
1090                operand: Box::new(operand_typed),
1091            },
1092            resolved_type: result_type,
1093            span,
1094        })
1095    }
1096
1097    fn infer_unary_op_type_with_scope(
1098        &self,
1099        op: UnaryOp,
1100        operand: &Expr,
1101        scope: &[ScopedTable],
1102        plan_subquery: &SubqueryPlanner<'_>,
1103        span: Span,
1104    ) -> Result<TypedExpr, PlannerError> {
1105        let operand_typed = self.infer_type_with_scope(operand, scope, plan_subquery)?;
1106
1107        let result_type = match op {
1108            UnaryOp::Not => {
1109                if !matches!(
1110                    operand_typed.resolved_type,
1111                    ResolvedType::Boolean | ResolvedType::Null
1112                ) {
1113                    return Err(PlannerError::TypeMismatch {
1114                        expected: "Boolean".to_string(),
1115                        found: operand_typed.resolved_type.type_name().to_string(),
1116                        line: span.start.line,
1117                        column: span.start.column,
1118                    });
1119                }
1120                ResolvedType::Boolean
1121            }
1122            UnaryOp::Minus => match &operand_typed.resolved_type {
1123                ResolvedType::Integer => ResolvedType::Integer,
1124                ResolvedType::BigInt => ResolvedType::BigInt,
1125                ResolvedType::Float => ResolvedType::Float,
1126                ResolvedType::Double => ResolvedType::Double,
1127                ResolvedType::Null => ResolvedType::Null,
1128                other => {
1129                    return Err(PlannerError::InvalidOperator {
1130                        op: "unary minus".to_string(),
1131                        type_name: other.type_name().to_string(),
1132                        line: span.start.line,
1133                        column: span.start.column,
1134                    });
1135                }
1136            },
1137        };
1138
1139        Ok(TypedExpr {
1140            kind: TypedExprKind::UnaryOp {
1141                op,
1142                operand: Box::new(operand_typed),
1143            },
1144            resolved_type: result_type,
1145            span,
1146        })
1147    }
1148
1149    /// Infer the type of a function call.
1150    #[allow(dead_code)]
1151    fn infer_function_call_type(
1152        &self,
1153        name: &str,
1154        args: &[Expr],
1155        distinct: bool,
1156        star: bool,
1157        table: &TableMetadata,
1158        span: Span,
1159    ) -> Result<TypedExpr, PlannerError> {
1160        // Type-check all arguments first
1161        let typed_args: Vec<TypedExpr> = args
1162            .iter()
1163            .map(|arg| self.infer_type(arg, table))
1164            .collect::<Result<Vec<_>, _>>()?;
1165
1166        // Delegate to check_function_call for validation and return type
1167        let result_type = self.check_function_call(name, &typed_args, distinct, star, span)?;
1168
1169        Ok(TypedExpr {
1170            kind: TypedExprKind::FunctionCall {
1171                name: name.to_string(),
1172                args: typed_args,
1173                distinct,
1174                star,
1175                filter: None,
1176                order_by: Vec::new(),
1177                over: None,
1178            },
1179            resolved_type: result_type,
1180            span,
1181        })
1182    }
1183
1184    #[allow(clippy::too_many_arguments)]
1185    fn infer_function_call_type_with_scope(
1186        &self,
1187        name: &str,
1188        args: &[Expr],
1189        distinct: bool,
1190        star: bool,
1191        order_by: &[crate::ast::dml::OrderByExpr],
1192        within_group: &[crate::ast::dml::OrderByExpr],
1193        filter: Option<&Expr>,
1194        over: Option<&WindowSpec>,
1195        scope: &[ScopedTable],
1196        plan_subquery: &SubqueryPlanner<'_>,
1197        span: Span,
1198    ) -> Result<TypedExpr, PlannerError> {
1199        let lower_name = name.to_ascii_lowercase();
1200        self.validate_aggregate_clause_placement(
1201            &lower_name,
1202            distinct,
1203            order_by,
1204            within_group,
1205            filter,
1206            over.is_some(),
1207            span,
1208        )?;
1209        if over.is_some() {
1210            match lower_name.as_str() {
1211                "lag" | "lead" => {
1212                    validate_offset_window_call(name, args.len(), distinct, star)?;
1213                }
1214                "first_value" | "last_value" | "ntile" => {
1215                    validate_exact_window_call(name, args.len(), 1, distinct, star)?;
1216                }
1217                "nth_value" => {
1218                    validate_exact_window_call(name, args.len(), 2, distinct, star)?;
1219                }
1220                "percent_rank" | "cume_dist" => {
1221                    validate_exact_window_call(name, args.len(), 0, distinct, star)?;
1222                }
1223                _ => {}
1224            }
1225        }
1226
1227        let mut typed_args: Vec<TypedExpr> = args
1228            .iter()
1229            .map(|arg| self.infer_type_with_scope(arg, scope, plan_subquery))
1230            .collect::<Result<Vec<_>, _>>()?;
1231
1232        // WITHIN GROUP normalizes onto the same aggregate-local ordering as an
1233        // in-argument ORDER BY; the parser rejects supplying both at once.
1234        let order_by_source = if within_group.is_empty() {
1235            order_by
1236        } else {
1237            within_group
1238        };
1239        let typed_order_by = order_by_source
1240            .iter()
1241            .map(|order| {
1242                let expr = self.infer_type_with_scope(&order.expr, scope, plan_subquery)?;
1243                if super::typed_expr_contains_aggregate(&expr) {
1244                    return Err(PlannerError::invalid_expression(
1245                        "aggregate functions are not allowed in aggregate ORDER BY".to_string(),
1246                    ));
1247                }
1248                if super::typed_expr_contains_window(&expr) {
1249                    return Err(PlannerError::invalid_expression(
1250                        "window functions are not allowed in aggregate ORDER BY".to_string(),
1251                    ));
1252                }
1253                Ok(SortExpr::new(
1254                    expr,
1255                    order.asc.unwrap_or(true),
1256                    order.nulls_first.unwrap_or(false),
1257                ))
1258            })
1259            .collect::<Result<Vec<_>, PlannerError>>()?;
1260
1261        let typed_filter = filter
1262            .map(|predicate| {
1263                let typed = self.infer_type_with_scope(predicate, scope, plan_subquery)?;
1264                if super::typed_expr_contains_aggregate(&typed) {
1265                    return Err(PlannerError::invalid_expression(
1266                        "aggregate functions are not allowed in FILTER".to_string(),
1267                    ));
1268                }
1269                if super::typed_expr_contains_window(&typed) {
1270                    return Err(PlannerError::invalid_expression(
1271                        "window functions are not allowed in FILTER".to_string(),
1272                    ));
1273                }
1274                if !matches!(
1275                    typed.resolved_type,
1276                    ResolvedType::Boolean | ResolvedType::Null
1277                ) {
1278                    return Err(PlannerError::type_mismatch(
1279                        "BOOLEAN FILTER predicate",
1280                        typed.resolved_type.type_name(),
1281                        typed.span,
1282                    ));
1283                }
1284                Ok(Box::new(typed))
1285            })
1286            .transpose()?;
1287
1288        // D4 (PostgreSQL rule): with DISTINCT, every aggregate ORDER BY
1289        // expression must appear in the argument list, otherwise the sort key
1290        // is undefined after deduplication.
1291        if distinct && !typed_order_by.is_empty() {
1292            for sort in &typed_order_by {
1293                let key = super::distinct_on_expr_signature(&sort.expr);
1294                let appears = typed_args
1295                    .iter()
1296                    .any(|arg| super::distinct_on_expr_signature(arg) == key);
1297                if !appears {
1298                    return Err(PlannerError::invalid_expression(
1299                        "in an aggregate with DISTINCT, ORDER BY expressions must appear in \
1300                         the argument list"
1301                            .to_string(),
1302                    ));
1303                }
1304            }
1305        }
1306
1307        let result_type = if over.is_some() {
1308            match lower_name.as_str() {
1309                "lag" | "lead" => self.infer_offset_window_result_type(name, &mut typed_args)?,
1310                "first_value" | "last_value" => typed_args[0].resolved_type.clone(),
1311                "nth_value" => {
1312                    validate_positive_integer_argument(name, &typed_args[1])?;
1313                    typed_args[0].resolved_type.clone()
1314                }
1315                "ntile" => {
1316                    validate_positive_integer_argument(name, &typed_args[0])?;
1317                    ResolvedType::BigInt
1318                }
1319                "percent_rank" | "cume_dist" => ResolvedType::Double,
1320                "row_number" | "rank" | "dense_rank" => {
1321                    if !typed_args.is_empty() || distinct || star {
1322                        return Err(PlannerError::invalid_expression(format!(
1323                            "{}() window function takes no arguments",
1324                            name.to_ascii_uppercase()
1325                        )));
1326                    }
1327                    ResolvedType::BigInt
1328                }
1329                "sum" | "count" | "avg" | "min" | "max" => {
1330                    self.check_function_call(name, &typed_args, distinct, star, span)?
1331                }
1332                _ => {
1333                    return Err(PlannerError::unsupported_feature(
1334                        format!("function '{}' with OVER", name),
1335                        "future",
1336                        span,
1337                    ));
1338                }
1339            }
1340        } else if lower_name == "percentile_disc" {
1341            self.check_percentile_disc(&typed_args, &typed_order_by, span)?
1342        } else {
1343            self.check_function_call(name, &typed_args, distinct, star, span)?
1344        };
1345
1346        let typed_over = over
1347            .map(|window| {
1348                if let Some(base) = &window.base {
1349                    return Err(PlannerError::invalid_expression(format!(
1350                        "named window '{base}' was not resolved in its query block"
1351                    )));
1352                }
1353                let partition_by = window
1354                    .partition_by
1355                    .iter()
1356                    .map(|expr| self.infer_type_with_scope(expr, scope, plan_subquery))
1357                    .collect::<Result<Vec<_>, _>>()?;
1358                let order_by = window
1359                    .order_by
1360                    .iter()
1361                    .map(|order| {
1362                        let expr = self.infer_type_with_scope(&order.expr, scope, plan_subquery)?;
1363                        Ok(SortExpr::new(
1364                            expr,
1365                            order.asc.unwrap_or(true),
1366                            order.nulls_first.unwrap_or(false),
1367                        ))
1368                    })
1369                    .collect::<Result<Vec<_>, PlannerError>>()?;
1370                if let Some(frame) = &window.frame {
1371                    validate_window_frame(&lower_name, frame, &order_by)?;
1372                }
1373                Ok(TypedWindowSpec {
1374                    partition_by,
1375                    order_by,
1376                    frame: window.frame.clone(),
1377                })
1378            })
1379            .transpose()?;
1380
1381        Ok(TypedExpr {
1382            kind: TypedExprKind::FunctionCall {
1383                name: name.to_string(),
1384                args: typed_args,
1385                distinct,
1386                star,
1387                filter: typed_filter,
1388                order_by: typed_order_by,
1389                over: typed_over,
1390            },
1391            resolved_type: result_type,
1392            span,
1393        })
1394    }
1395
1396    /// Placement rules for FILTER / WITHIN GROUP / aggregate ORDER BY that do
1397    /// not require typed arguments (issue #148, D2/D6/D7).
1398    #[allow(clippy::too_many_arguments)]
1399    fn validate_aggregate_clause_placement(
1400        &self,
1401        lower_name: &str,
1402        distinct: bool,
1403        order_by: &[crate::ast::dml::OrderByExpr],
1404        within_group: &[crate::ast::dml::OrderByExpr],
1405        filter: Option<&Expr>,
1406        has_over: bool,
1407        span: Span,
1408    ) -> Result<(), PlannerError> {
1409        let is_ordered_set = is_ordered_set_aggregate_name(lower_name);
1410        let is_aggregate = is_aggregate_name(lower_name);
1411
1412        if let Some(filter) = filter {
1413            if has_over {
1414                // PostgreSQL allows FILTER on window-aggregates; the Alopex
1415                // window executor frame path does not implement it yet, so the
1416                // boundary is a stable explicit error (D2).
1417                return Err(PlannerError::unsupported_feature(
1418                    "FILTER on a window function call",
1419                    "future",
1420                    span,
1421                ));
1422            }
1423            if !is_aggregate {
1424                return Err(PlannerError::invalid_expression(format!(
1425                    "FILTER (WHERE ...) is only valid for aggregate functions, not '{lower_name}'"
1426                )));
1427            }
1428            if super::expr_contains_subquery(filter) {
1429                return Err(PlannerError::unsupported_feature(
1430                    "subquery in aggregate FILTER",
1431                    "future",
1432                    filter.span,
1433                ));
1434            }
1435        }
1436
1437        if !within_group.is_empty() {
1438            if has_over {
1439                // PostgreSQL: ordered-set aggregates cannot be window calls.
1440                return Err(PlannerError::invalid_expression(
1441                    "WITHIN GROUP cannot be combined with OVER".to_string(),
1442                ));
1443            }
1444            if !is_ordered_set {
1445                return Err(PlannerError::invalid_expression(format!(
1446                    "WITHIN GROUP is only valid for ordered-set aggregate functions, \
1447                     not '{lower_name}'"
1448                )));
1449            }
1450            if distinct {
1451                return Err(PlannerError::invalid_expression(
1452                    "DISTINCT is not supported with WITHIN GROUP".to_string(),
1453                ));
1454            }
1455            if within_group
1456                .iter()
1457                .any(|order| super::expr_contains_subquery(&order.expr))
1458            {
1459                return Err(PlannerError::unsupported_feature(
1460                    "subquery in aggregate ORDER BY",
1461                    "future",
1462                    span,
1463                ));
1464            }
1465        }
1466
1467        if !order_by.is_empty() {
1468            if has_over {
1469                // PostgreSQL: "aggregate ORDER BY is not implemented for
1470                // window functions".
1471                return Err(PlannerError::invalid_expression(
1472                    "aggregate ORDER BY cannot be combined with OVER".to_string(),
1473                ));
1474            }
1475            if !is_aggregate || is_ordered_set {
1476                return Err(PlannerError::invalid_expression(format!(
1477                    "ORDER BY in the argument list is only valid for aggregate functions, \
1478                     not '{lower_name}'"
1479                )));
1480            }
1481            if order_by
1482                .iter()
1483                .any(|order| super::expr_contains_subquery(&order.expr))
1484            {
1485                return Err(PlannerError::unsupported_feature(
1486                    "subquery in aggregate ORDER BY",
1487                    "future",
1488                    span,
1489                ));
1490            }
1491        }
1492
1493        if is_ordered_set && within_group.is_empty() && !has_over {
1494            return Err(PlannerError::invalid_expression(format!(
1495                "WITHIN GROUP (ORDER BY ...) is required for {}",
1496                lower_name.to_ascii_uppercase()
1497            )));
1498        }
1499
1500        Ok(())
1501    }
1502
1503    /// Argument and ordering rules for `PERCENTILE_DISC(fraction) WITHIN
1504    /// GROUP (ORDER BY sort_expr)` (issue #148, D5). The result type is the
1505    /// sort expression's type; PostgreSQL 16 behaves identically.
1506    fn check_percentile_disc(
1507        &self,
1508        args: &[TypedExpr],
1509        order_by: &[SortExpr],
1510        span: Span,
1511    ) -> Result<ResolvedType, PlannerError> {
1512        if args.len() != 1 {
1513            return Err(PlannerError::type_mismatch(
1514                "1 argument",
1515                format!("{} arguments", args.len()),
1516                span,
1517            ));
1518        }
1519        let _ = percentile_fraction(&args[0])?;
1520        if order_by.len() != 1 {
1521            return Err(PlannerError::invalid_expression(
1522                "PERCENTILE_DISC requires WITHIN GROUP (ORDER BY ...) with exactly one \
1523                 sort expression"
1524                    .to_string(),
1525            ));
1526        }
1527        Ok(order_by[0].expr.resolved_type.clone())
1528    }
1529
1530    fn infer_offset_window_result_type(
1531        &self,
1532        name: &str,
1533        args: &mut [TypedExpr],
1534    ) -> Result<ResolvedType, PlannerError> {
1535        if let Some(offset) = args.get(1)
1536            && !matches!(
1537                offset.resolved_type,
1538                ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Null
1539            )
1540        {
1541            return Err(PlannerError::type_mismatch(
1542                "INTEGER offset",
1543                offset.resolved_type.type_name(),
1544                offset.span,
1545            ));
1546        }
1547
1548        let value_type = args
1549            .first()
1550            .map(|arg| arg.resolved_type.clone())
1551            .ok_or_else(|| {
1552                PlannerError::invalid_expression(format!(
1553                    "{}() window function expects 1 to 3 arguments",
1554                    name.to_ascii_uppercase()
1555                ))
1556            })?;
1557        let result_type = if let Some(default) = args.get(2) {
1558            self.common_compatible_result_type(&value_type, &default.resolved_type, default.span)?
1559        } else {
1560            value_type
1561        };
1562
1563        coerce_compatible_result(&mut args[0], &result_type);
1564        if let Some(default) = args.get_mut(2) {
1565            coerce_compatible_result(default, &result_type);
1566        }
1567
1568        Ok(result_type)
1569    }
1570
1571    fn common_compatible_result_type(
1572        &self,
1573        current: &ResolvedType,
1574        next: &ResolvedType,
1575        span: Span,
1576    ) -> Result<ResolvedType, PlannerError> {
1577        if matches!(current, ResolvedType::Null) {
1578            return Ok(next.clone());
1579        }
1580        if matches!(next, ResolvedType::Null) || current == next {
1581            return Ok(current.clone());
1582        }
1583        if is_numeric_type(current) && is_numeric_type(next) {
1584            return self.check_arithmetic_op(current, next, span);
1585        }
1586        if next.can_cast_to(current) {
1587            return Ok(current.clone());
1588        }
1589        if current.can_cast_to(next) {
1590            return Ok(next.clone());
1591        }
1592        Err(PlannerError::type_mismatch(
1593            current.type_name(),
1594            next.type_name(),
1595            span,
1596        ))
1597    }
1598
1599    /// Infer the type of a BETWEEN expression.
1600    #[allow(dead_code)]
1601    fn infer_between_type(
1602        &self,
1603        expr: &Expr,
1604        low: &Expr,
1605        high: &Expr,
1606        negated: bool,
1607        table: &TableMetadata,
1608        span: Span,
1609    ) -> Result<TypedExpr, PlannerError> {
1610        let expr_typed = self.infer_type(expr, table)?;
1611        let low_typed = self.infer_type(low, table)?;
1612        let high_typed = self.infer_type(high, table)?;
1613
1614        // Check that all three expressions have compatible types
1615        self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
1616        self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
1617
1618        Ok(TypedExpr {
1619            kind: TypedExprKind::Between {
1620                expr: Box::new(expr_typed),
1621                low: Box::new(low_typed),
1622                high: Box::new(high_typed),
1623                negated,
1624            },
1625            resolved_type: ResolvedType::Boolean,
1626            span,
1627        })
1628    }
1629
1630    #[allow(clippy::too_many_arguments)]
1631    fn infer_between_type_with_scope(
1632        &self,
1633        expr: &Expr,
1634        low: &Expr,
1635        high: &Expr,
1636        negated: bool,
1637        scope: &[ScopedTable],
1638        plan_subquery: &SubqueryPlanner<'_>,
1639        span: Span,
1640    ) -> Result<TypedExpr, PlannerError> {
1641        if row_items(expr).is_some() || row_items(low).is_some() || row_items(high).is_some() {
1642            let expr_typed = self.infer_row_operand_with_scope(expr, scope, plan_subquery)?;
1643            let low_typed = self.infer_row_operand_with_scope(low, scope, plan_subquery)?;
1644            let high_typed = self.infer_row_operand_with_scope(high, scope, plan_subquery)?;
1645            let width = expr_typed.len();
1646            self.check_row_arity(width, low_typed.len(), span)?;
1647            self.check_row_arity(width, high_typed.len(), span)?;
1648            self.check_row_types(&expr_typed, &low_typed, span)?;
1649            self.check_row_types(&expr_typed, &high_typed, span)?;
1650            let mut args = expr_typed;
1651            args.extend(low_typed);
1652            args.extend(high_typed);
1653            return Ok(internal_predicate(
1654                format!("{INTERNAL_ROW_BETWEEN}:{width}:{}", u8::from(negated)),
1655                args,
1656                span,
1657            ));
1658        }
1659
1660        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1661        let low_typed = self.infer_type_with_scope(low, scope, plan_subquery)?;
1662        let high_typed = self.infer_type_with_scope(high, scope, plan_subquery)?;
1663        self.check_comparison_op(&expr_typed.resolved_type, &low_typed.resolved_type, span)?;
1664        self.check_comparison_op(&expr_typed.resolved_type, &high_typed.resolved_type, span)?;
1665
1666        Ok(TypedExpr {
1667            kind: TypedExprKind::Between {
1668                expr: Box::new(expr_typed),
1669                low: Box::new(low_typed),
1670                high: Box::new(high_typed),
1671                negated,
1672            },
1673            resolved_type: ResolvedType::Boolean,
1674            span,
1675        })
1676    }
1677
1678    /// Infer the type of a LIKE expression.
1679    #[allow(dead_code)]
1680    #[allow(clippy::too_many_arguments)]
1681    fn infer_like_type(
1682        &self,
1683        expr: &Expr,
1684        pattern: &Expr,
1685        escape: Option<&Expr>,
1686        negated: bool,
1687        kind: PatternMatchKind,
1688        table: &TableMetadata,
1689        span: Span,
1690    ) -> Result<TypedExpr, PlannerError> {
1691        let expr_typed = self.infer_type(expr, table)?;
1692        let pattern_typed = self.infer_type(pattern, table)?;
1693
1694        // Expression must be text
1695        if !matches!(
1696            expr_typed.resolved_type,
1697            ResolvedType::Text | ResolvedType::Null
1698        ) {
1699            return Err(PlannerError::TypeMismatch {
1700                expected: "Text".to_string(),
1701                found: expr_typed.resolved_type.type_name().to_string(),
1702                line: expr.span.start.line,
1703                column: expr.span.start.column,
1704            });
1705        }
1706
1707        // Pattern must be text
1708        if !matches!(
1709            pattern_typed.resolved_type,
1710            ResolvedType::Text | ResolvedType::Null
1711        ) {
1712            return Err(PlannerError::TypeMismatch {
1713                expected: "Text".to_string(),
1714                found: pattern_typed.resolved_type.type_name().to_string(),
1715                line: pattern.span.start.line,
1716                column: pattern.span.start.column,
1717            });
1718        }
1719
1720        let escape_typed = if let Some(esc) = escape {
1721            let typed = self.infer_type(esc, table)?;
1722            if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
1723                return Err(PlannerError::TypeMismatch {
1724                    expected: "Text".to_string(),
1725                    found: typed.resolved_type.type_name().to_string(),
1726                    line: esc.span.start.line,
1727                    column: esc.span.start.column,
1728                });
1729            }
1730            Some(Box::new(typed))
1731        } else {
1732            None
1733        };
1734
1735        Ok(TypedExpr {
1736            kind: TypedExprKind::Like {
1737                expr: Box::new(expr_typed),
1738                pattern: Box::new(pattern_typed),
1739                escape: escape_typed,
1740                negated,
1741                kind,
1742            },
1743            resolved_type: ResolvedType::Boolean,
1744            span,
1745        })
1746    }
1747
1748    #[allow(clippy::too_many_arguments)]
1749    #[allow(clippy::too_many_arguments)]
1750    fn infer_like_type_with_scope(
1751        &self,
1752        expr: &Expr,
1753        pattern: &Expr,
1754        escape: Option<&Expr>,
1755        negated: bool,
1756        kind: PatternMatchKind,
1757        scope: &[ScopedTable],
1758        plan_subquery: &SubqueryPlanner<'_>,
1759        span: Span,
1760    ) -> Result<TypedExpr, PlannerError> {
1761        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1762        let pattern_typed = self.infer_type_with_scope(pattern, scope, plan_subquery)?;
1763
1764        if !matches!(
1765            expr_typed.resolved_type,
1766            ResolvedType::Text | ResolvedType::Null
1767        ) {
1768            return Err(PlannerError::TypeMismatch {
1769                expected: "Text".to_string(),
1770                found: expr_typed.resolved_type.type_name().to_string(),
1771                line: expr.span.start.line,
1772                column: expr.span.start.column,
1773            });
1774        }
1775
1776        if !matches!(
1777            pattern_typed.resolved_type,
1778            ResolvedType::Text | ResolvedType::Null
1779        ) {
1780            return Err(PlannerError::TypeMismatch {
1781                expected: "Text".to_string(),
1782                found: pattern_typed.resolved_type.type_name().to_string(),
1783                line: pattern.span.start.line,
1784                column: pattern.span.start.column,
1785            });
1786        }
1787
1788        let escape_typed = if let Some(esc) = escape {
1789            let typed = self.infer_type_with_scope(esc, scope, plan_subquery)?;
1790            if !matches!(typed.resolved_type, ResolvedType::Text | ResolvedType::Null) {
1791                return Err(PlannerError::TypeMismatch {
1792                    expected: "Text".to_string(),
1793                    found: typed.resolved_type.type_name().to_string(),
1794                    line: esc.span.start.line,
1795                    column: esc.span.start.column,
1796                });
1797            }
1798            Some(Box::new(typed))
1799        } else {
1800            None
1801        };
1802
1803        Ok(TypedExpr {
1804            kind: TypedExprKind::Like {
1805                expr: Box::new(expr_typed),
1806                pattern: Box::new(pattern_typed),
1807                escape: escape_typed,
1808                negated,
1809                kind,
1810            },
1811            resolved_type: ResolvedType::Boolean,
1812            span,
1813        })
1814    }
1815
1816    /// Infer the type of an IN list expression.
1817    #[allow(dead_code)]
1818    fn infer_in_list_type(
1819        &self,
1820        expr: &Expr,
1821        list: &[Expr],
1822        negated: bool,
1823        table: &TableMetadata,
1824        span: Span,
1825    ) -> Result<TypedExpr, PlannerError> {
1826        let expr_typed = self.infer_type(expr, table)?;
1827
1828        let typed_list: Vec<TypedExpr> = list
1829            .iter()
1830            .map(|item| {
1831                let typed = self.infer_type(item, table)?;
1832                // Check each item is compatible with the expression
1833                self.check_comparison_op(
1834                    &expr_typed.resolved_type,
1835                    &typed.resolved_type,
1836                    item.span,
1837                )?;
1838                Ok(typed)
1839            })
1840            .collect::<Result<Vec<_>, PlannerError>>()?;
1841
1842        Ok(TypedExpr {
1843            kind: TypedExprKind::InList {
1844                expr: Box::new(expr_typed),
1845                list: typed_list,
1846                negated,
1847            },
1848            resolved_type: ResolvedType::Boolean,
1849            span,
1850        })
1851    }
1852
1853    fn infer_in_list_type_with_scope(
1854        &self,
1855        expr: &Expr,
1856        list: &[Expr],
1857        negated: bool,
1858        scope: &[ScopedTable],
1859        plan_subquery: &SubqueryPlanner<'_>,
1860        span: Span,
1861    ) -> Result<TypedExpr, PlannerError> {
1862        if row_items(expr).is_some() || list.iter().any(|item| row_items(item).is_some()) {
1863            let mut args = self.infer_row_operand_with_scope(expr, scope, plan_subquery)?;
1864            let width = args.len();
1865            for item in list {
1866                let typed = self.infer_row_operand_with_scope(item, scope, plan_subquery)?;
1867                self.check_row_arity(width, typed.len(), item.span)?;
1868                self.check_row_types(&args[..width], &typed, item.span)?;
1869                args.extend(typed);
1870            }
1871            return Ok(internal_predicate(
1872                format!("{INTERNAL_ROW_IN}:{width}:{}", u8::from(negated)),
1873                args,
1874                span,
1875            ));
1876        }
1877
1878        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1879
1880        let typed_list: Vec<TypedExpr> = list
1881            .iter()
1882            .map(|item| {
1883                let typed = self.infer_type_with_scope(item, scope, plan_subquery)?;
1884                self.check_comparison_op(
1885                    &expr_typed.resolved_type,
1886                    &typed.resolved_type,
1887                    item.span,
1888                )?;
1889                Ok(typed)
1890            })
1891            .collect::<Result<Vec<_>, PlannerError>>()?;
1892
1893        Ok(TypedExpr {
1894            kind: TypedExprKind::InList {
1895                expr: Box::new(expr_typed),
1896                list: typed_list,
1897                negated,
1898            },
1899            resolved_type: ResolvedType::Boolean,
1900            span,
1901        })
1902    }
1903
1904    fn infer_truth_predicate_with_scope(
1905        &self,
1906        expr: &Expr,
1907        value: TruthValue,
1908        negated: bool,
1909        scope: &[ScopedTable],
1910        plan_subquery: &SubqueryPlanner<'_>,
1911        span: Span,
1912    ) -> Result<TypedExpr, PlannerError> {
1913        let typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
1914        if !matches!(
1915            typed.resolved_type,
1916            ResolvedType::Boolean | ResolvedType::Null
1917        ) {
1918            return Err(PlannerError::type_mismatch(
1919                "Boolean",
1920                typed.resolved_type.type_name(),
1921                expr.span,
1922            ));
1923        }
1924        let name = match value {
1925            TruthValue::True => INTERNAL_TRUTH_TRUE,
1926            TruthValue::False => INTERNAL_TRUTH_FALSE,
1927            TruthValue::Unknown => INTERNAL_TRUTH_UNKNOWN,
1928        };
1929        Ok(internal_predicate(
1930            format!("{name}:{}", u8::from(negated)),
1931            vec![typed],
1932            span,
1933        ))
1934    }
1935
1936    fn infer_distinct_predicate_with_scope(
1937        &self,
1938        left: &Expr,
1939        right: &Expr,
1940        negated: bool,
1941        scope: &[ScopedTable],
1942        plan_subquery: &SubqueryPlanner<'_>,
1943        span: Span,
1944    ) -> Result<TypedExpr, PlannerError> {
1945        let (mut left, right, width) =
1946            self.infer_row_pair_with_scope(left, right, scope, plan_subquery, span)?;
1947        left.extend(right);
1948        Ok(internal_predicate(
1949            format!("{INTERNAL_ROW_DISTINCT}:{width}:{}", u8::from(negated)),
1950            left,
1951            span,
1952        ))
1953    }
1954
1955    fn infer_row_pair_with_scope(
1956        &self,
1957        left: &Expr,
1958        right: &Expr,
1959        scope: &[ScopedTable],
1960        plan_subquery: &SubqueryPlanner<'_>,
1961        span: Span,
1962    ) -> Result<(Vec<TypedExpr>, Vec<TypedExpr>, usize), PlannerError> {
1963        let left = self.infer_row_operand_with_scope(left, scope, plan_subquery)?;
1964        let right = self.infer_row_operand_with_scope(right, scope, plan_subquery)?;
1965        let width = left.len();
1966        self.check_row_arity(width, right.len(), span)?;
1967        self.check_row_types(&left, &right, span)?;
1968        Ok((left, right, width))
1969    }
1970
1971    fn infer_row_operand_with_scope(
1972        &self,
1973        expr: &Expr,
1974        scope: &[ScopedTable],
1975        plan_subquery: &SubqueryPlanner<'_>,
1976    ) -> Result<Vec<TypedExpr>, PlannerError> {
1977        match row_items(expr) {
1978            Some(items) => items
1979                .iter()
1980                .map(|item| self.infer_type_with_scope(item, scope, plan_subquery))
1981                .collect(),
1982            None => Ok(vec![self.infer_type_with_scope(
1983                expr,
1984                scope,
1985                plan_subquery,
1986            )?]),
1987        }
1988    }
1989
1990    fn check_row_arity(
1991        &self,
1992        expected: usize,
1993        actual: usize,
1994        span: Span,
1995    ) -> Result<(), PlannerError> {
1996        if expected == actual {
1997            Ok(())
1998        } else {
1999            Err(PlannerError::RowArityMismatch {
2000                expected,
2001                actual,
2002                line: span.start.line,
2003                column: span.start.column,
2004            })
2005        }
2006    }
2007
2008    fn check_row_types(
2009        &self,
2010        left: &[TypedExpr],
2011        right: &[TypedExpr],
2012        span: Span,
2013    ) -> Result<(), PlannerError> {
2014        for (left, right) in left.iter().zip(right) {
2015            self.check_comparison_op(&left.resolved_type, &right.resolved_type, span)?;
2016        }
2017        Ok(())
2018    }
2019
2020    /// Infer the type of an IS NULL expression.
2021    #[allow(dead_code)]
2022    fn infer_is_null_type(
2023        &self,
2024        expr: &Expr,
2025        negated: bool,
2026        table: &TableMetadata,
2027        span: Span,
2028    ) -> Result<TypedExpr, PlannerError> {
2029        let expr_typed = self.infer_type(expr, table)?;
2030
2031        Ok(TypedExpr {
2032            kind: TypedExprKind::IsNull {
2033                expr: Box::new(expr_typed),
2034                negated,
2035            },
2036            resolved_type: ResolvedType::Boolean,
2037            span,
2038        })
2039    }
2040
2041    fn infer_is_null_type_with_scope(
2042        &self,
2043        expr: &Expr,
2044        negated: bool,
2045        scope: &[ScopedTable],
2046        plan_subquery: &SubqueryPlanner<'_>,
2047        span: Span,
2048    ) -> Result<TypedExpr, PlannerError> {
2049        let expr_typed = self.infer_type_with_scope(expr, scope, plan_subquery)?;
2050
2051        Ok(TypedExpr {
2052            kind: TypedExprKind::IsNull {
2053                expr: Box::new(expr_typed),
2054                negated,
2055            },
2056            resolved_type: ResolvedType::Boolean,
2057            span,
2058        })
2059    }
2060
2061    /// Infer the type of a vector literal.
2062    fn infer_vector_literal_type(
2063        &self,
2064        values: &[f64],
2065        span: Span,
2066    ) -> Result<TypedExpr, PlannerError> {
2067        Ok(TypedExpr {
2068            kind: TypedExprKind::VectorLiteral(values.to_vec()),
2069            resolved_type: ResolvedType::Vector {
2070                dimension: values.len() as u32,
2071                metric: VectorMetric::Cosine, // Default metric for literals
2072            },
2073            span,
2074        })
2075    }
2076
2077    /// Normalize a metric string to VectorMetric enum (case-insensitive).
2078    ///
2079    /// # Valid Values
2080    ///
2081    /// - "cosine" (case-insensitive) → `VectorMetric::Cosine`
2082    /// - "l2" (case-insensitive) → `VectorMetric::L2`
2083    /// - "inner" (case-insensitive) → `VectorMetric::Inner`
2084    ///
2085    /// # Errors
2086    ///
2087    /// Returns `PlannerError::InvalidMetric` if the value is not recognized.
2088    pub fn normalize_metric(&self, metric: &str, span: Span) -> Result<VectorMetric, PlannerError> {
2089        match metric.to_lowercase().as_str() {
2090            "cosine" => Ok(VectorMetric::Cosine),
2091            "l2" => Ok(VectorMetric::L2),
2092            "inner" => Ok(VectorMetric::Inner),
2093            _ => Err(PlannerError::InvalidMetric {
2094                value: metric.to_string(),
2095                line: span.start.line,
2096                column: span.start.column,
2097            }),
2098        }
2099    }
2100
2101    /// Check function call and return the result type.
2102    ///
2103    /// Validates that the function arguments have correct types and returns
2104    /// the result type.
2105    pub fn check_function_call(
2106        &self,
2107        name: &str,
2108        args: &[TypedExpr],
2109        distinct: bool,
2110        star: bool,
2111        span: Span,
2112    ) -> Result<ResolvedType, PlannerError> {
2113        let lower_name = name.to_ascii_lowercase();
2114
2115        match lower_name.as_str() {
2116            "count" => self.check_count(args, distinct, star, span),
2117            "sum" => self.check_sum(args, distinct, star, span),
2118            "total" => self.check_total(args, distinct, star, span),
2119            "avg" => self.check_avg(args, distinct, star, span),
2120            "min" => self.check_min_max(args, distinct, star, span),
2121            "max" => self.check_min_max(args, distinct, star, span),
2122            "group_concat" => self.check_group_concat(args, distinct, star, span),
2123            "string_agg" => self.check_string_agg(args, distinct, star, span),
2124            // GROUPING/GROUPING_ID distinguish grouping-set placeholder NULLs
2125            // from data NULLs (issue #149, D4). Placement and argument
2126            // validation happen in the planner; the result is a BIGINT
2127            // bitmask, so at most 63 arguments are accepted.
2128            "grouping" | "grouping_id" => {
2129                if distinct || star {
2130                    return Err(PlannerError::invalid_expression(
2131                        "GROUPING does not support DISTINCT or *".to_string(),
2132                    ));
2133                }
2134                if args.is_empty() {
2135                    return Err(PlannerError::invalid_expression(
2136                        "GROUPING requires at least one argument".to_string(),
2137                    ));
2138                }
2139                if args.len() > 63 {
2140                    return Err(PlannerError::invalid_expression(
2141                        "GROUPING accepts at most 63 arguments".to_string(),
2142                    ));
2143                }
2144                Ok(ResolvedType::BigInt)
2145            }
2146            _ => {
2147                let Some(signature) = crate::scalar::signature(&lower_name) else {
2148                    return Err(PlannerError::unsupported_feature(
2149                        format!("function '{name}'"),
2150                        "future",
2151                        span,
2152                    ));
2153                };
2154                if distinct || star {
2155                    return Err(PlannerError::invalid_expression(format!(
2156                        "scalar function '{name}' does not support DISTINCT or *"
2157                    )));
2158                }
2159                signature.arity.validate(name, args.len(), span)?;
2160                (signature.check)(args)?;
2161                let types: Vec<_> = args.iter().map(|arg| arg.resolved_type.clone()).collect();
2162                match &signature.ret {
2163                    crate::scalar::ReturnRule::Fixed(ty) => Ok(ty.clone()),
2164                    crate::scalar::ReturnRule::FromArgs(rule) => rule(&types),
2165                }
2166            }
2167        }
2168    }
2169
2170    pub fn validate_having_expr(
2171        &self,
2172        expr: &TypedExpr,
2173        group_keys: &[TypedExpr],
2174        aggregates: &[AggregateExpr],
2175    ) -> Result<(), PlannerError> {
2176        use std::collections::HashSet;
2177
2178        let group_key_indices: HashSet<usize> = group_keys
2179            .iter()
2180            .filter_map(|expr| match &expr.kind {
2181                TypedExprKind::ColumnRef { column_index, .. } => Some(*column_index),
2182                _ => None,
2183            })
2184            .collect();
2185
2186        let aggregate_signatures: HashSet<AggregateSignature> = aggregates
2187            .iter()
2188            .map(aggregate_signature_from_expr)
2189            .collect();
2190
2191        fn walk(
2192            expr: &TypedExpr,
2193            group_key_indices: &HashSet<usize>,
2194            aggregate_signatures: &HashSet<AggregateSignature>,
2195        ) -> Result<(), PlannerError> {
2196            match &expr.kind {
2197                TypedExprKind::ColumnRef { column_index, .. } => {
2198                    if group_key_indices.contains(column_index) {
2199                        Ok(())
2200                    } else {
2201                        Err(PlannerError::invalid_expression(
2202                            "column in HAVING must be in GROUP BY or be aggregated".to_string(),
2203                        ))
2204                    }
2205                }
2206                TypedExprKind::FunctionCall { name, args, .. }
2207                    if name.eq_ignore_ascii_case("grouping")
2208                        || name.eq_ignore_ascii_case("grouping_id") =>
2209                {
2210                    // GROUPING in HAVING is valid when every argument is a
2211                    // grouping expression (issue #149, D5); the planner
2212                    // rewrites the call onto __grouping_id afterwards.
2213                    for arg in args {
2214                        match &arg.kind {
2215                            TypedExprKind::ColumnRef { column_index, .. }
2216                                if group_key_indices.contains(column_index) => {}
2217                            _ => {
2218                                return Err(PlannerError::invalid_expression(
2219                                    "arguments to GROUPING must be grouping expressions \
2220                                     of the query"
2221                                        .to_string(),
2222                                ));
2223                            }
2224                        }
2225                    }
2226                    Ok(())
2227                }
2228                TypedExprKind::FunctionCall {
2229                    name,
2230                    args,
2231                    distinct,
2232                    star,
2233                    filter,
2234                    order_by,
2235                    over: _,
2236                } if is_aggregate_name(name) => {
2237                    let signature = aggregate_signature_from_call(
2238                        name,
2239                        args,
2240                        *distinct,
2241                        *star,
2242                        filter.as_deref(),
2243                        order_by,
2244                    )?;
2245                    if aggregate_signatures.contains(&signature) {
2246                        Ok(())
2247                    } else {
2248                        Err(PlannerError::invalid_expression(
2249                            "aggregate in HAVING must appear in plan".to_string(),
2250                        ))
2251                    }
2252                }
2253                TypedExprKind::BinaryOp { left, right, .. } => {
2254                    walk(left, group_key_indices, aggregate_signatures)?;
2255                    walk(right, group_key_indices, aggregate_signatures)
2256                }
2257                TypedExprKind::UnaryOp { operand, .. } => {
2258                    walk(operand, group_key_indices, aggregate_signatures)
2259                }
2260                TypedExprKind::Case {
2261                    operand,
2262                    branches,
2263                    else_expr,
2264                } => {
2265                    if let Some(operand) = operand {
2266                        walk(operand, group_key_indices, aggregate_signatures)?;
2267                    }
2268                    for branch in branches {
2269                        walk(&branch.when, group_key_indices, aggregate_signatures)?;
2270                        walk(&branch.then, group_key_indices, aggregate_signatures)?;
2271                    }
2272                    if let Some(else_expr) = else_expr {
2273                        walk(else_expr, group_key_indices, aggregate_signatures)?;
2274                    }
2275                    Ok(())
2276                }
2277                TypedExprKind::FunctionCall { args, .. } => {
2278                    for arg in args {
2279                        walk(arg, group_key_indices, aggregate_signatures)?;
2280                    }
2281                    Ok(())
2282                }
2283                TypedExprKind::Between {
2284                    expr, low, high, ..
2285                } => {
2286                    walk(expr, group_key_indices, aggregate_signatures)?;
2287                    walk(low, group_key_indices, aggregate_signatures)?;
2288                    walk(high, group_key_indices, aggregate_signatures)
2289                }
2290                TypedExprKind::Like {
2291                    expr,
2292                    pattern,
2293                    escape,
2294                    ..
2295                } => {
2296                    walk(expr, group_key_indices, aggregate_signatures)?;
2297                    walk(pattern, group_key_indices, aggregate_signatures)?;
2298                    if let Some(esc) = escape {
2299                        walk(esc, group_key_indices, aggregate_signatures)?;
2300                    }
2301                    Ok(())
2302                }
2303                TypedExprKind::InList { expr, list, .. } => {
2304                    walk(expr, group_key_indices, aggregate_signatures)?;
2305                    for item in list {
2306                        walk(item, group_key_indices, aggregate_signatures)?;
2307                    }
2308                    Ok(())
2309                }
2310                TypedExprKind::IsNull { expr, .. } => {
2311                    walk(expr, group_key_indices, aggregate_signatures)
2312                }
2313                _ => Ok(()),
2314            }
2315        }
2316
2317        walk(expr, &group_key_indices, &aggregate_signatures)
2318    }
2319
2320    fn check_count(
2321        &self,
2322        args: &[TypedExpr],
2323        distinct: bool,
2324        star: bool,
2325        span: Span,
2326    ) -> Result<ResolvedType, PlannerError> {
2327        if star {
2328            if distinct {
2329                return Err(PlannerError::unsupported_feature(
2330                    "COUNT(DISTINCT *)",
2331                    "future",
2332                    span,
2333                ));
2334            }
2335            if !args.is_empty() {
2336                return Err(PlannerError::type_mismatch(
2337                    "no arguments with COUNT(*)",
2338                    format!("{} arguments", args.len()),
2339                    span,
2340                ));
2341            }
2342            return Ok(ResolvedType::BigInt);
2343        }
2344
2345        if args.len() != 1 {
2346            return Err(PlannerError::type_mismatch(
2347                "1 argument",
2348                format!("{} arguments", args.len()),
2349                span,
2350            ));
2351        }
2352
2353        if distinct {
2354            return Ok(ResolvedType::BigInt);
2355        }
2356
2357        Ok(ResolvedType::BigInt)
2358    }
2359
2360    fn check_sum(
2361        &self,
2362        args: &[TypedExpr],
2363        _distinct: bool,
2364        star: bool,
2365        span: Span,
2366    ) -> Result<ResolvedType, PlannerError> {
2367        if star {
2368            return Err(PlannerError::type_mismatch(
2369                "numeric argument",
2370                "COUNT(*) style",
2371                span,
2372            ));
2373        }
2374        let arg = self.require_single_arg(args, span)?;
2375        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
2376            return Err(PlannerError::type_mismatch(
2377                "numeric",
2378                arg.resolved_type.type_name().to_string(),
2379                arg.span,
2380            ));
2381        }
2382        Ok(crate::planner::aggregate_expr::sum_result_type(
2383            &arg.resolved_type,
2384        ))
2385    }
2386
2387    fn check_total(
2388        &self,
2389        args: &[TypedExpr],
2390        distinct: bool,
2391        star: bool,
2392        span: Span,
2393    ) -> Result<ResolvedType, PlannerError> {
2394        if star {
2395            return Err(PlannerError::type_mismatch(
2396                "numeric argument",
2397                "COUNT(*) style",
2398                span,
2399            ));
2400        }
2401        if distinct {
2402            return Err(PlannerError::unsupported_feature(
2403                "TOTAL(DISTINCT ...)",
2404                "future",
2405                span,
2406            ));
2407        }
2408        let arg = self.require_single_arg(args, span)?;
2409        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
2410            return Err(PlannerError::type_mismatch(
2411                "numeric",
2412                arg.resolved_type.type_name().to_string(),
2413                arg.span,
2414            ));
2415        }
2416        Ok(ResolvedType::Double)
2417    }
2418
2419    fn check_avg(
2420        &self,
2421        args: &[TypedExpr],
2422        _distinct: bool,
2423        star: bool,
2424        span: Span,
2425    ) -> Result<ResolvedType, PlannerError> {
2426        if star {
2427            return Err(PlannerError::type_mismatch(
2428                "numeric argument",
2429                "COUNT(*) style",
2430                span,
2431            ));
2432        }
2433        let arg = self.require_single_arg(args, span)?;
2434        if !is_numeric_type(&arg.resolved_type) && arg.resolved_type != ResolvedType::Null {
2435            return Err(PlannerError::type_mismatch(
2436                "numeric",
2437                arg.resolved_type.type_name().to_string(),
2438                arg.span,
2439            ));
2440        }
2441        Ok(ResolvedType::Double)
2442    }
2443
2444    fn check_min_max(
2445        &self,
2446        args: &[TypedExpr],
2447        _distinct: bool,
2448        star: bool,
2449        span: Span,
2450    ) -> Result<ResolvedType, PlannerError> {
2451        if star {
2452            return Err(PlannerError::type_mismatch(
2453                "argument",
2454                "COUNT(*) style",
2455                span,
2456            ));
2457        }
2458        let arg = self.require_single_arg(args, span)?;
2459        if matches!(arg.resolved_type, ResolvedType::Vector { .. }) {
2460            return Err(PlannerError::type_mismatch(
2461                "comparable",
2462                arg.resolved_type.type_name().to_string(),
2463                arg.span,
2464            ));
2465        }
2466        Ok(arg.resolved_type.clone())
2467    }
2468
2469    fn check_group_concat(
2470        &self,
2471        args: &[TypedExpr],
2472        _distinct: bool,
2473        star: bool,
2474        span: Span,
2475    ) -> Result<ResolvedType, PlannerError> {
2476        if star {
2477            return Err(PlannerError::type_mismatch(
2478                "text argument",
2479                "COUNT(*) style",
2480                span,
2481            ));
2482        }
2483        if args.is_empty() || args.len() > 2 {
2484            return Err(PlannerError::type_mismatch(
2485                "1 or 2 arguments",
2486                format!("{} arguments", args.len()),
2487                span,
2488            ));
2489        }
2490        if !matches!(
2491            args[0].resolved_type,
2492            ResolvedType::Text | ResolvedType::Null
2493        ) {
2494            return Err(PlannerError::type_mismatch(
2495                "Text",
2496                args[0].resolved_type.type_name().to_string(),
2497                args[0].span,
2498            ));
2499        }
2500        if args.len() == 2
2501            && !matches!(
2502                args[1].resolved_type,
2503                ResolvedType::Text | ResolvedType::Null
2504            )
2505        {
2506            return Err(PlannerError::type_mismatch(
2507                "Text",
2508                args[1].resolved_type.type_name().to_string(),
2509                args[1].span,
2510            ));
2511        }
2512        Ok(ResolvedType::Text)
2513    }
2514
2515    fn check_string_agg(
2516        &self,
2517        args: &[TypedExpr],
2518        _distinct: bool,
2519        star: bool,
2520        span: Span,
2521    ) -> Result<ResolvedType, PlannerError> {
2522        if star {
2523            return Err(PlannerError::type_mismatch(
2524                "text argument",
2525                "COUNT(*) style",
2526                span,
2527            ));
2528        }
2529        if args.len() != 2 {
2530            return Err(PlannerError::type_mismatch(
2531                "2 arguments",
2532                format!("{} arguments", args.len()),
2533                span,
2534            ));
2535        }
2536        if !matches!(
2537            args[0].resolved_type,
2538            ResolvedType::Text | ResolvedType::Null
2539        ) {
2540            return Err(PlannerError::type_mismatch(
2541                "Text",
2542                args[0].resolved_type.type_name().to_string(),
2543                args[0].span,
2544            ));
2545        }
2546        if !matches!(
2547            args[1].resolved_type,
2548            ResolvedType::Text | ResolvedType::Null
2549        ) {
2550            return Err(PlannerError::type_mismatch(
2551                "Text",
2552                args[1].resolved_type.type_name().to_string(),
2553                args[1].span,
2554            ));
2555        }
2556        Ok(ResolvedType::Text)
2557    }
2558
2559    fn require_single_arg<'b>(
2560        &self,
2561        args: &'b [TypedExpr],
2562        span: Span,
2563    ) -> Result<&'b TypedExpr, PlannerError> {
2564        if args.len() != 1 {
2565            return Err(PlannerError::type_mismatch(
2566                "1 argument",
2567                format!("{} arguments", args.len()),
2568                span,
2569            ));
2570        }
2571        Ok(&args[0])
2572    }
2573
2574    /// Check vector_distance function arguments.
2575    ///
2576    /// Signature: `vector_distance(column: Vector, vector: Vector, metric: Text) -> Double`
2577    ///
2578    /// # Requirements
2579    ///
2580    /// - First argument must be a Vector type (column reference)
2581    /// - Second argument must be a Vector type (vector literal)
2582    /// - Third argument must be a Text type (metric string)
2583    /// - Vector dimensions must match
2584    pub fn check_vector_distance(
2585        &self,
2586        args: &[TypedExpr],
2587        span: Span,
2588    ) -> Result<ResolvedType, PlannerError> {
2589        if args.len() != 3 {
2590            return Err(PlannerError::TypeMismatch {
2591                expected: "3 arguments".to_string(),
2592                found: format!("{} arguments", args.len()),
2593                line: span.start.line,
2594                column: span.start.column,
2595            });
2596        }
2597
2598        // First argument: Vector column
2599        let col_dim = match &args[0].resolved_type {
2600            ResolvedType::Vector { dimension, .. } => *dimension,
2601            other => {
2602                return Err(PlannerError::TypeMismatch {
2603                    expected: "Vector".to_string(),
2604                    found: other.type_name().to_string(),
2605                    line: args[0].span.start.line,
2606                    column: args[0].span.start.column,
2607                });
2608            }
2609        };
2610
2611        // Second argument: Vector literal
2612        let vec_dim = match &args[1].resolved_type {
2613            ResolvedType::Vector { dimension, .. } => *dimension,
2614            other => {
2615                return Err(PlannerError::TypeMismatch {
2616                    expected: "Vector".to_string(),
2617                    found: other.type_name().to_string(),
2618                    line: args[1].span.start.line,
2619                    column: args[1].span.start.column,
2620                });
2621            }
2622        };
2623
2624        // Check dimension match
2625        self.check_vector_dimension(col_dim, vec_dim, args[1].span)?;
2626
2627        // Third argument: Metric string
2628        match &args[2].resolved_type {
2629            ResolvedType::Text => {
2630                // Validate metric value if it's a literal
2631                if let TypedExprKind::Literal(Literal::String(s)) = &args[2].kind {
2632                    self.normalize_metric(s, args[2].span)?;
2633                }
2634            }
2635            ResolvedType::Null => {
2636                // NULL metric is not allowed
2637                return Err(PlannerError::TypeMismatch {
2638                    expected: "Text (metric)".to_string(),
2639                    found: "Null".to_string(),
2640                    line: args[2].span.start.line,
2641                    column: args[2].span.start.column,
2642                });
2643            }
2644            other => {
2645                return Err(PlannerError::TypeMismatch {
2646                    expected: "Text (metric)".to_string(),
2647                    found: other.type_name().to_string(),
2648                    line: args[2].span.start.line,
2649                    column: args[2].span.start.column,
2650                });
2651            }
2652        }
2653
2654        Ok(ResolvedType::Double)
2655    }
2656
2657    /// Check vector_similarity function arguments.
2658    ///
2659    /// Signature: `vector_similarity(column: Vector, vector: Vector, metric: Text) -> Double`
2660    ///
2661    /// Same validation rules as vector_distance.
2662    pub fn check_vector_similarity(
2663        &self,
2664        args: &[TypedExpr],
2665        span: Span,
2666    ) -> Result<ResolvedType, PlannerError> {
2667        // Same validation as vector_distance
2668        self.check_vector_distance(args, span)
2669    }
2670
2671    /// Check that two vector dimensions match.
2672    ///
2673    /// # Errors
2674    ///
2675    /// Returns `PlannerError::VectorDimensionMismatch` if dimensions don't match.
2676    pub fn check_vector_dimension(
2677        &self,
2678        expected: u32,
2679        found: u32,
2680        span: Span,
2681    ) -> Result<(), PlannerError> {
2682        if expected != found {
2683            Err(PlannerError::VectorDimensionMismatch {
2684                expected,
2685                found,
2686                line: span.start.line,
2687                column: span.start.column,
2688            })
2689        } else {
2690            Ok(())
2691        }
2692    }
2693
2694    // ============================================================
2695    // INSERT/UPDATE Type Checking Methods (Task 13)
2696    // ============================================================
2697
2698    /// Check INSERT values against table columns.
2699    ///
2700    /// Validates that:
2701    /// - The number of values matches the number of columns
2702    /// - Each value's type is compatible with the column type
2703    /// - NOT NULL constraints are satisfied
2704    /// - Vector dimensions match for vector columns
2705    ///
2706    /// # Column Order
2707    ///
2708    /// If `columns` is empty, uses `TableMetadata.column_names()` order (definition order).
2709    ///
2710    /// # Errors
2711    ///
2712    /// - `ColumnValueCountMismatch`: Number of values doesn't match columns
2713    /// - `TypeMismatch`: Value type incompatible with column type
2714    /// - `NullConstraintViolation`: NULL value for NOT NULL column
2715    /// - `VectorDimensionMismatch`: Vector dimension mismatch
2716    pub fn check_insert_values(
2717        &self,
2718        table: &TableMetadata,
2719        columns: &[String],
2720        values: &[Vec<Expr>],
2721        span: Span,
2722    ) -> Result<Vec<Vec<TypedExpr>>, PlannerError> {
2723        // Determine the target columns
2724        let target_columns: Vec<&str> = if columns.is_empty() {
2725            table.column_names()
2726        } else {
2727            columns.iter().map(|s| s.as_str()).collect()
2728        };
2729
2730        let mut typed_rows = Vec::with_capacity(values.len());
2731
2732        for row in values {
2733            // Check value count matches column count
2734            if row.len() != target_columns.len() {
2735                return Err(PlannerError::ColumnValueCountMismatch {
2736                    columns: target_columns.len(),
2737                    values: row.len(),
2738                    line: span.start.line,
2739                    column: span.start.column,
2740                });
2741            }
2742
2743            let mut typed_values = Vec::with_capacity(row.len());
2744
2745            for (value, col_name) in row.iter().zip(target_columns.iter()) {
2746                // Get column metadata
2747                let col_meta =
2748                    table
2749                        .get_column(col_name)
2750                        .ok_or_else(|| PlannerError::ColumnNotFound {
2751                            column: col_name.to_string(),
2752                            table: table.name.clone(),
2753                            line: span.start.line,
2754                            col: span.start.column,
2755                        })?;
2756
2757                // Type-check the value expression
2758                let typed_value = self.infer_type(value, table)?;
2759
2760                // Check NOT NULL constraint
2761                self.check_null_constraint(col_meta, &typed_value, value.span)?;
2762
2763                // Check type compatibility
2764                self.check_type_compatibility(
2765                    &col_meta.data_type,
2766                    &typed_value.resolved_type,
2767                    value.span,
2768                )?;
2769
2770                let typed_value =
2771                    self.coerce_column_value(&col_meta.data_type, typed_value, value.span);
2772
2773                // For vector types, also check dimension
2774                if let (
2775                    ResolvedType::Vector {
2776                        dimension: expected_dim,
2777                        ..
2778                    },
2779                    ResolvedType::Vector {
2780                        dimension: actual_dim,
2781                        ..
2782                    },
2783                ) = (&col_meta.data_type, &typed_value.resolved_type)
2784                {
2785                    self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
2786                }
2787
2788                typed_values.push(typed_value);
2789            }
2790
2791            typed_rows.push(typed_values);
2792        }
2793
2794        Ok(typed_rows)
2795    }
2796
2797    /// Check UPDATE assignment type compatibility.
2798    ///
2799    /// Validates that the value's type is compatible with the column type.
2800    ///
2801    /// # Errors
2802    ///
2803    /// - `ColumnNotFound`: Column doesn't exist
2804    /// - `TypeMismatch`: Value type incompatible with column type
2805    /// - `NullConstraintViolation`: NULL value for NOT NULL column
2806    /// - `VectorDimensionMismatch`: Vector dimension mismatch
2807    pub fn check_assignment(
2808        &self,
2809        table: &TableMetadata,
2810        column: &str,
2811        value: &Expr,
2812        span: Span,
2813    ) -> Result<TypedExpr, PlannerError> {
2814        // Get column metadata
2815        let col_meta = table
2816            .get_column(column)
2817            .ok_or_else(|| PlannerError::ColumnNotFound {
2818                column: column.to_string(),
2819                table: table.name.clone(),
2820                line: span.start.line,
2821                col: span.start.column,
2822            })?;
2823
2824        // Type-check the value expression
2825        let typed_value = self.infer_type(value, table)?;
2826
2827        // Check NOT NULL constraint
2828        self.check_null_constraint(col_meta, &typed_value, value.span)?;
2829
2830        // Check type compatibility
2831        self.check_type_compatibility(&col_meta.data_type, &typed_value.resolved_type, value.span)?;
2832
2833        let typed_value = self.coerce_column_value(&col_meta.data_type, typed_value, value.span);
2834
2835        // For vector types, also check dimension
2836        if let (
2837            ResolvedType::Vector {
2838                dimension: expected_dim,
2839                ..
2840            },
2841            ResolvedType::Vector {
2842                dimension: actual_dim,
2843                ..
2844            },
2845        ) = (&col_meta.data_type, &typed_value.resolved_type)
2846        {
2847            self.check_vector_dimension(*expected_dim, *actual_dim, value.span)?;
2848        }
2849
2850        Ok(typed_value)
2851    }
2852
2853    /// Check NOT NULL constraint for a value.
2854    ///
2855    /// # Errors
2856    ///
2857    /// Returns `PlannerError::NullConstraintViolation` if the column has NOT NULL
2858    /// constraint and the value is NULL.
2859    pub fn check_null_constraint(
2860        &self,
2861        column: &crate::catalog::ColumnMetadata,
2862        value: &TypedExpr,
2863        span: Span,
2864    ) -> Result<(), PlannerError> {
2865        if column.not_null && matches!(value.resolved_type, ResolvedType::Null) {
2866            Err(PlannerError::NullConstraintViolation {
2867                column: column.name.clone(),
2868                line: span.start.line,
2869                col: span.start.column,
2870            })
2871        } else {
2872            Ok(())
2873        }
2874    }
2875
2876    /// Check type compatibility between expected and actual types.
2877    ///
2878    /// Uses implicit type conversion rules defined in `ResolvedType::can_cast_to`.
2879    ///
2880    /// # Errors
2881    ///
2882    /// Returns `PlannerError::TypeMismatch` if types are incompatible.
2883    fn check_type_compatibility(
2884        &self,
2885        expected: &ResolvedType,
2886        actual: &ResolvedType,
2887        span: Span,
2888    ) -> Result<(), PlannerError> {
2889        // Same type is always compatible
2890        if expected == actual {
2891            return Ok(());
2892        }
2893
2894        // Check if implicit cast is allowed
2895        if actual.can_cast_to(expected) {
2896            return Ok(());
2897        }
2898
2899        // Special case: Vector types with same dimension but different metric are compatible
2900        // (the column's metric is used)
2901        if let (
2902            ResolvedType::Vector {
2903                dimension: d1,
2904                metric: _,
2905            },
2906            ResolvedType::Vector {
2907                dimension: d2,
2908                metric: _,
2909            },
2910        ) = (expected, actual)
2911        {
2912            // Dimensions must match for vector compatibility
2913            if *d1 == *d2 {
2914                return Ok(());
2915            }
2916            // Different dimensions will fall through to TypeMismatch error
2917        }
2918
2919        Err(PlannerError::TypeMismatch {
2920            expected: expected.type_name().to_string(),
2921            found: actual.type_name().to_string(),
2922            line: span.start.line,
2923            column: span.start.column,
2924        })
2925    }
2926
2927    /// Insert an execution-time coercion where a column accepts a value whose
2928    /// source representation differs from its storage representation.
2929    fn coerce_column_value(
2930        &self,
2931        expected: &ResolvedType,
2932        value: TypedExpr,
2933        span: Span,
2934    ) -> TypedExpr {
2935        if value.resolved_type != *expected
2936            && value.resolved_type != ResolvedType::Null
2937            && matches!(
2938                expected,
2939                ResolvedType::Integer
2940                    | ResolvedType::BigInt
2941                    | ResolvedType::Float
2942                    | ResolvedType::Double
2943                    | ResolvedType::Timestamp
2944            )
2945        {
2946            TypedExpr::cast(value, expected.clone(), span)
2947        } else {
2948            value
2949        }
2950    }
2951}
2952
2953fn is_numeric_type(ty: &ResolvedType) -> bool {
2954    matches!(
2955        ty,
2956        ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Float | ResolvedType::Double
2957    )
2958}
2959
2960fn validate_window_frame(
2961    function_name: &str,
2962    frame: &WindowFrame,
2963    order_by: &[SortExpr],
2964) -> Result<(), PlannerError> {
2965    if !matches!(
2966        function_name,
2967        "sum" | "count" | "avg" | "min" | "max" | "first_value" | "last_value" | "nth_value"
2968    ) {
2969        return Err(PlannerError::invalid_expression(format!(
2970            "explicit window frames are only supported for aggregate functions and \
2971             FIRST_VALUE/LAST_VALUE/NTH_VALUE, not {}()",
2972            function_name.to_ascii_uppercase()
2973        )));
2974    }
2975    if order_by.is_empty() {
2976        return Err(PlannerError::invalid_expression(
2977            "explicit ROWS/RANGE window frames require ORDER BY for deterministic evaluation",
2978        ));
2979    }
2980    if matches!(frame.start_bound, WindowFrameBound::UnboundedFollowing) {
2981        return Err(PlannerError::invalid_expression(
2982            "window frame start cannot be UNBOUNDED FOLLOWING",
2983        ));
2984    }
2985    if matches!(frame.end_bound, WindowFrameBound::UnboundedPreceding) {
2986        return Err(PlannerError::invalid_expression(
2987            "window frame end cannot be UNBOUNDED PRECEDING",
2988        ));
2989    }
2990    if (matches!(frame.start_bound, WindowFrameBound::CurrentRow)
2991        && matches!(frame.end_bound, WindowFrameBound::Preceding(_)))
2992        || (matches!(frame.start_bound, WindowFrameBound::Following(_))
2993            && matches!(
2994                frame.end_bound,
2995                WindowFrameBound::Preceding(_) | WindowFrameBound::CurrentRow
2996            ))
2997    {
2998        return Err(PlannerError::invalid_expression(
2999            "window frame bounds are reversed",
3000        ));
3001    }
3002
3003    let has_offset = matches!(
3004        frame.start_bound,
3005        WindowFrameBound::Preceding(_) | WindowFrameBound::Following(_)
3006    ) || matches!(
3007        frame.end_bound,
3008        WindowFrameBound::Preceding(_) | WindowFrameBound::Following(_)
3009    );
3010    if frame.units == WindowFrameUnits::Range && has_offset {
3011        if order_by.len() != 1 {
3012            return Err(PlannerError::invalid_expression(
3013                "RANGE offset frames require exactly one ORDER BY expression",
3014            ));
3015        }
3016        if !is_numeric_type(&order_by[0].expr.resolved_type) {
3017            return Err(PlannerError::invalid_expression(format!(
3018                "RANGE offset ORDER BY expression must be numeric, found {:?}",
3019                order_by[0].expr.resolved_type
3020            )));
3021        }
3022    }
3023    Ok(())
3024}
3025
3026fn validate_offset_window_call(
3027    name: &str,
3028    arg_count: usize,
3029    distinct: bool,
3030    star: bool,
3031) -> Result<(), PlannerError> {
3032    let display_name = name.to_ascii_uppercase();
3033    if distinct {
3034        return Err(PlannerError::invalid_expression(format!(
3035            "{display_name}() window function does not accept DISTINCT"
3036        )));
3037    }
3038    if star {
3039        return Err(PlannerError::invalid_expression(format!(
3040            "{display_name}() window function does not accept a star argument"
3041        )));
3042    }
3043    if !(1..=3).contains(&arg_count) {
3044        return Err(PlannerError::invalid_expression(format!(
3045            "{display_name}() window function expects 1 to 3 arguments"
3046        )));
3047    }
3048    Ok(())
3049}
3050
3051fn validate_exact_window_call(
3052    name: &str,
3053    arg_count: usize,
3054    expected: usize,
3055    distinct: bool,
3056    star: bool,
3057) -> Result<(), PlannerError> {
3058    let display_name = name.to_ascii_uppercase();
3059    if distinct {
3060        return Err(PlannerError::invalid_expression(format!(
3061            "{display_name}() window function does not support DISTINCT"
3062        )));
3063    }
3064    if star {
3065        return Err(PlannerError::invalid_expression(format!(
3066            "{display_name}() window function does not support a star argument"
3067        )));
3068    }
3069    if arg_count != expected {
3070        let signature = match expected {
3071            0 => "no arguments",
3072            1 => "one argument",
3073            2 => "two arguments",
3074            _ => unreachable!("window signatures are bounded above"),
3075        };
3076        return Err(PlannerError::invalid_expression(format!(
3077            "{display_name}() window function takes {signature}"
3078        )));
3079    }
3080    Ok(())
3081}
3082
3083fn validate_positive_integer_argument(
3084    name: &str,
3085    argument: &TypedExpr,
3086) -> Result<(), PlannerError> {
3087    if matches!(
3088        argument.resolved_type,
3089        ResolvedType::Integer | ResolvedType::BigInt | ResolvedType::Null
3090    ) {
3091        return Ok(());
3092    }
3093    Err(PlannerError::type_mismatch(
3094        format!("positive INTEGER {} argument", name.to_ascii_uppercase()),
3095        argument.resolved_type.type_name(),
3096        argument.span,
3097    ))
3098}
3099
3100fn coerce_compatible_result(expr: &mut TypedExpr, target: &ResolvedType) {
3101    if expr.resolved_type == *target || matches!(expr.resolved_type, ResolvedType::Null) {
3102        return;
3103    }
3104    let span = expr.span;
3105    *expr = TypedExpr::cast(expr.clone(), target.clone(), span);
3106}
3107
3108fn coerce_case_result(expr: &mut TypedExpr, target: &ResolvedType) {
3109    if expr.resolved_type == *target || matches!(expr.resolved_type, ResolvedType::Null) {
3110        return;
3111    }
3112    let span = expr.span;
3113    *expr = TypedExpr::cast(expr.clone(), target.clone(), span);
3114}
3115
3116#[derive(Debug, Clone, PartialEq, Eq, Hash)]
3117struct AggregateSignature {
3118    name: String,
3119    distinct: bool,
3120    star: bool,
3121    arg_key: Option<String>,
3122    separator: Option<String>,
3123    /// FILTER (WHERE ...) predicate identity; aggregates that differ only in
3124    /// their filter are distinct physical aggregates (issue #148, D10).
3125    filter_key: Option<String>,
3126    /// Aggregate-local ordering identity. Populated only for order-sensitive
3127    /// aggregates so that a validated-then-discarded ORDER BY (D3) still
3128    /// deduplicates with the unordered call.
3129    order_key: Option<String>,
3130}
3131
3132fn is_aggregate_name(name: &str) -> bool {
3133    matches!(
3134        name.to_ascii_lowercase().as_str(),
3135        "count"
3136            | "sum"
3137            | "total"
3138            | "avg"
3139            | "min"
3140            | "max"
3141            | "group_concat"
3142            | "string_agg"
3143            | "percentile_disc"
3144    )
3145}
3146
3147fn is_ordered_set_aggregate_name(name: &str) -> bool {
3148    // Issue #154 adds percentile_cont / mode here.
3149    name.eq_ignore_ascii_case("percentile_disc")
3150}
3151
3152/// Order identity participates in the signature only where ordering changes
3153/// the result (D3): order-insensitive aggregates discard their validated
3154/// ORDER BY, and their signature must match the unordered spelling.
3155fn is_order_sensitive_aggregate_name(name: &str) -> bool {
3156    matches!(
3157        name.to_ascii_lowercase().as_str(),
3158        "group_concat" | "string_agg" | "percentile_disc"
3159    )
3160}
3161
3162/// Extract and validate the `PERCENTILE_DISC` fraction literal (D5): a
3163/// numeric literal (optionally negated) inside `[0, 1]`.
3164pub(crate) fn percentile_fraction(arg: &TypedExpr) -> Result<f64, PlannerError> {
3165    let literal = match &arg.kind {
3166        TypedExprKind::Literal(Literal::Number(text)) => text.parse::<f64>().ok(),
3167        TypedExprKind::UnaryOp {
3168            op: crate::ast::expr::UnaryOp::Minus,
3169            operand,
3170        } => match &operand.kind {
3171            TypedExprKind::Literal(Literal::Number(text)) => {
3172                text.parse::<f64>().ok().map(|value| -value)
3173            }
3174            _ => None,
3175        },
3176        _ => None,
3177    };
3178    let Some(value) = literal else {
3179        return Err(PlannerError::invalid_expression(
3180            "PERCENTILE_DISC fraction must be a numeric literal".to_string(),
3181        ));
3182    };
3183    if !(0.0..=1.0).contains(&value) {
3184        return Err(PlannerError::invalid_expression(
3185            "PERCENTILE_DISC fraction must be between 0 and 1".to_string(),
3186        ));
3187    }
3188    Ok(value)
3189}
3190
3191fn typed_sort_signature(order_by: &[SortExpr]) -> Option<String> {
3192    if order_by.is_empty() {
3193        return None;
3194    }
3195    Some(
3196        order_by
3197            .iter()
3198            .map(|sort| {
3199                format!(
3200                    "{}|{}|{}",
3201                    typed_expr_signature(&sort.expr),
3202                    sort.asc,
3203                    sort.nulls_first
3204                )
3205            })
3206            .collect::<Vec<_>>()
3207            .join(","),
3208    )
3209}
3210
3211fn aggregate_signature_from_expr(expr: &AggregateExpr) -> AggregateSignature {
3212    let (name, separator, star, arg) = match &expr.function {
3213        AggregateFunction::Count => (
3214            "count".to_string(),
3215            None,
3216            expr.arg.is_none(),
3217            expr.arg.as_ref(),
3218        ),
3219        AggregateFunction::Sum => ("sum".to_string(), None, false, expr.arg.as_ref()),
3220        AggregateFunction::Total => ("total".to_string(), None, false, expr.arg.as_ref()),
3221        AggregateFunction::Avg => ("avg".to_string(), None, false, expr.arg.as_ref()),
3222        AggregateFunction::Min => ("min".to_string(), None, false, expr.arg.as_ref()),
3223        AggregateFunction::Max => ("max".to_string(), None, false, expr.arg.as_ref()),
3224        AggregateFunction::GroupConcat { separator } => (
3225            "group_concat".to_string(),
3226            separator.clone(),
3227            false,
3228            expr.arg.as_ref(),
3229        ),
3230        AggregateFunction::StringAgg { separator } => (
3231            "string_agg".to_string(),
3232            separator.clone(),
3233            false,
3234            expr.arg.as_ref(),
3235        ),
3236        // The sort value lives in `order_key`; the fraction rides the
3237        // separator slot so both signature constructions stay symmetric.
3238        AggregateFunction::PercentileDisc { fraction } => (
3239            "percentile_disc".to_string(),
3240            Some(format!("{fraction:?}")),
3241            false,
3242            None,
3243        ),
3244    };
3245    AggregateSignature {
3246        name,
3247        distinct: expr.distinct,
3248        star,
3249        arg_key: arg.map(typed_expr_signature),
3250        separator,
3251        filter_key: expr.filter.as_ref().map(typed_expr_signature),
3252        order_key: typed_sort_signature(&expr.order_by),
3253    }
3254}
3255
3256fn aggregate_signature_from_call(
3257    name: &str,
3258    args: &[TypedExpr],
3259    distinct: bool,
3260    star: bool,
3261    filter: Option<&TypedExpr>,
3262    order_by: &[SortExpr],
3263) -> Result<AggregateSignature, PlannerError> {
3264    let is_percentile = name.eq_ignore_ascii_case("percentile_disc");
3265    let separator = if name.eq_ignore_ascii_case("group_concat") && args.len() == 2 {
3266        if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
3267            Some(value.clone())
3268        } else {
3269            return Err(PlannerError::invalid_expression(
3270                "GROUP_CONCAT separator must be a string literal".to_string(),
3271            ));
3272        }
3273    } else if name.eq_ignore_ascii_case("string_agg") && args.len() == 2 {
3274        if let TypedExprKind::Literal(Literal::String(value)) = &args[1].kind {
3275            Some(value.clone())
3276        } else {
3277            return Err(PlannerError::invalid_expression(
3278                "STRING_AGG separator must be a string literal".to_string(),
3279            ));
3280        }
3281    } else if is_percentile && args.len() == 1 {
3282        Some(format!("{:?}", percentile_fraction(&args[0])?))
3283    } else {
3284        None
3285    };
3286    Ok(AggregateSignature {
3287        name: name.to_ascii_lowercase(),
3288        distinct,
3289        star,
3290        arg_key: if is_percentile {
3291            None
3292        } else {
3293            args.first().map(typed_expr_signature)
3294        },
3295        separator,
3296        filter_key: filter.map(typed_expr_signature),
3297        order_key: if is_order_sensitive_aggregate_name(name) {
3298            typed_sort_signature(order_by)
3299        } else {
3300            None
3301        },
3302    })
3303}
3304
3305fn typed_expr_signature(expr: &TypedExpr) -> String {
3306    format!("{:?}", expr.kind)
3307}
3308
3309fn single_column_type(schema: &[ColumnMetadata], span: Span) -> Result<ResolvedType, PlannerError> {
3310    match schema {
3311        [column] => Ok(column.data_type.clone()),
3312        [] => Err(PlannerError::type_mismatch(
3313            "one-column subquery",
3314            "zero-column subquery",
3315            span,
3316        )),
3317        _ => Err(PlannerError::type_mismatch(
3318            "one-column subquery",
3319            format!("{} columns", schema.len()),
3320            span,
3321        )),
3322    }
3323}
3324
3325fn row_items(expr: &Expr) -> Option<&[Expr]> {
3326    match &expr.kind {
3327        ExprKind::Row { items } => Some(items),
3328        _ => None,
3329    }
3330}
3331
3332fn internal_predicate(name: String, args: Vec<TypedExpr>, span: Span) -> TypedExpr {
3333    TypedExpr::function_call(name, args, false, false, ResolvedType::Boolean, span)
3334}
3335
3336// Tests are in type_checker/tests.rs
3337#[cfg(test)]
3338#[path = "type_checker/tests.rs"]
3339mod tests;