Skip to main content

fsqlite_parser/
semantic.rs

1//! Semantic analysis: name resolution, type checking, and scope validation.
2//!
3//! Validates AST nodes against a schema to ensure:
4//! - Column references resolve to known tables/columns
5//! - Table aliases are unique within a query scope
6//! - Function arity matches known functions
7//! - CTE names are visible in the correct scope
8//! - Type affinity is tracked for expression results
9//!
10//! # Usage
11//!
12//! ```ignore
13//! let schema = Schema::new();
14//! schema.add_table(TableDef { name: "users", columns: vec![...] });
15//! let mut resolver = Resolver::new(&schema);
16//! let errors = resolver.resolve_statement(&stmt);
17//! ```
18
19use std::collections::{HashMap, HashSet};
20use std::sync::atomic::{AtomicU64, Ordering};
21
22use fsqlite_ast::{
23    ColumnRef, Expr, FromClause, FunctionArgs, InSet, JoinClause, JoinConstraint, ResultColumn,
24    SelectCore, SelectStatement, Statement, TableOrSubquery,
25};
26use fsqlite_types::TypeAffinity;
27
28// ---------------------------------------------------------------------------
29// Metrics
30// ---------------------------------------------------------------------------
31
32/// Monotonic counter of semantic errors encountered.
33static FSQLITE_SEMANTIC_ERRORS_TOTAL: AtomicU64 = AtomicU64::new(0);
34
35/// Point-in-time snapshot of semantic analysis metrics.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37pub struct SemanticMetricsSnapshot {
38    pub fsqlite_semantic_errors_total: u64,
39}
40
41/// Take a point-in-time snapshot of semantic metrics.
42#[must_use]
43pub fn semantic_metrics_snapshot() -> SemanticMetricsSnapshot {
44    SemanticMetricsSnapshot {
45        fsqlite_semantic_errors_total: FSQLITE_SEMANTIC_ERRORS_TOTAL.load(Ordering::Relaxed),
46    }
47}
48
49/// Reset semantic metrics.
50pub fn reset_semantic_metrics() {
51    FSQLITE_SEMANTIC_ERRORS_TOTAL.store(0, Ordering::Relaxed);
52}
53
54// ---------------------------------------------------------------------------
55// Schema types
56// ---------------------------------------------------------------------------
57
58/// A column definition in the schema.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct ColumnDef {
61    /// Column name (stored in original case).
62    pub name: String,
63    /// Type affinity determined from the DDL type name.
64    pub affinity: TypeAffinity,
65    /// Whether this column is an INTEGER PRIMARY KEY (rowid alias).
66    pub is_ipk: bool,
67    /// Whether this column has a NOT NULL constraint.
68    pub not_null: bool,
69}
70
71/// A table definition in the schema.
72#[derive(Debug, Clone)]
73pub struct TableDef {
74    /// Table name.
75    pub name: String,
76    /// Column definitions in declaration order.
77    pub columns: Vec<ColumnDef>,
78    /// Whether this is a WITHOUT ROWID table.
79    pub without_rowid: bool,
80    /// Whether this is a STRICT table.
81    pub strict: bool,
82}
83
84impl TableDef {
85    /// Find a column by name (case-insensitive).
86    #[must_use]
87    pub fn find_column(&self, name: &str) -> Option<&ColumnDef> {
88        self.columns
89            .iter()
90            .find(|c| c.name.eq_ignore_ascii_case(name))
91    }
92
93    /// Check if this table has a column with the given name (case-insensitive).
94    #[must_use]
95    pub fn has_column(&self, name: &str) -> bool {
96        self.find_column(name).is_some()
97    }
98
99    /// Check if a name is a rowid alias for this table.
100    #[must_use]
101    pub fn is_rowid_alias(&self, name: &str) -> bool {
102        if self.without_rowid {
103            return false;
104        }
105        let lower = name.to_ascii_lowercase();
106        matches!(lower.as_str(), "rowid" | "_rowid_" | "oid")
107            || self
108                .columns
109                .iter()
110                .any(|c| c.is_ipk && c.name.eq_ignore_ascii_case(name))
111    }
112}
113
114/// The database schema: a collection of table definitions.
115#[derive(Debug, Clone, Default)]
116pub struct Schema {
117    /// Tables by lowercase name.
118    tables: HashMap<String, TableDef>,
119}
120
121impl Schema {
122    /// Create an empty schema.
123    #[must_use]
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Add a table definition.
129    pub fn add_table(&mut self, table: TableDef) {
130        self.tables
131            .insert(table.name.to_ascii_lowercase(), table);
132    }
133
134    /// Look up a table by name (case-insensitive).
135    #[must_use]
136    pub fn find_table(&self, name: &str) -> Option<&TableDef> {
137        self.tables.get(&name.to_ascii_lowercase())
138    }
139
140    /// Number of tables in the schema.
141    #[must_use]
142    pub fn table_count(&self) -> usize {
143        self.tables.len()
144    }
145}
146
147// ---------------------------------------------------------------------------
148// Scope tracking
149// ---------------------------------------------------------------------------
150
151/// A name scope for query resolution. Scopes nest for subqueries and CTEs.
152#[derive(Debug, Clone)]
153pub struct Scope {
154    /// Table aliases visible in this scope: alias → table name.
155    aliases: HashMap<String, String>,
156    /// Columns visible from each alias: alias → set of column names.
157    /// None means the columns are unknown (CTE or subquery), so any column reference is optimistically accepted.
158    columns: HashMap<String, Option<HashSet<String>>>,
159    /// CTE names visible in this scope.
160    ctes: HashSet<String>,
161    /// Parent scope (for subquery nesting).
162    parent: Option<Box<Self>>,
163}
164
165impl Scope {
166    /// Create a root scope.
167    #[must_use]
168    pub fn root() -> Self {
169        Self {
170            aliases: HashMap::new(),
171            columns: HashMap::new(),
172            ctes: HashSet::new(),
173            parent: None,
174        }
175    }
176
177    /// Create a child scope (for subqueries).
178    #[must_use]
179    pub fn child(parent: Self) -> Self {
180        Self {
181            aliases: HashMap::new(),
182            columns: HashMap::new(),
183            ctes: HashSet::new(),
184            parent: Some(Box::new(parent)),
185        }
186    }
187
188    /// Register a table alias with its columns.
189    pub fn add_alias(&mut self, alias: &str, table_name: &str, columns: Option<HashSet<String>>) {
190        let key = alias.to_ascii_lowercase();
191        self.aliases.insert(key.clone(), table_name.to_owned());
192        self.columns.insert(key, columns);
193    }
194
195    /// Register a CTE name.
196    pub fn add_cte(&mut self, name: &str) {
197        self.ctes.insert(name.to_ascii_lowercase());
198    }
199
200    /// Check if an alias is visible in this scope (or parent scopes).
201    #[must_use]
202    pub fn has_alias(&self, alias: &str) -> bool {
203        let key = alias.to_ascii_lowercase();
204        if self.aliases.contains_key(&key) || self.ctes.contains(&key) {
205            return true;
206        }
207        self.parent.as_ref().is_some_and(|p| p.has_alias(alias))
208    }
209
210    /// Resolve a column reference: find which alias provides it.
211    ///
212    /// If `table_qualifier` is Some, checks only that alias.
213    /// If None, searches all visible aliases for the column name.
214    /// Returns the resolved (alias, column_name) or None.
215    #[must_use]
216    pub fn resolve_column(
217        &self,
218        table_qualifier: Option<&str>,
219        column_name: &str,
220    ) -> ResolveResult {
221        let col_lower = column_name.to_ascii_lowercase();
222
223        if let Some(qualifier) = table_qualifier {
224            let key = qualifier.to_ascii_lowercase();
225            if let Some(cols) = self.columns.get(&key) {
226                if cols.as_ref().is_none_or(|c| c.contains(&col_lower)) {
227                    return ResolveResult::Resolved(key);
228                }
229                return ResolveResult::ColumnNotFound;
230            }
231            // Check parent scope.
232            if let Some(ref parent) = self.parent {
233                return parent.resolve_column(table_qualifier, column_name);
234            }
235            return ResolveResult::TableNotFound;
236        }
237
238        // Unqualified: search all aliases in this scope.
239        let mut matches = Vec::new();
240        for (alias, cols) in &self.columns {
241            if cols.as_ref().is_none_or(|c| c.contains(&col_lower)) {
242                matches.push(alias.clone());
243            }
244        }
245
246        match matches.len() {
247            0 => {
248                // Check parent scope.
249                if let Some(ref parent) = self.parent {
250                    return parent.resolve_column(None, column_name);
251                }
252                ResolveResult::ColumnNotFound
253            }
254            1 => ResolveResult::Resolved(matches.into_iter().next().unwrap()),
255            _ => ResolveResult::Ambiguous(matches),
256        }
257    }
258
259    /// Number of aliases registered in this scope (not counting parents).
260    #[must_use]
261    pub fn alias_count(&self) -> usize {
262        self.aliases.len()
263    }
264}
265
266/// Result of resolving a column reference.
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub enum ResolveResult {
269    /// Column resolved to the given alias.
270    Resolved(String),
271    /// The table qualifier was not found.
272    TableNotFound,
273    /// The column was not found in the specified table.
274    ColumnNotFound,
275    /// The column was found in multiple tables (ambiguous).
276    Ambiguous(Vec<String>),
277}
278
279// ---------------------------------------------------------------------------
280// Semantic errors
281// ---------------------------------------------------------------------------
282
283/// A semantic analysis error.
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct SemanticError {
286    /// Error kind.
287    pub kind: SemanticErrorKind,
288    /// Human-readable message.
289    pub message: String,
290}
291
292/// Kinds of semantic errors.
293#[derive(Debug, Clone, PartialEq, Eq)]
294pub enum SemanticErrorKind {
295    /// Column reference could not be resolved.
296    UnresolvedColumn {
297        table: Option<String>,
298        column: String,
299    },
300    /// Column reference is ambiguous (exists in multiple tables).
301    AmbiguousColumn {
302        column: String,
303        candidates: Vec<String>,
304    },
305    /// Table or alias not found.
306    UnresolvedTable { name: String },
307    /// Duplicate alias in the same scope.
308    DuplicateAlias { alias: String },
309    /// Function called with wrong number of arguments.
310    FunctionArityMismatch {
311        function: String,
312        expected: FunctionArity,
313        actual: usize,
314    },
315    /// Type coercion warning (not fatal).
316    ImplicitTypeCoercion {
317        from: TypeAffinity,
318        to: TypeAffinity,
319        context: String,
320    },
321}
322
323/// Expected function arity.
324#[derive(Debug, Clone, PartialEq, Eq)]
325pub enum FunctionArity {
326    /// Exact number of arguments.
327    Exact(usize),
328    /// Range of acceptable argument counts.
329    Range(usize, usize),
330    /// Any number of arguments.
331    Variadic,
332}
333
334impl std::fmt::Display for SemanticError {
335    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336        write!(f, "{}", self.message)
337    }
338}
339
340// ---------------------------------------------------------------------------
341// Resolver
342// ---------------------------------------------------------------------------
343
344/// The semantic analyzer / name resolver.
345///
346/// Given a `Schema` and an AST, validates all name references and collects
347/// errors. Uses scope tracking for nested queries and CTEs.
348pub struct Resolver<'a> {
349    schema: &'a Schema,
350    errors: Vec<SemanticError>,
351    tables_resolved: u64,
352    columns_bound: u64,
353}
354
355impl<'a> Resolver<'a> {
356    /// Create a new resolver for the given schema.
357    #[must_use]
358    pub fn new(schema: &'a Schema) -> Self {
359        Self {
360            schema,
361            errors: Vec::new(),
362            tables_resolved: 0,
363            columns_bound: 0,
364        }
365    }
366
367    /// Resolve all name references in a statement.
368    ///
369    /// Returns the list of semantic errors found.
370    pub fn resolve_statement(&mut self, stmt: &Statement) -> Vec<SemanticError> {
371        let span = tracing::debug_span!(
372            target: "fsqlite.parse",
373            "semantic_analysis",
374            tables_resolved = tracing::field::Empty,
375            columns_bound = tracing::field::Empty,
376            errors = tracing::field::Empty,
377        );
378        let _guard = span.enter();
379
380        self.errors.clear();
381        self.tables_resolved = 0;
382        self.columns_bound = 0;
383
384        let mut scope = Scope::root();
385        self.resolve_stmt_inner(stmt, &mut scope);
386
387        span.record("tables_resolved", self.tables_resolved);
388        span.record("columns_bound", self.columns_bound);
389        span.record("errors", self.errors.len() as u64);
390
391        // Record error metrics.
392        if !self.errors.is_empty() {
393            FSQLITE_SEMANTIC_ERRORS_TOTAL
394                .fetch_add(self.errors.len() as u64, Ordering::Relaxed);
395        }
396
397        self.errors.clone()
398    }
399
400    fn resolve_stmt_inner(&mut self, stmt: &Statement, scope: &mut Scope) {
401        match stmt {
402            Statement::Select(select) => self.resolve_select(select, scope),
403            Statement::Insert(insert) => {
404                self.resolve_table_name(&insert.table.name, scope);
405            }
406            Statement::Update(update) => {
407                self.resolve_table_name(&update.table.name.name, scope);
408            }
409            Statement::Delete(delete) => {
410                self.resolve_table_name(&delete.table.name.name, scope);
411            }
412            // DDL and control statements don't need name resolution.
413            _ => {}
414        }
415    }
416
417    fn resolve_select(&mut self, select: &SelectStatement, scope: &mut Scope) {
418        // Register CTEs first (they are visible in the entire WITH scope).
419        if let Some(ref with) = select.with {
420            for cte in &with.ctes {
421                scope.add_cte(&cte.name);
422            }
423        }
424
425        // Resolve the primary select core.
426        self.resolve_select_core(&select.body.select, scope);
427
428        // Resolve any compound queries (UNION, INTERSECT, EXCEPT).
429        for (_op, core) in &select.body.compounds {
430            self.resolve_select_core(core, scope);
431        }
432    }
433
434    fn resolve_select_core(&mut self, core: &SelectCore, scope: &mut Scope) {
435        match core {
436            SelectCore::Select {
437                columns,
438                from,
439                where_clause,
440                group_by,
441                having,
442                ..
443            } => {
444                // Resolve FROM clause first (registers table aliases).
445                if let Some(from) = from {
446                    self.resolve_from(from, scope);
447                }
448
449                // Resolve column references in SELECT list.
450                for col in columns {
451                    self.resolve_result_column(col, scope);
452                }
453
454                // Resolve WHERE clause.
455                if let Some(where_expr) = where_clause {
456                    self.resolve_expr(where_expr, scope);
457                }
458
459                // Resolve GROUP BY.
460                for expr in group_by {
461                    self.resolve_expr(expr, scope);
462                }
463
464                // Resolve HAVING.
465                if let Some(having_expr) = having {
466                    self.resolve_expr(having_expr, scope);
467                }
468            }
469            SelectCore::Values(_) => {
470                // VALUES doesn't reference columns.
471            }
472        }
473    }
474
475    fn resolve_from(&mut self, from: &FromClause, scope: &mut Scope) {
476        self.resolve_table_or_subquery(&from.source, scope);
477
478        for join in &from.joins {
479            self.resolve_join(join, scope);
480        }
481    }
482
483    fn resolve_table_or_subquery(&mut self, tos: &TableOrSubquery, scope: &mut Scope) {
484        match tos {
485            TableOrSubquery::Table { name, alias, .. } => {
486                let table_name = &name.name;
487                let alias_name = alias.as_deref().unwrap_or(table_name);
488
489                // Check for duplicate alias.
490                if scope.has_alias(alias_name) {
491                    self.push_error(SemanticErrorKind::DuplicateAlias {
492                        alias: alias_name.to_owned(),
493                    });
494                }
495
496                // Resolve table name against schema or CTEs.
497                if scope.ctes.contains(&table_name.to_ascii_lowercase()) {
498                    // CTE reference — columns are unknown at this stage.
499                    scope.add_alias(alias_name, table_name, None);
500                    self.tables_resolved += 1;
501                } else if let Some(table_def) = self.schema.find_table(table_name) {
502                    let col_set: HashSet<String> = table_def
503                        .columns
504                        .iter()
505                        .map(|c| c.name.to_ascii_lowercase())
506                        .collect();
507                    scope.add_alias(alias_name, table_name, Some(col_set));
508                    self.tables_resolved += 1;
509                } else {
510                    self.push_error(SemanticErrorKind::UnresolvedTable {
511                        name: table_name.clone(),
512                    });
513                }
514            }
515            TableOrSubquery::Subquery { query, alias, .. } => {
516                // Resolve subquery in a child scope.
517                let mut child = Scope::child(scope.clone());
518                self.resolve_select(query, &mut child);
519
520                // Register the subquery alias with empty columns (we don't
521                // track subquery output columns at this stage).
522                if let Some(alias) = alias {
523                    scope.add_alias(alias, "<subquery>", None);
524                }
525            }
526            TableOrSubquery::TableFunction {
527                name, alias, ..
528            } => {
529                let alias_name = alias.as_deref().unwrap_or(name);
530                scope.add_alias(alias_name, name, None);
531                self.tables_resolved += 1;
532            }
533            TableOrSubquery::ParenJoin(inner_from) => {
534                self.resolve_from(inner_from, scope);
535            }
536        }
537    }
538
539    fn resolve_join(&mut self, join: &JoinClause, scope: &mut Scope) {
540        self.resolve_table_or_subquery(&join.table, scope);
541        if let Some(ref constraint) = join.constraint {
542            match constraint {
543                JoinConstraint::On(expr) => self.resolve_expr(expr, scope),
544                JoinConstraint::Using(cols) => {
545                    for col in cols {
546                        self.resolve_unqualified_column(col, scope);
547                    }
548                }
549            }
550        }
551    }
552
553    fn resolve_result_column(&mut self, col: &ResultColumn, scope: &Scope) {
554        match col {
555            ResultColumn::Star => {
556                // SELECT * is valid if there's at least one table in scope.
557                if scope.alias_count() == 0 && scope.parent.is_none() {
558                    tracing::warn!(
559                        target: "fsqlite.parse",
560                        "SELECT * with no tables in scope"
561                    );
562                }
563            }
564            ResultColumn::TableStar(table_name) => {
565                if !scope.has_alias(table_name) {
566                    self.push_error(SemanticErrorKind::UnresolvedTable {
567                        name: table_name.clone(),
568                    });
569                }
570            }
571            ResultColumn::Expr { expr, .. } => {
572                self.resolve_expr(expr, scope);
573            }
574        }
575    }
576
577    #[allow(clippy::too_many_lines)]
578    fn resolve_expr(&mut self, expr: &Expr, scope: &Scope) {
579        match expr {
580            Expr::Column(col_ref, _span) => {
581                self.resolve_column_ref(col_ref, scope);
582            }
583            Expr::BinaryOp { left, right, .. } => {
584                self.resolve_expr(left, scope);
585                self.resolve_expr(right, scope);
586            }
587            Expr::UnaryOp { expr: inner, .. }
588            | Expr::Cast { expr: inner, .. }
589            | Expr::Collate { expr: inner, .. }
590            | Expr::IsNull { expr: inner, .. } => {
591                self.resolve_expr(inner, scope);
592            }
593            Expr::Between {
594                expr: inner,
595                low,
596                high,
597                ..
598            } => {
599                self.resolve_expr(inner, scope);
600                self.resolve_expr(low, scope);
601                self.resolve_expr(high, scope);
602            }
603            Expr::In {
604                expr: inner, set, ..
605            } => {
606                self.resolve_expr(inner, scope);
607                match set {
608                    InSet::List(items) => {
609                        for item in items {
610                            self.resolve_expr(item, scope);
611                        }
612                    }
613                    InSet::Subquery(select) => {
614                        let mut child = Scope::child(scope.clone());
615                        self.resolve_select(select, &mut child);
616                    }
617                    InSet::Table(name) => {
618                        self.resolve_table_name(&name.name, scope);
619                    }
620                }
621            }
622            Expr::Like {
623                expr: inner,
624                pattern,
625                escape,
626                ..
627            } => {
628                self.resolve_expr(inner, scope);
629                self.resolve_expr(pattern, scope);
630                if let Some(esc) = escape {
631                    self.resolve_expr(esc, scope);
632                }
633            }
634            Expr::Subquery(select, _)
635            | Expr::Exists {
636                subquery: select, ..
637            } => {
638                let mut child = Scope::child(scope.clone());
639                self.resolve_select(select, &mut child);
640            }
641            Expr::FunctionCall {
642                name, args, filter, ..
643            } => {
644                let arg_slice: &[Expr] = match args {
645                    FunctionArgs::Star => &[],
646                    FunctionArgs::List(list) => list,
647                };
648                self.resolve_function(name, arg_slice, scope);
649                if let Some(filter) = filter {
650                    self.resolve_expr(filter, scope);
651                }
652            }
653            Expr::Case {
654                operand,
655                whens,
656                else_expr,
657                ..
658            } => {
659                if let Some(op) = operand {
660                    self.resolve_expr(op, scope);
661                }
662                for (when_expr, then_expr) in whens {
663                    self.resolve_expr(when_expr, scope);
664                    self.resolve_expr(then_expr, scope);
665                }
666                if let Some(else_e) = else_expr {
667                    self.resolve_expr(else_e, scope);
668                }
669            }
670            Expr::JsonAccess { expr: inner, path, .. } => {
671                self.resolve_expr(inner, scope);
672                self.resolve_expr(path, scope);
673            }
674            Expr::RowValue(exprs, _) => {
675                for e in exprs {
676                    self.resolve_expr(e, scope);
677                }
678            }
679            // Literals, placeholders, and RAISE don't need resolution.
680            Expr::Literal(_, _)
681            | Expr::Placeholder(_, _)
682            | Expr::Raise { .. } => {}
683        }
684    }
685
686    fn resolve_column_ref(&mut self, col_ref: &ColumnRef, scope: &Scope) {
687        let result = scope.resolve_column(col_ref.table.as_deref(), &col_ref.column);
688        match result {
689            ResolveResult::Resolved(_) => {
690                self.columns_bound += 1;
691            }
692            ResolveResult::TableNotFound => {
693                tracing::error!(
694                    target: "fsqlite.parse",
695                    table = ?col_ref.table,
696                    column = %col_ref.column,
697                    "unresolvable table reference"
698                );
699                self.push_error(SemanticErrorKind::UnresolvedColumn {
700                    table: col_ref.table.clone(),
701                    column: col_ref.column.clone(),
702                });
703            }
704            ResolveResult::ColumnNotFound => {
705                tracing::error!(
706                    target: "fsqlite.parse",
707                    table = ?col_ref.table,
708                    column = %col_ref.column,
709                    "unresolvable column reference"
710                );
711                self.push_error(SemanticErrorKind::UnresolvedColumn {
712                    table: col_ref.table.clone(),
713                    column: col_ref.column.clone(),
714                });
715            }
716            ResolveResult::Ambiguous(candidates) => {
717                tracing::error!(
718                    target: "fsqlite.parse",
719                    column = %col_ref.column,
720                    candidates = ?candidates,
721                    "ambiguous column reference"
722                );
723                self.push_error(SemanticErrorKind::AmbiguousColumn {
724                    column: col_ref.column.clone(),
725                    candidates,
726                });
727            }
728        }
729    }
730
731    fn resolve_unqualified_column(&mut self, name: &str, scope: &Scope) {
732        let result = scope.resolve_column(None, name);
733        match result {
734            ResolveResult::Resolved(_) => {
735                self.columns_bound += 1;
736            }
737            ResolveResult::ColumnNotFound | ResolveResult::TableNotFound => {
738                self.push_error(SemanticErrorKind::UnresolvedColumn {
739                    table: None,
740                    column: name.to_owned(),
741                });
742            }
743            ResolveResult::Ambiguous(candidates) => {
744                self.push_error(SemanticErrorKind::AmbiguousColumn {
745                    column: name.to_owned(),
746                    candidates,
747                });
748            }
749        }
750    }
751
752    fn resolve_table_name(&mut self, name: &str, scope: &Scope) {
753        if scope.ctes.contains(&name.to_ascii_lowercase()) || self.schema.find_table(name).is_some()
754        {
755            self.tables_resolved += 1;
756        } else {
757            self.push_error(SemanticErrorKind::UnresolvedTable {
758                name: name.to_owned(),
759            });
760        }
761    }
762
763    fn resolve_function(&mut self, name: &str, args: &[Expr], scope: &Scope) {
764        // Resolve argument expressions.
765        for arg in args {
766            self.resolve_expr(arg, scope);
767        }
768
769        // Validate known function arity.
770        if let Some(expected) = known_function_arity(name) {
771            let actual = args.len();
772            let valid = match &expected {
773                FunctionArity::Exact(n) => actual == *n,
774                FunctionArity::Range(lo, hi) => actual >= *lo && actual <= *hi,
775                FunctionArity::Variadic => true,
776            };
777            if !valid {
778                self.push_error(SemanticErrorKind::FunctionArityMismatch {
779                    function: name.to_owned(),
780                    expected,
781                    actual,
782                });
783            }
784        }
785    }
786
787    fn push_error(&mut self, kind: SemanticErrorKind) {
788        let message = match &kind {
789            SemanticErrorKind::UnresolvedColumn { table, column } => {
790                if let Some(t) = table {
791                    format!("no such column: {t}.{column}")
792                } else {
793                    format!("no such column: {column}")
794                }
795            }
796            SemanticErrorKind::AmbiguousColumn {
797                column, candidates, ..
798            } => {
799                format!(
800                    "ambiguous column name: {column} (candidates: {})",
801                    candidates.join(", ")
802                )
803            }
804            SemanticErrorKind::UnresolvedTable { name } => {
805                format!("no such table: {name}")
806            }
807            SemanticErrorKind::DuplicateAlias { alias } => {
808                format!("duplicate alias: {alias}")
809            }
810            SemanticErrorKind::FunctionArityMismatch {
811                function,
812                expected,
813                actual,
814            } => {
815                format!(
816                    "wrong number of arguments to function {function}: expected {expected:?}, got {actual}"
817                )
818            }
819            SemanticErrorKind::ImplicitTypeCoercion {
820                from, to, context, ..
821            } => {
822                format!("implicit type coercion from {from:?} to {to:?} in {context}")
823            }
824        };
825
826        self.errors.push(SemanticError { kind, message });
827    }
828}
829
830// ---------------------------------------------------------------------------
831// Known function arity table
832// ---------------------------------------------------------------------------
833
834/// Returns the expected arity for a known SQLite function, if recognized.
835#[must_use]
836fn known_function_arity(name: &str) -> Option<FunctionArity> {
837    match name.to_ascii_lowercase().as_str() {
838        "random" | "changes" | "last_insert_rowid" | "total_changes" => {
839            Some(FunctionArity::Exact(0))
840        }
841        // Aggregate (1-arg) and scalar (1-arg) functions
842        "sum" | "total" | "avg" | "abs" | "hex" | "length" | "lower" | "upper" | "typeof"
843        | "unicode" | "quote" | "zeroblob" | "soundex" | "likelihood" | "randomblob" => {
844            Some(FunctionArity::Exact(1))
845        }
846        "ifnull" | "nullif" | "instr" | "glob" => Some(FunctionArity::Exact(2)),
847        "iif" | "replace" => Some(FunctionArity::Exact(3)),
848        "count" => Some(FunctionArity::Range(0, 1)),
849        "group_concat" | "trim" | "ltrim" | "rtrim" => Some(FunctionArity::Range(1, 2)),
850        "substr" | "substring" | "like" => Some(FunctionArity::Range(2, 3)),
851        // Variadic: aggregates, scalars, date/time, and JSON functions
852        "min" | "max" | "coalesce" | "printf" | "format" | "char" | "date" | "time"
853        | "datetime" | "julianday" | "strftime" | "unixepoch" | "json" | "json_array"
854        | "json_object" | "json_type" | "json_valid" | "json_extract" | "json_insert"
855        | "json_replace" | "json_set" | "json_remove" => Some(FunctionArity::Variadic),
856
857        _ => None, // Unknown function — skip arity check.
858    }
859}
860
861// ---------------------------------------------------------------------------
862// Tests
863// ---------------------------------------------------------------------------
864
865#[cfg(test)]
866mod tests {
867    use super::*;
868    use crate::parser::Parser;
869
870    fn make_schema() -> Schema {
871        let mut schema = Schema::new();
872        schema.add_table(TableDef {
873            name: "users".to_owned(),
874            columns: vec![
875                ColumnDef {
876                    name: "id".to_owned(),
877                    affinity: TypeAffinity::Integer,
878                    is_ipk: true,
879                    not_null: true,
880                },
881                ColumnDef {
882                    name: "name".to_owned(),
883                    affinity: TypeAffinity::Text,
884                    is_ipk: false,
885                    not_null: true,
886                },
887                ColumnDef {
888                    name: "email".to_owned(),
889                    affinity: TypeAffinity::Text,
890                    is_ipk: false,
891                    not_null: false,
892                },
893            ],
894            without_rowid: false,
895            strict: false,
896        });
897        schema.add_table(TableDef {
898            name: "orders".to_owned(),
899            columns: vec![
900                ColumnDef {
901                    name: "id".to_owned(),
902                    affinity: TypeAffinity::Integer,
903                    is_ipk: true,
904                    not_null: true,
905                },
906                ColumnDef {
907                    name: "user_id".to_owned(),
908                    affinity: TypeAffinity::Integer,
909                    is_ipk: false,
910                    not_null: true,
911                },
912                ColumnDef {
913                    name: "amount".to_owned(),
914                    affinity: TypeAffinity::Real,
915                    is_ipk: false,
916                    not_null: false,
917                },
918            ],
919            without_rowid: false,
920            strict: false,
921        });
922        schema
923    }
924
925    fn parse_one(sql: &str) -> Statement {
926        let mut p = Parser::from_sql(sql);
927        let (stmts, errs) = p.parse_all();
928        assert!(errs.is_empty(), "parse errors: {errs:?}");
929        assert_eq!(stmts.len(), 1);
930        stmts.into_iter().next().unwrap()
931    }
932
933    // ── Schema tests ──
934
935    #[test]
936    fn test_schema_find_table_case_insensitive() {
937        let schema = make_schema();
938        assert!(schema.find_table("users").is_some());
939        assert!(schema.find_table("USERS").is_some());
940        assert!(schema.find_table("Users").is_some());
941        assert!(schema.find_table("nonexistent").is_none());
942    }
943
944    #[test]
945    fn test_table_find_column() {
946        let schema = make_schema();
947        let users = schema.find_table("users").unwrap();
948        assert!(users.has_column("id"));
949        assert!(users.has_column("ID"));
950        assert!(!users.has_column("nonexistent"));
951    }
952
953    #[test]
954    fn test_table_rowid_alias() {
955        let schema = make_schema();
956        let users = schema.find_table("users").unwrap();
957        assert!(users.is_rowid_alias("rowid"));
958        assert!(users.is_rowid_alias("_rowid_"));
959        assert!(users.is_rowid_alias("oid"));
960        assert!(users.is_rowid_alias("id")); // IPK
961        assert!(!users.is_rowid_alias("name"));
962    }
963
964    // ── Scope tests ──
965
966    #[test]
967    fn test_scope_resolve_qualified_column() {
968        let mut scope = Scope::root();
969        let cols: HashSet<String> = ["id", "name", "email"]
970            .iter()
971            .map(ToString::to_string)
972            .collect();
973        scope.add_alias("u", "users", Some(cols));
974
975        assert_eq!(
976            scope.resolve_column(Some("u"), "id"),
977            ResolveResult::Resolved("u".to_string())
978        );
979        assert_eq!(
980            scope.resolve_column(Some("u"), "nonexistent"),
981            ResolveResult::ColumnNotFound
982        );
983        assert_eq!(
984            scope.resolve_column(Some("x"), "id"),
985            ResolveResult::TableNotFound
986        );
987    }
988
989    #[test]
990    fn test_scope_resolve_unqualified_column() {
991        let mut scope = Scope::root();
992        scope.add_alias(
993            "u",
994            "users",
995            Some(["id", "name"].iter().map(ToString::to_string).collect()),
996        );
997        scope.add_alias(
998            "o",
999            "orders",
1000            Some(["id", "user_id"].iter().map(ToString::to_string).collect()),
1001        );
1002
1003        // "name" is unique → resolved to "u"
1004        assert_eq!(
1005            scope.resolve_column(None, "name"),
1006            ResolveResult::Resolved("u".to_string())
1007        );
1008
1009        // "user_id" is unique → resolved to "o"
1010        assert_eq!(
1011            scope.resolve_column(None, "user_id"),
1012            ResolveResult::Resolved("o".to_string())
1013        );
1014
1015        // "id" is ambiguous
1016        match scope.resolve_column(None, "id") {
1017            ResolveResult::Ambiguous(candidates) => {
1018                assert_eq!(candidates.len(), 2);
1019            }
1020            other => panic!("expected Ambiguous, got {other:?}"),
1021        }
1022
1023        // "nonexistent" not found
1024        assert_eq!(
1025            scope.resolve_column(None, "nonexistent"),
1026            ResolveResult::ColumnNotFound
1027        );
1028    }
1029
1030    #[test]
1031    fn test_scope_child_inherits_parent() {
1032        let mut parent = Scope::root();
1033        parent.add_alias(
1034            "u",
1035            "users",
1036            Some(["id", "name"].iter().map(ToString::to_string).collect()),
1037        );
1038        let child = Scope::child(parent);
1039
1040        // Child can see parent's columns.
1041        assert_eq!(
1042            child.resolve_column(Some("u"), "id"),
1043            ResolveResult::Resolved("u".to_string())
1044        );
1045    }
1046
1047    // ── Resolver tests ──
1048
1049    #[test]
1050    fn test_resolve_simple_select() {
1051        let schema = make_schema();
1052        let stmt = parse_one("SELECT id, name FROM users");
1053        let mut resolver = Resolver::new(&schema);
1054        let errors = resolver.resolve_statement(&stmt);
1055        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
1056        assert_eq!(resolver.tables_resolved, 1);
1057        assert_eq!(resolver.columns_bound, 2);
1058    }
1059
1060    #[test]
1061    fn test_resolve_qualified_column() {
1062        let schema = make_schema();
1063        let stmt = parse_one("SELECT u.id, u.name FROM users u");
1064        let mut resolver = Resolver::new(&schema);
1065        let errors = resolver.resolve_statement(&stmt);
1066        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
1067        assert_eq!(resolver.tables_resolved, 1);
1068        assert_eq!(resolver.columns_bound, 2);
1069    }
1070
1071    #[test]
1072    fn test_resolve_join() {
1073        let schema = make_schema();
1074        let stmt = parse_one(
1075            "SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id",
1076        );
1077        let mut resolver = Resolver::new(&schema);
1078        let errors = resolver.resolve_statement(&stmt);
1079        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
1080        assert_eq!(resolver.tables_resolved, 2);
1081        assert_eq!(resolver.columns_bound, 4); // u.name, o.amount, u.id, o.user_id
1082    }
1083
1084    #[test]
1085    fn test_resolve_unresolved_table() {
1086        let schema = make_schema();
1087        let stmt = parse_one("SELECT * FROM nonexistent");
1088        let mut resolver = Resolver::new(&schema);
1089        let errors = resolver.resolve_statement(&stmt);
1090        assert_eq!(errors.len(), 1);
1091        assert!(matches!(
1092            errors[0].kind,
1093            SemanticErrorKind::UnresolvedTable { .. }
1094        ));
1095    }
1096
1097    #[test]
1098    fn test_resolve_unresolved_column() {
1099        let schema = make_schema();
1100        let stmt = parse_one("SELECT nonexistent FROM users");
1101        let mut resolver = Resolver::new(&schema);
1102        let errors = resolver.resolve_statement(&stmt);
1103        assert_eq!(errors.len(), 1);
1104        assert!(matches!(
1105            errors[0].kind,
1106            SemanticErrorKind::UnresolvedColumn { .. }
1107        ));
1108    }
1109
1110    #[test]
1111    fn test_resolve_ambiguous_column() {
1112        let schema = make_schema();
1113        let stmt = parse_one("SELECT id FROM users, orders");
1114        let mut resolver = Resolver::new(&schema);
1115        let errors = resolver.resolve_statement(&stmt);
1116        assert_eq!(errors.len(), 1);
1117        assert!(matches!(
1118            errors[0].kind,
1119            SemanticErrorKind::AmbiguousColumn { .. }
1120        ));
1121    }
1122
1123    #[test]
1124    fn test_resolve_where_clause() {
1125        let schema = make_schema();
1126        let stmt = parse_one("SELECT name FROM users WHERE id > 10");
1127        let mut resolver = Resolver::new(&schema);
1128        let errors = resolver.resolve_statement(&stmt);
1129        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
1130        assert_eq!(resolver.columns_bound, 2); // name, id
1131    }
1132
1133    #[test]
1134    fn test_resolve_star_select() {
1135        let schema = make_schema();
1136        let stmt = parse_one("SELECT * FROM users");
1137        let mut resolver = Resolver::new(&schema);
1138        let errors = resolver.resolve_statement(&stmt);
1139        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
1140        assert_eq!(resolver.tables_resolved, 1);
1141    }
1142
1143    #[test]
1144    fn test_resolve_insert_checks_table() {
1145        let schema = make_schema();
1146        let stmt = parse_one("INSERT INTO nonexistent VALUES (1)");
1147        let mut resolver = Resolver::new(&schema);
1148        let errors = resolver.resolve_statement(&stmt);
1149        assert_eq!(errors.len(), 1);
1150        assert!(matches!(
1151            errors[0].kind,
1152            SemanticErrorKind::UnresolvedTable { .. }
1153        ));
1154    }
1155
1156    // ── Metrics tests ──
1157
1158    #[test]
1159    fn test_semantic_metrics() {
1160        reset_semantic_metrics();
1161        let schema = make_schema();
1162
1163        // Trigger an error.
1164        let stmt = parse_one("SELECT nonexistent FROM users");
1165        let mut resolver = Resolver::new(&schema);
1166        let _ = resolver.resolve_statement(&stmt);
1167
1168        let snap = semantic_metrics_snapshot();
1169        assert!(snap.fsqlite_semantic_errors_total >= 1);
1170    }
1171}