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, Literal,
24    QualifiedName, ResultColumn, SelectCore, SelectStatement, Statement, TableOrSubquery,
25    WithClause,
26};
27use fsqlite_types::TypeAffinity;
28
29// ---------------------------------------------------------------------------
30// Metrics
31// ---------------------------------------------------------------------------
32
33/// Monotonic counter of semantic errors encountered.
34static FSQLITE_SEMANTIC_ERRORS_TOTAL: AtomicU64 = AtomicU64::new(0);
35
36/// Point-in-time snapshot of semantic analysis metrics.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub struct SemanticMetricsSnapshot {
39    pub fsqlite_semantic_errors_total: u64,
40}
41
42/// Take a point-in-time snapshot of semantic metrics.
43#[must_use]
44pub fn semantic_metrics_snapshot() -> SemanticMetricsSnapshot {
45    SemanticMetricsSnapshot {
46        fsqlite_semantic_errors_total: FSQLITE_SEMANTIC_ERRORS_TOTAL.load(Ordering::Relaxed),
47    }
48}
49
50/// Reset semantic metrics.
51pub fn reset_semantic_metrics() {
52    FSQLITE_SEMANTIC_ERRORS_TOTAL.store(0, Ordering::Relaxed);
53}
54
55// ---------------------------------------------------------------------------
56// Schema types
57// ---------------------------------------------------------------------------
58
59/// A column definition in the schema.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ColumnDef {
62    /// Column name (stored in original case).
63    pub name: String,
64    /// Type affinity determined from the DDL type name.
65    pub affinity: TypeAffinity,
66    /// Whether this column is an INTEGER PRIMARY KEY (rowid alias).
67    pub is_ipk: bool,
68    /// Whether this column has a NOT NULL constraint.
69    pub not_null: bool,
70}
71
72/// A table definition in the schema.
73#[derive(Debug, Clone)]
74pub struct TableDef {
75    /// Table name.
76    pub name: String,
77    /// Column definitions in declaration order.
78    pub columns: Vec<ColumnDef>,
79    /// Whether this is a WITHOUT ROWID table.
80    pub without_rowid: bool,
81    /// Whether this is a STRICT table.
82    pub strict: bool,
83}
84
85impl TableDef {
86    /// Find a column by name (case-insensitive).
87    #[must_use]
88    pub fn find_column(&self, name: &str) -> Option<&ColumnDef> {
89        self.columns
90            .iter()
91            .find(|c| c.name.eq_ignore_ascii_case(name))
92    }
93
94    /// Check if this table has a column with the given name (case-insensitive).
95    #[must_use]
96    pub fn has_column(&self, name: &str) -> bool {
97        self.find_column(name).is_some()
98    }
99
100    /// Check if a name is a rowid alias for this table.
101    #[must_use]
102    pub fn is_rowid_alias(&self, name: &str) -> bool {
103        if self.without_rowid {
104            return false;
105        }
106        if let Some(column) = self.find_column(name) {
107            return column.is_ipk;
108        }
109        is_hidden_rowid_alias_name(name)
110    }
111}
112
113fn is_hidden_rowid_alias_name(name: &str) -> bool {
114    matches!(
115        name.to_ascii_lowercase().as_str(),
116        "rowid" | "_rowid_" | "oid"
117    )
118}
119
120/// The database schema: a collection of table definitions.
121#[derive(Debug, Clone, Default)]
122pub struct Schema {
123    /// Tables by lowercase name.
124    tables: HashMap<String, TableDef>,
125    /// Non-main schema tables by lowercase schema name then lowercase table name.
126    namespaced_tables: HashMap<String, HashMap<String, TableDef>>,
127}
128
129impl Schema {
130    /// Create an empty schema.
131    #[must_use]
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// Add a table definition.
137    pub fn add_table(&mut self, table: TableDef) {
138        self.tables.insert(table.name.to_ascii_lowercase(), table);
139    }
140
141    /// Add a table definition to a specific schema namespace.
142    pub fn add_table_in_schema(&mut self, schema_name: &str, table: TableDef) {
143        if schema_name.eq_ignore_ascii_case("main") {
144            self.add_table(table);
145            return;
146        }
147
148        self.namespaced_tables
149            .entry(schema_name.to_ascii_lowercase())
150            .or_default()
151            .insert(table.name.to_ascii_lowercase(), table);
152    }
153
154    /// Look up a table by name (case-insensitive).
155    #[must_use]
156    pub fn find_table(&self, name: &str) -> Option<&TableDef> {
157        self.tables.get(&name.to_ascii_lowercase())
158    }
159
160    /// Look up a table by optional schema-qualified name.
161    #[must_use]
162    pub fn find_table_in_schema(&self, schema: Option<&str>, name: &str) -> Option<&TableDef> {
163        match schema {
164            None => self.find_table(name),
165            Some(schema_name) if schema_name.eq_ignore_ascii_case("main") => self.find_table(name),
166            Some(schema_name) => self
167                .namespaced_tables
168                .get(&schema_name.to_ascii_lowercase())
169                .and_then(|tables| tables.get(&name.to_ascii_lowercase())),
170        }
171    }
172
173    /// Look up a table by a scope lookup key produced by `table_lookup_key`.
174    #[must_use]
175    pub fn find_table_by_lookup_key(&self, lookup_key: &str) -> Option<&TableDef> {
176        if let Some((schema_name, table_name)) = lookup_key.split_once('\0') {
177            self.find_table_in_schema(Some(schema_name), table_name)
178        } else {
179            self.find_table(lookup_key)
180        }
181    }
182
183    /// Number of tables in the schema.
184    #[must_use]
185    pub fn table_count(&self) -> usize {
186        self.tables.len()
187            + self
188                .namespaced_tables
189                .values()
190                .map(std::collections::HashMap::len)
191                .sum::<usize>()
192    }
193}
194
195fn table_lookup_key(name: &QualifiedName) -> String {
196    match name.schema.as_deref() {
197        None => name.name.to_ascii_lowercase(),
198        Some(schema_name) if schema_name.eq_ignore_ascii_case("main") => {
199            name.name.to_ascii_lowercase()
200        }
201        Some(schema_name) => format!(
202            "{}\0{}",
203            schema_name.to_ascii_lowercase(),
204            name.name.to_ascii_lowercase()
205        ),
206    }
207}
208
209fn lookup_key_table_name(lookup_key: &str) -> &str {
210    lookup_key
211        .split_once('\0')
212        .map_or(lookup_key, |(_, table_name)| table_name)
213}
214
215// ---------------------------------------------------------------------------
216// Scope tracking
217// ---------------------------------------------------------------------------
218
219/// A name scope for query resolution. Scopes nest for subqueries and CTEs.
220#[derive(Debug, Clone)]
221pub struct Scope {
222    /// Table aliases visible in this scope: alias → table name.
223    aliases: HashMap<String, String>,
224    /// Columns visible from each alias: alias → set of column names.
225    /// None means the columns are unknown (CTE or subquery), so any column reference is optimistically accepted.
226    columns: HashMap<String, Option<HashSet<String>>>,
227    /// Columns that were joined via `USING` and are therefore unambiguous.
228    pub using_columns: HashSet<String>,
229    /// CTE names visible in this scope.
230    ctes: HashSet<String>,
231    /// Aliases that can only be referenced by qualified names (e.g. UPSERT's "excluded").
232    qualified_only: HashSet<String>,
233    /// Parent scope (for subquery nesting).
234    parent: Option<Box<Self>>,
235}
236
237impl Scope {
238    /// Create a root scope.
239    #[must_use]
240    pub fn root() -> Self {
241        Self {
242            aliases: HashMap::new(),
243            columns: HashMap::new(),
244            using_columns: HashSet::new(),
245            ctes: HashSet::new(),
246            qualified_only: HashSet::new(),
247            parent: None,
248        }
249    }
250
251    /// Create a child scope (for subqueries).
252    #[must_use]
253    pub fn child(parent: Self) -> Self {
254        Self {
255            aliases: HashMap::new(),
256            columns: HashMap::new(),
257            using_columns: HashSet::new(),
258            ctes: HashSet::new(),
259            qualified_only: HashSet::new(),
260            parent: Some(Box::new(parent)),
261        }
262    }
263
264    /// Register a table alias with its columns.
265    pub fn add_alias(&mut self, alias: &str, table_name: &str, columns: Option<HashSet<String>>) {
266        let key = alias.to_ascii_lowercase();
267        if self.aliases.contains_key(&key) {
268            self.aliases.insert(key.clone(), "<AMBIGUOUS>".to_owned());
269            self.columns.insert(key, None);
270        } else {
271            self.aliases.insert(key.clone(), table_name.to_owned());
272            self.columns.insert(key, columns);
273        }
274    }
275
276    /// Register an alias that does not participate in unqualified column resolution.
277    pub fn add_qualified_only_alias(
278        &mut self,
279        alias: &str,
280        table_name: &str,
281        columns: Option<HashSet<String>>,
282    ) {
283        self.add_alias(alias, table_name, columns);
284        self.qualified_only.insert(alias.to_ascii_lowercase());
285    }
286
287    /// Register a CTE name.
288    pub fn add_cte(&mut self, name: &str) {
289        self.ctes.insert(name.to_ascii_lowercase());
290    }
291
292    /// Check if a CTE is visible in this scope (or parent scopes).
293    #[must_use]
294    pub fn has_cte(&self, name: &str) -> bool {
295        let key = name.to_ascii_lowercase();
296        if self.ctes.contains(&key) {
297            return true;
298        }
299        self.parent.as_ref().is_some_and(|p| p.has_cte(name))
300    }
301
302    /// Check if an alias is visible in this scope (or parent scopes).
303    #[must_use]
304    pub fn has_alias(&self, alias: &str) -> bool {
305        let key = alias.to_ascii_lowercase();
306        if self.aliases.contains_key(&key) {
307            return true;
308        }
309        self.parent.as_ref().is_some_and(|p| p.has_alias(alias))
310    }
311
312    /// Check if a table reference is visible in this scope (or parent scopes).
313    ///
314    /// Bare `table.*` can match either a visible alias or the underlying table
315    /// name. Schema-qualified references must match the bound table identity
316    /// exactly, with `main.table` normalized to bare `table`.
317    #[must_use]
318    pub fn has_table_reference(&self, name: &QualifiedName) -> bool {
319        let target_lookup_key = table_lookup_key(name);
320        let target_name = name.name.to_ascii_lowercase();
321
322        if self.aliases.iter().any(|(alias, bound_name)| {
323            if name.schema.is_none() {
324                alias.eq_ignore_ascii_case(&target_name)
325                    || lookup_key_table_name(bound_name).eq_ignore_ascii_case(&target_name)
326            } else {
327                bound_name.eq_ignore_ascii_case(&target_lookup_key)
328            }
329        }) {
330            return true;
331        }
332
333        self.parent
334            .as_ref()
335            .is_some_and(|parent| parent.has_table_reference(name))
336    }
337
338    /// Check if an alias is defined locally in this scope.
339    #[must_use]
340    pub fn has_alias_local(&self, alias: &str) -> bool {
341        let key = alias.to_ascii_lowercase();
342        self.aliases.contains_key(&key)
343    }
344
345    /// Resolve a column reference: find which alias provides it.
346    ///
347    /// If `table_qualifier` is Some, checks only that alias.
348    /// If None, searches all visible aliases for the column name.
349    /// Returns the resolved (alias, column_name) or None.
350    #[must_use]
351    pub fn resolve_column(
352        &self,
353        schema: &Schema,
354        table_qualifier: Option<&str>,
355        column_name: &str,
356    ) -> ResolveResult {
357        let col_lower = column_name.to_ascii_lowercase();
358
359        if let Some(qualifier) = table_qualifier {
360            let key = qualifier.to_ascii_lowercase();
361            if self.aliases.get(&key).map(String::as_str) == Some("<AMBIGUOUS>") {
362                return ResolveResult::Ambiguous(vec![key]);
363            }
364            if let Some(cols) = self.columns.get(&key) {
365                if cols.as_ref().is_none_or(|c| c.contains(&col_lower)) {
366                    return ResolveResult::Resolved(key);
367                }
368                if let Some(table_name) = self.aliases.get(&key)
369                    && let Some(table_def) = schema.find_table_by_lookup_key(table_name)
370                    && table_def.is_rowid_alias(&col_lower)
371                {
372                    return ResolveResult::Resolved(key);
373                }
374                return ResolveResult::ColumnNotFound;
375            }
376            // Check parent scope.
377            if let Some(ref parent) = self.parent {
378                return parent.resolve_column(schema, table_qualifier, column_name);
379            }
380            return ResolveResult::TableNotFound;
381        }
382
383        // Unqualified: search all aliases in this scope.
384        let mut known_matches = Vec::new();
385        let mut unknown_matches = Vec::new();
386
387        for (alias, cols) in &self.columns {
388            if self.qualified_only.contains(alias) {
389                continue;
390            }
391            if self.aliases.get(alias).map(String::as_str) == Some("<AMBIGUOUS>") {
392                continue; // Do not resolve unqualified columns from ambiguous aliases
393            }
394            let is_match = match cols {
395                Some(c) => {
396                    c.contains(&col_lower) || {
397                        self.aliases
398                            .get(alias)
399                            .and_then(|t| schema.find_table_by_lookup_key(t))
400                            .is_some_and(|td| td.is_rowid_alias(&col_lower))
401                    }
402                }
403                None => true,
404            };
405            if is_match {
406                if cols.is_some() {
407                    known_matches.push(alias.clone());
408                } else {
409                    unknown_matches.push(alias.clone());
410                }
411            }
412        }
413
414        match (known_matches.len(), unknown_matches.len()) {
415            (0, 0) => {
416                // Check parent scope.
417                if let Some(ref parent) = self.parent {
418                    return parent.resolve_column(schema, None, column_name);
419                }
420                ResolveResult::ColumnNotFound
421            }
422            (1, 0) => ResolveResult::Resolved(known_matches.into_iter().next().unwrap_or_default()),
423            (0, 1) => {
424                ResolveResult::Resolved(unknown_matches.into_iter().next().unwrap_or_default())
425            }
426            _ => {
427                let mut all_matches = known_matches;
428                all_matches.extend(unknown_matches);
429                all_matches.sort();
430                if self.using_columns.contains(&col_lower) {
431                    // For USING columns, just pick the first one (they are equivalent).
432                    ResolveResult::Resolved(all_matches.into_iter().next().unwrap_or_default())
433                } else if all_matches.contains(&"<output>".to_owned()) {
434                    ResolveResult::Resolved("<output>".to_owned())
435                } else {
436                    ResolveResult::Ambiguous(all_matches)
437                }
438            }
439        }
440    }
441
442    /// Number of aliases registered in this scope (not counting parents).
443    #[must_use]
444    pub fn alias_count(&self) -> usize {
445        self.aliases.len()
446    }
447
448    /// Return known column sets from all local aliases (for NATURAL JOIN).
449    /// Aliases with unknown columns (`None`) are omitted.
450    #[must_use]
451    pub fn known_local_column_sets(&self) -> Vec<&HashSet<String>> {
452        self.columns
453            .values()
454            .filter_map(|opt| opt.as_ref())
455            .collect()
456    }
457
458    /// Return the column set for a specific alias (lowercased lookup).
459    #[must_use]
460    pub fn columns_for_alias(&self, alias: &str) -> Option<&HashSet<String>> {
461        self.columns
462            .get(&alias.to_ascii_lowercase())
463            .and_then(|opt| opt.as_ref())
464    }
465}
466
467/// Result of resolving a column reference.
468#[derive(Debug, Clone, PartialEq, Eq)]
469pub enum ResolveResult {
470    /// Column resolved to the given alias.
471    Resolved(String),
472    /// The table qualifier was not found.
473    TableNotFound,
474    /// The column was not found in the specified table.
475    ColumnNotFound,
476    /// The column was found in multiple tables (ambiguous).
477    Ambiguous(Vec<String>),
478}
479
480// ---------------------------------------------------------------------------
481// Semantic errors
482// ---------------------------------------------------------------------------
483
484/// A semantic analysis error.
485#[derive(Debug, Clone, PartialEq, Eq)]
486pub struct SemanticError {
487    /// Error kind.
488    pub kind: SemanticErrorKind,
489    /// Human-readable message.
490    pub message: String,
491}
492
493/// Kinds of semantic errors.
494#[derive(Debug, Clone, PartialEq, Eq)]
495pub enum SemanticErrorKind {
496    /// Column reference could not be resolved.
497    UnresolvedColumn {
498        table: Option<String>,
499        column: String,
500    },
501    /// Column reference is ambiguous (exists in multiple tables).
502    AmbiguousColumn {
503        column: String,
504        candidates: Vec<String>,
505    },
506    /// Table or alias not found.
507    UnresolvedTable { name: String },
508    /// Duplicate alias in the same scope.
509    DuplicateAlias { alias: String },
510    /// Function called with wrong number of arguments.
511    FunctionArityMismatch {
512        function: String,
513        expected: FunctionArity,
514        actual: usize,
515    },
516    /// SELECT * used without any tables in scope.
517    NoTablesSpecifiedForStar,
518    /// Type coercion warning (not fatal).
519    ImplicitTypeCoercion {
520        from: TypeAffinity,
521        to: TypeAffinity,
522        context: String,
523    },
524    /// A function argument fails a compile-time constraint (e.g. the
525    /// probability argument to `likelihood()` must be a constant float literal
526    /// in `[0.0, 1.0]`). Carries the fully-formed diagnostic message.
527    InvalidFunctionArgument { message: String },
528}
529
530/// Expected function arity.
531#[derive(Debug, Clone, PartialEq, Eq)]
532pub enum FunctionArity {
533    /// Exact number of arguments.
534    Exact(usize),
535    /// Range of acceptable argument counts.
536    Range(usize, usize),
537    /// Any number of arguments.
538    Variadic,
539    /// Minimum number of arguments.
540    VariadicMin(usize),
541}
542
543impl std::fmt::Display for SemanticError {
544    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545        write!(f, "{}", self.message)
546    }
547}
548
549// ---------------------------------------------------------------------------
550// Resolver
551// ---------------------------------------------------------------------------
552
553/// The semantic analyzer / name resolver.
554///
555/// Given a `Schema` and an AST, validates all name references and collects
556/// errors. Uses scope tracking for nested queries and CTEs.
557pub struct Resolver<'a> {
558    schema: &'a Schema,
559    errors: Vec<SemanticError>,
560    tables_resolved: u64,
561    columns_bound: u64,
562}
563
564impl<'a> Resolver<'a> {
565    /// Create a new resolver for the given schema.
566    #[must_use]
567    pub fn new(schema: &'a Schema) -> Self {
568        Self {
569            schema,
570            errors: Vec::new(),
571            tables_resolved: 0,
572            columns_bound: 0,
573        }
574    }
575
576    /// Resolve all name references in a statement.
577    ///
578    /// Returns the list of semantic errors found.
579    pub fn resolve_statement(&mut self, stmt: &Statement) -> Vec<SemanticError> {
580        let span = tracing::debug_span!(
581            target: "fsqlite.parse",
582            "semantic_analysis",
583            tables_resolved = tracing::field::Empty,
584            columns_bound = tracing::field::Empty,
585            errors = tracing::field::Empty,
586        );
587        let _guard = span.enter();
588
589        self.errors.clear();
590        self.tables_resolved = 0;
591        self.columns_bound = 0;
592
593        let mut scope = Scope::root();
594        self.resolve_stmt_inner(stmt, &mut scope);
595
596        span.record("tables_resolved", self.tables_resolved);
597        span.record("columns_bound", self.columns_bound);
598        span.record("errors", self.errors.len() as u64);
599
600        // Record error metrics.
601        if !self.errors.is_empty() {
602            FSQLITE_SEMANTIC_ERRORS_TOTAL.fetch_add(self.errors.len() as u64, Ordering::Relaxed);
603        }
604
605        self.errors.clone()
606    }
607
608    fn resolve_stmt_inner(&mut self, stmt: &Statement, scope: &mut Scope) {
609        match stmt {
610            Statement::Select(select) => self.resolve_select(select, scope),
611            Statement::Insert(insert) => {
612                // Process WITH clause CTEs if present.
613                if let Some(ref with) = insert.with {
614                    self.resolve_with_clause(with, scope);
615                }
616
617                // Resolve the data source (VALUES or SELECT).
618                // The target table is NOT visible to the body.
619                match &insert.source {
620                    fsqlite_ast::InsertSource::Values(rows) => {
621                        for row in rows {
622                            for expr in row {
623                                self.resolve_expr(expr, scope);
624                            }
625                        }
626                    }
627                    fsqlite_ast::InsertSource::Select(select) => {
628                        let mut source_scope = scope.clone();
629                        self.resolve_select(select, &mut source_scope);
630                    }
631                    fsqlite_ast::InsertSource::DefaultValues => {}
632                }
633
634                // Bind the target table so RETURNING or UPSERT can reference it.
635                self.bind_table_to_scope(&insert.table, None, scope);
636
637                // Scope strictly for target column checks
638                let mut target_scope = Scope::root();
639                if insert.table.schema.is_none() && scope.has_cte(&insert.table.name) {
640                    target_scope.add_alias(&insert.table.name, &insert.table.name, None);
641                } else if let Some(table_def) = self
642                    .schema
643                    .find_table_in_schema(insert.table.schema.as_deref(), &insert.table.name)
644                {
645                    let col_set: HashSet<String> = table_def
646                        .columns
647                        .iter()
648                        .map(|c| c.name.to_ascii_lowercase())
649                        .collect();
650                    target_scope.add_alias(
651                        &insert.table.name,
652                        &table_lookup_key(&insert.table),
653                        Some(col_set),
654                    );
655                }
656
657                for col in &insert.columns {
658                    self.resolve_unqualified_column(col, &target_scope, false);
659                }
660
661                // Resolve UPSERT.
662                for upsert in &insert.upsert {
663                    if let Some(target) = &upsert.target {
664                        for col in &target.columns {
665                            self.resolve_expr(&col.expr, scope);
666                        }
667                        if let Some(where_clause) = &target.where_clause {
668                            self.resolve_expr(where_clause, scope);
669                        }
670                    }
671                    match &upsert.action {
672                        fsqlite_ast::UpsertAction::Update {
673                            assignments,
674                            where_clause,
675                        } => {
676                            let mut upsert_scope = Scope::child(scope.clone());
677                            let alias_name = insert.alias.as_deref().unwrap_or(&insert.table.name);
678                            let target_lookup_key = table_lookup_key(&insert.table);
679                            if let Some(table_def) = self.schema.find_table_in_schema(
680                                insert.table.schema.as_deref(),
681                                &insert.table.name,
682                            ) {
683                                let col_set: HashSet<String> = table_def
684                                    .columns
685                                    .iter()
686                                    .map(|c| c.name.to_ascii_lowercase())
687                                    .collect();
688                                upsert_scope.add_qualified_only_alias(
689                                    "excluded",
690                                    &target_lookup_key,
691                                    Some(col_set.clone()),
692                                );
693                                upsert_scope.add_alias(
694                                    alias_name,
695                                    &target_lookup_key,
696                                    Some(col_set),
697                                );
698                            } else {
699                                upsert_scope.add_qualified_only_alias("excluded", "<pseudo>", None);
700                                upsert_scope.add_alias(alias_name, "<pseudo>", None);
701                            }
702
703                            for assignment in assignments {
704                                match &assignment.target {
705                                    fsqlite_ast::AssignmentTarget::Column(col) => {
706                                        self.resolve_unqualified_column(col, &target_scope, false);
707                                    }
708                                    fsqlite_ast::AssignmentTarget::ColumnList(cols) => {
709                                        for col in cols {
710                                            self.resolve_unqualified_column(
711                                                col,
712                                                &target_scope,
713                                                false,
714                                            );
715                                        }
716                                    }
717                                }
718                                self.resolve_expr(&assignment.value, &upsert_scope);
719                            }
720                            if let Some(w) = where_clause {
721                                self.resolve_expr(w, &upsert_scope);
722                            }
723                        }
724                        fsqlite_ast::UpsertAction::Nothing => {}
725                    }
726                }
727                for ret in &insert.returning {
728                    self.resolve_result_column(ret, scope);
729                }
730            }
731            Statement::Update(update) => {
732                // Process WITH clause CTEs if present.
733                if let Some(ref with) = update.with {
734                    self.resolve_with_clause(with, scope);
735                }
736
737                // LIMIT and OFFSET cannot reference target or FROM tables.
738                let limit_scope = scope.clone();
739
740                self.bind_table_to_scope(&update.table.name, update.table.alias.as_deref(), scope);
741
742                // Scope strictly for target column checks
743                let mut target_scope = Scope::root();
744                self.bind_table_to_scope(
745                    &update.table.name,
746                    update.table.alias.as_deref(),
747                    &mut target_scope,
748                );
749
750                // The RETURNING clause can ONLY see the target table (and outer scopes/CTEs).
751                // It CANNOT see tables from the FROM clause.
752                let returning_scope = scope.clone();
753
754                for assignment in &update.assignments {
755                    match &assignment.target {
756                        fsqlite_ast::AssignmentTarget::Column(col) => {
757                            self.resolve_unqualified_column(col, &target_scope, false);
758                        }
759                        fsqlite_ast::AssignmentTarget::ColumnList(cols) => {
760                            for col in cols {
761                                self.resolve_unqualified_column(col, &target_scope, false);
762                            }
763                        }
764                    }
765                }
766                if let Some(from) = &update.from {
767                    self.resolve_from(from, scope);
768                }
769                for assignment in &update.assignments {
770                    self.resolve_expr(&assignment.value, scope);
771                }
772                if let Some(where_clause) = &update.where_clause {
773                    self.resolve_expr(where_clause, scope);
774                }
775                for ret in &update.returning {
776                    self.resolve_result_column(ret, &returning_scope);
777                }
778                for term in &update.order_by {
779                    self.resolve_expr(&term.expr, scope);
780                }
781                if let Some(limit) = &update.limit {
782                    self.resolve_expr(&limit.limit, &limit_scope);
783                    if let Some(offset) = &limit.offset {
784                        self.resolve_expr(offset, &limit_scope);
785                    }
786                }
787            }
788            Statement::Delete(delete) => {
789                // Process WITH clause CTEs if present.
790                if let Some(ref with) = delete.with {
791                    self.resolve_with_clause(with, scope);
792                }
793
794                // LIMIT and OFFSET cannot reference the target table.
795                let limit_scope = scope.clone();
796
797                self.bind_table_to_scope(&delete.table.name, delete.table.alias.as_deref(), scope);
798                if let Some(where_clause) = &delete.where_clause {
799                    self.resolve_expr(where_clause, scope);
800                }
801                for ret in &delete.returning {
802                    self.resolve_result_column(ret, scope);
803                }
804                for term in &delete.order_by {
805                    self.resolve_expr(&term.expr, scope);
806                }
807                if let Some(limit) = &delete.limit {
808                    self.resolve_expr(&limit.limit, &limit_scope);
809                    if let Some(offset) = &limit.offset {
810                        self.resolve_expr(offset, &limit_scope);
811                    }
812                }
813            }
814            // DDL and control statements don't need name resolution.
815            _ => {}
816        }
817    }
818
819    fn resolve_with_clause(&mut self, with: &WithClause, scope: &mut Scope) {
820        if with.recursive {
821            // In WITH RECURSIVE, all CTE names are visible to all CTE bodies.
822            for cte in &with.ctes {
823                scope.add_cte(&cte.name);
824            }
825            for cte in &with.ctes {
826                let mut cte_scope = scope.clone();
827                self.resolve_select(&cte.query, &mut cte_scope);
828            }
829        } else {
830            // In plain WITH, a CTE body can only see previously defined CTEs.
831            for cte in &with.ctes {
832                let mut cte_scope = scope.clone();
833                self.resolve_select(&cte.query, &mut cte_scope);
834                // Add *after* resolving the query so it can't see itself or subsequent CTEs.
835                scope.add_cte(&cte.name);
836            }
837        }
838    }
839
840    // SQLite compound SELECTs allow ORDER BY terms to reuse a projected
841    // expression verbatim, even though underlying table aliases are no longer
842    // in scope at the compound boundary.
843    fn compound_order_by_matches_output_expr(select: &SelectStatement, order_expr: &Expr) -> bool {
844        if select.body.compounds.is_empty() {
845            return false;
846        }
847
848        std::iter::once(&select.body.select)
849            .chain(select.body.compounds.iter().map(|(_, core)| core))
850            .filter_map(|core| match core {
851                SelectCore::Select { columns, .. } => Some(columns.iter()),
852                _ => None,
853            })
854            .flatten()
855            .any(|column| match column {
856                ResultColumn::Expr { expr, .. } => expr == order_expr,
857                _ => false,
858            })
859    }
860
861    fn resolve_select(&mut self, select: &SelectStatement, scope: &mut Scope) {
862        // Register CTEs if present.
863        if let Some(ref with) = select.with {
864            self.resolve_with_clause(with, scope);
865        }
866
867        // Resolve the primary select core in an isolated scope.
868        let mut first_core_scope = scope.clone();
869        self.resolve_select_core(&select.body.select, &mut first_core_scope);
870
871        // Resolve any compound queries (UNION, INTERSECT, EXCEPT) in isolated scopes.
872        for (_op, core) in &select.body.compounds {
873            let mut comp_scope = scope.clone();
874            self.resolve_select_core(core, &mut comp_scope);
875        }
876
877        // Resolve ORDER BY against the appropriate scope.
878        let mut order_by_scope = if select.body.compounds.is_empty() {
879            first_core_scope.clone()
880        } else {
881            scope.clone() // Compounds can only see outer scope + result columns
882        };
883
884        let mut output_cols = HashSet::new();
885        for core in std::iter::once(&select.body.select)
886            .chain(select.body.compounds.iter().map(|(_, core)| core))
887        {
888            if let SelectCore::Select { columns, .. } = core {
889                for col in columns {
890                    match col {
891                        ResultColumn::Expr {
892                            alias: Some(alias_id),
893                            ..
894                        } => {
895                            output_cols.insert(alias_id.to_ascii_lowercase());
896                        }
897                        ResultColumn::Expr {
898                            expr: Expr::Column(col_ref, _),
899                            ..
900                        } => {
901                            output_cols.insert(col_ref.column.to_ascii_lowercase());
902                        }
903                        _ => {}
904                    }
905                }
906            }
907        }
908        if !output_cols.is_empty() {
909            // Add the output columns as a pseudo-table so ORDER BY can reference them.
910            order_by_scope.add_alias("<output>", "<output>", Some(output_cols));
911        }
912
913        for term in &select.order_by {
914            if Self::compound_order_by_matches_output_expr(select, &term.expr) {
915                continue;
916            }
917            self.resolve_expr(&term.expr, &order_by_scope);
918        }
919
920        // Resolve LIMIT against the base scope (no FROM aliases).
921        if let Some(limit) = &select.limit {
922            self.resolve_expr(&limit.limit, scope);
923            if let Some(offset) = &limit.offset {
924                self.resolve_expr(offset, scope);
925            }
926        }
927    }
928
929    fn resolve_select_core(&mut self, core: &SelectCore, scope: &mut Scope) {
930        match core {
931            SelectCore::Select {
932                columns,
933                from,
934                where_clause,
935                group_by,
936                having,
937                windows,
938                ..
939            } => {
940                // Resolve FROM clause first (registers table aliases).
941                if let Some(from) = from {
942                    self.resolve_from(from, scope);
943                }
944
945                // Resolve column references in SELECT list.
946                for col in columns {
947                    self.resolve_result_column(col, scope);
948                }
949
950                // Resolve WHERE clause.
951                if let Some(where_expr) = where_clause {
952                    self.resolve_expr(where_expr, scope);
953                }
954
955                // Create a scope for GROUP BY, HAVING, and WINDOW that includes output columns.
956                let mut post_select_scope = scope.clone();
957                let mut output_cols = HashSet::new();
958                for col in columns {
959                    if let ResultColumn::Expr {
960                        alias: Some(alias_id),
961                        ..
962                    } = col
963                    {
964                        output_cols.insert(alias_id.to_ascii_lowercase());
965                    } else if let ResultColumn::Expr {
966                        expr: Expr::Column(col_ref, _),
967                        ..
968                    } = col
969                    {
970                        output_cols.insert(col_ref.column.to_ascii_lowercase());
971                    }
972                }
973                if !output_cols.is_empty() {
974                    post_select_scope.add_alias("<output>", "<output>", Some(output_cols));
975                } else {
976                    post_select_scope.add_alias("<output>", "<output>", None);
977                }
978
979                for expr in group_by {
980                    self.resolve_expr(expr, &post_select_scope);
981                }
982                if let Some(having) = having {
983                    self.resolve_expr(having, &post_select_scope);
984                }
985                for window in windows {
986                    for part in &window.spec.partition_by {
987                        self.resolve_expr(part, &post_select_scope);
988                    }
989                    for order in &window.spec.order_by {
990                        self.resolve_expr(&order.expr, &post_select_scope);
991                    }
992                }
993            }
994            SelectCore::Values(rows) => {
995                for row in rows {
996                    for expr in row {
997                        self.resolve_expr(expr, scope);
998                    }
999                }
1000            }
1001        }
1002    }
1003
1004    fn resolve_from(&mut self, from: &FromClause, scope: &mut Scope) {
1005        self.resolve_table_or_subquery(&from.source, scope);
1006
1007        for join in &from.joins {
1008            self.resolve_join(join, scope);
1009        }
1010    }
1011
1012    fn resolve_table_or_subquery(&mut self, tos: &TableOrSubquery, scope: &mut Scope) {
1013        match tos {
1014            TableOrSubquery::Table { name, alias, .. } => {
1015                let table_name = &name.name;
1016                let alias_name = alias.as_deref().unwrap_or(table_name);
1017
1018                // Check for duplicate alias in the CURRENT scope only.
1019                if scope.has_alias_local(alias_name) {
1020                    self.push_error(SemanticErrorKind::DuplicateAlias {
1021                        alias: alias_name.to_owned(),
1022                    });
1023                }
1024
1025                // Resolve table name against schema or CTEs.
1026                if name.schema.is_none() && scope.has_cte(table_name) {
1027                    // CTE reference — columns are unknown at this stage.
1028                    scope.add_alias(alias_name, table_name, None);
1029                    self.tables_resolved += 1;
1030                } else if let Some(table_def) = self
1031                    .schema
1032                    .find_table_in_schema(name.schema.as_deref(), table_name)
1033                {
1034                    let col_set: HashSet<String> = table_def
1035                        .columns
1036                        .iter()
1037                        .map(|c| c.name.to_ascii_lowercase())
1038                        .collect();
1039                    scope.add_alias(alias_name, &table_lookup_key(name), Some(col_set));
1040                    self.tables_resolved += 1;
1041                } else {
1042                    self.push_error(SemanticErrorKind::UnresolvedTable {
1043                        name: name.to_string(),
1044                    });
1045                }
1046            }
1047            TableOrSubquery::Subquery { query, alias, .. } => {
1048                // Resolve subquery in a child scope.
1049                let mut child = Scope::child(scope.clone());
1050                self.resolve_select(query, &mut child);
1051
1052                let alias_name = if let Some(a) = alias {
1053                    a.clone()
1054                } else {
1055                    format!("<subquery_{}>", self.tables_resolved)
1056                };
1057
1058                if !alias_name.starts_with("<subquery_") && scope.has_alias_local(&alias_name) {
1059                    self.push_error(SemanticErrorKind::DuplicateAlias {
1060                        alias: alias_name.clone(),
1061                    });
1062                }
1063
1064                let mut output_cols = HashSet::new();
1065                let mut is_complete = true;
1066                if let SelectCore::Select { columns, .. } = &query.body.select {
1067                    for col in columns {
1068                        match col {
1069                            ResultColumn::Expr {
1070                                alias: Some(alias_id),
1071                                ..
1072                            } => {
1073                                output_cols.insert(alias_id.to_ascii_lowercase());
1074                            }
1075                            ResultColumn::Expr {
1076                                expr: Expr::Column(col_ref, _),
1077                                ..
1078                            } => {
1079                                output_cols.insert(col_ref.column.to_ascii_lowercase());
1080                            }
1081                            ResultColumn::Star | ResultColumn::TableStar(_) => {
1082                                is_complete = false;
1083                            }
1084                            _ => {}
1085                        }
1086                    }
1087                } else {
1088                    is_complete = false;
1089                }
1090
1091                if is_complete {
1092                    scope.add_alias(&alias_name, "<subquery>", Some(output_cols));
1093                } else {
1094                    scope.add_alias(&alias_name, "<subquery>", None);
1095                }
1096
1097                self.tables_resolved += 1;
1098            }
1099            TableOrSubquery::TableFunction {
1100                name, args, alias, ..
1101            } => {
1102                for arg in args {
1103                    self.resolve_expr(arg, scope);
1104                }
1105
1106                let alias_name = alias.as_deref().unwrap_or(name);
1107
1108                if scope.has_alias_local(alias_name) {
1109                    self.push_error(SemanticErrorKind::DuplicateAlias {
1110                        alias: alias_name.to_owned(),
1111                    });
1112                }
1113
1114                scope.add_alias(alias_name, name, None);
1115                self.tables_resolved += 1;
1116            }
1117            TableOrSubquery::ParenJoin(inner_from) => {
1118                self.resolve_from(inner_from, scope);
1119            }
1120        }
1121    }
1122
1123    fn resolve_join(&mut self, join: &JoinClause, scope: &mut Scope) {
1124        // Snapshot column names from existing aliases BEFORE adding the new
1125        // table, so we can compute shared columns for NATURAL JOIN and USING.
1126        let pre_join_columns: Vec<HashSet<String>> = scope
1127            .known_local_column_sets()
1128            .into_iter()
1129            .cloned()
1130            .collect();
1131        let pre_join_aliases: HashSet<String> = scope.aliases.keys().cloned().collect();
1132
1133        self.resolve_table_or_subquery(&join.table, scope);
1134
1135        if join.join_type.natural && join.constraint.is_none() {
1136            // NATURAL JOIN: implicitly equate all columns with matching names
1137            // between the pre-existing tables and the newly joined table(s).
1138            let mut to_insert = Vec::new();
1139            for (alias, cols_opt) in &scope.columns {
1140                if !pre_join_aliases.contains(alias)
1141                    && let Some(new_cols) = cols_opt
1142                {
1143                    for col_name in new_cols {
1144                        if pre_join_columns.iter().any(|cs| cs.contains(col_name)) {
1145                            to_insert.push(col_name.clone());
1146                        }
1147                    }
1148                }
1149            }
1150            for col_name in to_insert {
1151                scope.using_columns.insert(col_name);
1152            }
1153        }
1154
1155        if let Some(ref constraint) = join.constraint {
1156            match constraint {
1157                JoinConstraint::On(expr) => self.resolve_expr(expr, scope),
1158                JoinConstraint::Using(cols) => {
1159                    for col in cols {
1160                        let col_lower = col.to_ascii_lowercase();
1161                        scope.using_columns.insert(col_lower.clone());
1162
1163                        // Validate that column exists on the left side
1164                        let in_left = pre_join_columns.iter().any(|cs| cs.contains(&col_lower));
1165                        // Validate that column exists on the right side
1166                        let mut in_right = false;
1167                        for (alias, cols_opt) in &scope.columns {
1168                            if !pre_join_aliases.contains(alias) {
1169                                if let Some(new_cols) = cols_opt {
1170                                    if new_cols.contains(&col_lower) {
1171                                        in_right = true;
1172                                        break;
1173                                    }
1174                                } else {
1175                                    // If right side columns are unknown (e.g. subquery), assume it exists
1176                                    in_right = true;
1177                                    break;
1178                                }
1179                            }
1180                        }
1181
1182                        // If left side has unknown columns, we might not find it in `pre_join_columns`
1183                        let left_has_unknown = scope.columns.iter().any(|(alias, cols_opt)| {
1184                            pre_join_aliases.contains(alias) && cols_opt.is_none()
1185                        });
1186
1187                        if (!in_left && !left_has_unknown) || !in_right {
1188                            self.push_error(SemanticErrorKind::UnresolvedColumn {
1189                                table: None,
1190                                column: col.clone(),
1191                            });
1192                        }
1193
1194                        self.resolve_unqualified_column(col, scope, true);
1195                    }
1196                }
1197            }
1198        }
1199    }
1200
1201    fn resolve_result_column(&mut self, col: &ResultColumn, scope: &Scope) {
1202        match col {
1203            ResultColumn::Star => {
1204                // SELECT * is valid if there's at least one table in scope.
1205                // Suppress this error if we already reported an UnresolvedTable
1206                // error — the missing star target is a cascading consequence.
1207                if scope.alias_count() == 0
1208                    && !self
1209                        .errors
1210                        .iter()
1211                        .any(|e| matches!(e.kind, SemanticErrorKind::UnresolvedTable { .. }))
1212                {
1213                    self.push_error(SemanticErrorKind::NoTablesSpecifiedForStar);
1214                }
1215            }
1216            ResultColumn::TableStar(table_name) => {
1217                if !scope.has_table_reference(table_name) {
1218                    self.push_error(SemanticErrorKind::UnresolvedTable {
1219                        name: table_name.to_string(),
1220                    });
1221                }
1222            }
1223            ResultColumn::Expr { expr, .. } => {
1224                self.resolve_expr(expr, scope);
1225            }
1226        }
1227    }
1228
1229    #[allow(clippy::too_many_lines)]
1230    fn resolve_expr(&mut self, expr: &Expr, scope: &Scope) {
1231        match expr {
1232            Expr::Column(col_ref, _span) => {
1233                self.resolve_column_ref(col_ref, scope);
1234            }
1235            Expr::BinaryOp { left, right, .. } => {
1236                self.resolve_expr(left, scope);
1237                self.resolve_expr(right, scope);
1238            }
1239            Expr::UnaryOp { expr: inner, .. }
1240            | Expr::Cast { expr: inner, .. }
1241            | Expr::Collate { expr: inner, .. }
1242            | Expr::IsNull { expr: inner, .. } => {
1243                self.resolve_expr(inner, scope);
1244            }
1245            Expr::Between {
1246                expr: inner,
1247                low,
1248                high,
1249                ..
1250            } => {
1251                self.resolve_expr(inner, scope);
1252                self.resolve_expr(low, scope);
1253                self.resolve_expr(high, scope);
1254            }
1255            Expr::In {
1256                expr: inner, set, ..
1257            } => {
1258                self.resolve_expr(inner, scope);
1259                match set {
1260                    InSet::List(items) => {
1261                        for item in items {
1262                            self.resolve_expr(item, scope);
1263                        }
1264                    }
1265                    InSet::Subquery(select) => {
1266                        let mut child = Scope::child(scope.clone());
1267                        self.resolve_select(select, &mut child);
1268                    }
1269                    InSet::Table(name) => self.resolve_table_name(name, scope),
1270                }
1271            }
1272            Expr::Like {
1273                expr: inner,
1274                pattern,
1275                escape,
1276                op,
1277                ..
1278            } => {
1279                self.resolve_expr(inner, scope);
1280                self.resolve_expr(pattern, scope);
1281                if let Some(esc) = escape {
1282                    if *op != fsqlite_ast::LikeOp::Like {
1283                        // SQLite only supports ESCAPE with LIKE. For GLOB, MATCH, REGEXP it throws "wrong number of arguments to function X()"
1284                        self.push_error(SemanticErrorKind::FunctionArityMismatch {
1285                            function: match op {
1286                                fsqlite_ast::LikeOp::Like => "LIKE",
1287                                fsqlite_ast::LikeOp::Glob => "GLOB",
1288                                fsqlite_ast::LikeOp::Match => "MATCH",
1289                                fsqlite_ast::LikeOp::Regexp => "REGEXP",
1290                            }
1291                            .to_owned(),
1292                            expected: FunctionArity::Exact(2),
1293                            actual: 3,
1294                        });
1295                    }
1296                    self.resolve_expr(esc, scope);
1297                }
1298            }
1299            Expr::Subquery(select, _)
1300            | Expr::Exists {
1301                subquery: select, ..
1302            } => {
1303                let mut child = Scope::child(scope.clone());
1304                self.resolve_select(select, &mut child);
1305            }
1306            Expr::FunctionCall {
1307                name,
1308                args,
1309                filter,
1310                over,
1311                ..
1312            } => {
1313                self.resolve_function(name, args, scope);
1314                if let Some(filter) = filter {
1315                    self.resolve_expr(filter, scope);
1316                }
1317                if let Some(window_spec) = over {
1318                    for expr in &window_spec.partition_by {
1319                        self.resolve_expr(expr, scope);
1320                    }
1321                    for term in &window_spec.order_by {
1322                        self.resolve_expr(&term.expr, scope);
1323                    }
1324                    if let Some(frame) = &window_spec.frame {
1325                        match &frame.start {
1326                            fsqlite_ast::FrameBound::Preceding(expr)
1327                            | fsqlite_ast::FrameBound::Following(expr) => {
1328                                self.resolve_expr(expr, scope);
1329                            }
1330                            _ => {}
1331                        }
1332                        if let Some(
1333                            fsqlite_ast::FrameBound::Preceding(expr)
1334                            | fsqlite_ast::FrameBound::Following(expr),
1335                        ) = &frame.end
1336                        {
1337                            self.resolve_expr(expr, scope);
1338                        }
1339                    }
1340                }
1341            }
1342            Expr::Case {
1343                operand,
1344                whens,
1345                else_expr,
1346                ..
1347            } => {
1348                if let Some(op) = operand {
1349                    self.resolve_expr(op, scope);
1350                }
1351                for (when_expr, then_expr) in whens {
1352                    self.resolve_expr(when_expr, scope);
1353                    self.resolve_expr(then_expr, scope);
1354                }
1355                if let Some(else_e) = else_expr {
1356                    self.resolve_expr(else_e, scope);
1357                }
1358            }
1359            Expr::JsonAccess {
1360                expr: inner, path, ..
1361            } => {
1362                self.resolve_expr(inner, scope);
1363                self.resolve_expr(path, scope);
1364            }
1365            Expr::RowValue(exprs, _) => {
1366                for e in exprs {
1367                    self.resolve_expr(e, scope);
1368                }
1369            }
1370            // Constants, placeholders, and RAISE don't need resolution.
1371            Expr::Literal(_, _)
1372            | Expr::BoundOuterValue { .. }
1373            | Expr::Placeholder(_, _)
1374            | Expr::Raise { .. } => {}
1375        }
1376    }
1377
1378    fn resolve_column_ref(&mut self, col_ref: &ColumnRef, scope: &Scope) {
1379        let result = scope.resolve_column(self.schema, col_ref.table.as_deref(), &col_ref.column);
1380        match result {
1381            ResolveResult::Resolved(_) => {
1382                self.columns_bound += 1;
1383            }
1384            ResolveResult::TableNotFound => {
1385                tracing::error!(
1386                    target: "fsqlite.parse",
1387                    table = ?col_ref.table,
1388                    column = %col_ref.column,
1389                    "unresolvable table reference"
1390                );
1391                self.push_error(SemanticErrorKind::UnresolvedColumn {
1392                    table: col_ref.table.as_ref().map(ToString::to_string),
1393                    column: col_ref.column.to_string(),
1394                });
1395            }
1396            ResolveResult::ColumnNotFound => {
1397                tracing::error!(
1398                    target: "fsqlite.parse",
1399                    table = ?col_ref.table,
1400                    column = %col_ref.column,
1401                    "unresolvable column reference"
1402                );
1403                self.push_error(SemanticErrorKind::UnresolvedColumn {
1404                    table: col_ref.table.as_ref().map(ToString::to_string),
1405                    column: col_ref.column.to_string(),
1406                });
1407            }
1408            ResolveResult::Ambiguous(candidates) => {
1409                tracing::error!(
1410                    target: "fsqlite.parse",
1411                    column = %col_ref.column,
1412                    candidates = ?candidates,
1413                    "ambiguous column reference"
1414                );
1415                self.push_error(SemanticErrorKind::AmbiguousColumn {
1416                    column: col_ref.column.to_string(),
1417                    candidates,
1418                });
1419            }
1420        }
1421    }
1422
1423    fn resolve_unqualified_column(&mut self, name: &str, scope: &Scope, is_using_clause: bool) {
1424        let result = scope.resolve_column(self.schema, None, name);
1425        match result {
1426            ResolveResult::Resolved(_) => {
1427                self.columns_bound += 1;
1428            }
1429            ResolveResult::Ambiguous(candidates) => {
1430                if is_using_clause {
1431                    self.columns_bound += 1;
1432                } else {
1433                    self.push_error(SemanticErrorKind::AmbiguousColumn {
1434                        column: name.to_owned(),
1435                        candidates,
1436                    });
1437                }
1438            }
1439            ResolveResult::ColumnNotFound | ResolveResult::TableNotFound => {
1440                self.push_error(SemanticErrorKind::UnresolvedColumn {
1441                    table: None,
1442                    column: name.to_owned(),
1443                });
1444            }
1445        }
1446    }
1447
1448    fn bind_table_to_scope(
1449        &mut self,
1450        name: &QualifiedName,
1451        alias: Option<&str>,
1452        scope: &mut Scope,
1453    ) {
1454        let alias_name = alias.unwrap_or(&name.name);
1455        if name.schema.is_none() && scope.has_cte(&name.name) {
1456            scope.add_alias(alias_name, &name.name, None);
1457            self.tables_resolved += 1;
1458        } else if let Some(table_def) = self
1459            .schema
1460            .find_table_in_schema(name.schema.as_deref(), &name.name)
1461        {
1462            let col_set: HashSet<String> = table_def
1463                .columns
1464                .iter()
1465                .map(|c| c.name.to_ascii_lowercase())
1466                .collect();
1467            scope.add_alias(alias_name, &table_lookup_key(name), Some(col_set));
1468            self.tables_resolved += 1;
1469        } else {
1470            self.push_error(SemanticErrorKind::UnresolvedTable {
1471                name: name.to_string(),
1472            });
1473        }
1474    }
1475
1476    fn resolve_table_name(&mut self, name: &QualifiedName, _scope: &Scope) {
1477        if self
1478            .schema
1479            .find_table_in_schema(name.schema.as_deref(), &name.name)
1480            .is_some()
1481        {
1482            self.tables_resolved += 1;
1483        } else {
1484            self.push_error(SemanticErrorKind::UnresolvedTable {
1485                name: name.to_string(),
1486            });
1487        }
1488    }
1489
1490    fn resolve_function(&mut self, name: &str, args: &FunctionArgs, scope: &Scope) {
1491        // Resolve argument expressions.
1492        let actual = match args {
1493            FunctionArgs::Star => {
1494                if !name.eq_ignore_ascii_case("count") {
1495                    let expected = known_function_arity(name).unwrap_or(FunctionArity::Range(0, 1));
1496                    self.push_error(SemanticErrorKind::FunctionArityMismatch {
1497                        function: name.to_owned(),
1498                        expected,
1499                        actual: 1,
1500                    });
1501                }
1502                1 // `*` counts as 1 argument for arity purposes (e.g. count(*))
1503            }
1504            FunctionArgs::List(list) => {
1505                for arg in list {
1506                    self.resolve_expr(arg, scope);
1507                }
1508                list.len()
1509            }
1510        };
1511
1512        // Validate known function arity.
1513        if let Some(expected) = known_function_arity(name) {
1514            let valid = match &expected {
1515                FunctionArity::Exact(n) => actual == *n,
1516                FunctionArity::Range(lo, hi) => actual >= *lo && actual <= *hi,
1517                FunctionArity::Variadic => true,
1518                FunctionArity::VariadicMin(min) => actual >= *min,
1519            };
1520            if !valid {
1521                self.push_error(SemanticErrorKind::FunctionArityMismatch {
1522                    function: name.to_owned(),
1523                    expected,
1524                    actual,
1525                });
1526            }
1527        }
1528
1529        // likelihood(X, prob): the probability must be a constant floating-point
1530        // literal in [0.0, 1.0], matching C SQLite's exprProbability() contract.
1531        // Integer literals, out-of-range values, and non-literal expressions are
1532        // all rejected at prepare time.
1533        if name.eq_ignore_ascii_case("likelihood")
1534            && let FunctionArgs::List(list) = args
1535            && list.len() == 2
1536        {
1537            let is_valid_probability = matches!(
1538                &list[1],
1539                Expr::Literal(Literal::Float(p), _) if (0.0..=1.0).contains(p)
1540            );
1541            if !is_valid_probability {
1542                self.push_error(SemanticErrorKind::InvalidFunctionArgument {
1543                    message:
1544                        "second argument to likelihood() must be a constant between 0.0 and 1.0"
1545                            .to_owned(),
1546                });
1547            }
1548        }
1549    }
1550
1551    fn push_error(&mut self, kind: SemanticErrorKind) {
1552        let message = match &kind {
1553            SemanticErrorKind::UnresolvedColumn { table, column } => {
1554                if let Some(t) = table {
1555                    format!("no such column: {t}.{column}")
1556                } else {
1557                    format!("no such column: {column}")
1558                }
1559            }
1560            SemanticErrorKind::AmbiguousColumn {
1561                column, candidates, ..
1562            } => {
1563                format!(
1564                    "ambiguous column name: {column} (candidates: {})",
1565                    candidates.join(", ")
1566                )
1567            }
1568            SemanticErrorKind::UnresolvedTable { name } => {
1569                format!("no such table: {name}")
1570            }
1571            SemanticErrorKind::DuplicateAlias { alias } => {
1572                format!("duplicate alias: {alias}")
1573            }
1574            SemanticErrorKind::FunctionArityMismatch {
1575                function,
1576                expected,
1577                actual,
1578            } => {
1579                format!(
1580                    "wrong number of arguments to function {function}: expected {expected:?}, got {actual}"
1581                )
1582            }
1583            SemanticErrorKind::NoTablesSpecifiedForStar => "no tables specified".to_string(),
1584            SemanticErrorKind::ImplicitTypeCoercion {
1585                from, to, context, ..
1586            } => {
1587                format!("implicit type coercion from {from:?} to {to:?} in {context}")
1588            }
1589            SemanticErrorKind::InvalidFunctionArgument { message } => message.clone(),
1590        };
1591
1592        self.errors.push(SemanticError { kind, message });
1593    }
1594}
1595
1596// ---------------------------------------------------------------------------
1597// Known function arity table
1598// ---------------------------------------------------------------------------
1599
1600/// Returns the expected arity for a known SQLite function, if recognized.
1601#[must_use]
1602fn known_function_arity(name: &str) -> Option<FunctionArity> {
1603    match name.to_ascii_lowercase().as_str() {
1604        "random" | "changes" | "last_insert_rowid" | "total_changes" => {
1605            Some(FunctionArity::Exact(0))
1606        }
1607        // Aggregate (1-arg) and scalar (1-arg) functions
1608        "sum" | "total" | "avg" | "abs" | "hex" | "length" | "lower" | "upper" | "typeof"
1609        | "unicode" | "quote" | "zeroblob" | "soundex" | "likely" | "unlikely" | "randomblob" => {
1610            Some(FunctionArity::Exact(1))
1611        }
1612        "ifnull" | "nullif" | "instr" | "glob" | "likelihood" => Some(FunctionArity::Exact(2)),
1613        "replace" => Some(FunctionArity::Exact(3)),
1614        "count" => Some(FunctionArity::Range(0, 1)),
1615        "group_concat" | "trim" | "ltrim" | "rtrim" | "round" => Some(FunctionArity::Range(1, 2)),
1616        // iif/if accept the 2-argument shorthand iif(X,Y) as of SQLite 3.48;
1617        // `if` is iif's registered alias.
1618        "substr" | "substring" | "like" | "iif" | "if" => Some(FunctionArity::Range(2, 3)),
1619        "coalesce" | "json_extract" => Some(FunctionArity::VariadicMin(2)),
1620        "json_remove" => Some(FunctionArity::VariadicMin(1)),
1621        "json_insert" | "json_replace" | "json_set" => Some(FunctionArity::VariadicMin(3)),
1622        // Variadic: aggregates, scalars, date/time, and JSON functions
1623        "min" | "max" | "printf" | "format" | "strftime" | "json" | "json_type" | "json_valid" => {
1624            Some(FunctionArity::VariadicMin(1))
1625        }
1626        "date" | "time" | "datetime" | "julianday" | "unixepoch" => {
1627            Some(FunctionArity::VariadicMin(0))
1628        }
1629        "char" | "json_array" | "json_object" => Some(FunctionArity::Variadic),
1630
1631        _ => None, // Unknown function — skip arity check.
1632    }
1633}
1634
1635// ---------------------------------------------------------------------------
1636// Tests
1637// ---------------------------------------------------------------------------
1638
1639#[cfg(test)]
1640#[path = "semantic_test.rs"]
1641mod semantic_test;
1642
1643#[cfg(test)]
1644mod tests {
1645    use super::*;
1646    use crate::parser::Parser;
1647
1648    fn make_schema() -> Schema {
1649        let mut schema = Schema::new();
1650        schema.add_table(TableDef {
1651            name: "users".to_owned(),
1652            columns: vec![
1653                ColumnDef {
1654                    name: "id".to_owned(),
1655                    affinity: TypeAffinity::Integer,
1656                    is_ipk: true,
1657                    not_null: true,
1658                },
1659                ColumnDef {
1660                    name: "name".to_owned(),
1661                    affinity: TypeAffinity::Text,
1662                    is_ipk: false,
1663                    not_null: true,
1664                },
1665                ColumnDef {
1666                    name: "email".to_owned(),
1667                    affinity: TypeAffinity::Text,
1668                    is_ipk: false,
1669                    not_null: false,
1670                },
1671            ],
1672            without_rowid: false,
1673            strict: false,
1674        });
1675        schema.add_table(TableDef {
1676            name: "orders".to_owned(),
1677            columns: vec![
1678                ColumnDef {
1679                    name: "id".to_owned(),
1680                    affinity: TypeAffinity::Integer,
1681                    is_ipk: true,
1682                    not_null: true,
1683                },
1684                ColumnDef {
1685                    name: "user_id".to_owned(),
1686                    affinity: TypeAffinity::Integer,
1687                    is_ipk: false,
1688                    not_null: true,
1689                },
1690                ColumnDef {
1691                    name: "amount".to_owned(),
1692                    affinity: TypeAffinity::Real,
1693                    is_ipk: false,
1694                    not_null: false,
1695                },
1696            ],
1697            without_rowid: false,
1698            strict: false,
1699        });
1700        schema
1701    }
1702
1703    fn parse_one(sql: &str) -> Statement {
1704        let mut p = Parser::from_sql(sql);
1705        let (stmts, errs) = p.parse_all();
1706        assert!(errs.is_empty(), "parse errors: {errs:?}");
1707        assert_eq!(stmts.len(), 1);
1708        stmts.into_iter().next().unwrap()
1709    }
1710
1711    // ── Schema tests ──
1712
1713    #[test]
1714    fn test_schema_find_table_case_insensitive() {
1715        let schema = make_schema();
1716        assert!(schema.find_table("users").is_some());
1717        assert!(schema.find_table("USERS").is_some());
1718        assert!(schema.find_table("Users").is_some());
1719        assert!(schema.find_table("nonexistent").is_none());
1720    }
1721
1722    #[test]
1723    fn test_schema_find_table_in_named_namespace() {
1724        let mut schema = make_schema();
1725        schema.add_table_in_schema(
1726            "aux",
1727            TableDef {
1728                name: "users".to_owned(),
1729                columns: vec![ColumnDef {
1730                    name: "nickname".to_owned(),
1731                    affinity: TypeAffinity::Text,
1732                    is_ipk: false,
1733                    not_null: false,
1734                }],
1735                without_rowid: false,
1736                strict: false,
1737            },
1738        );
1739
1740        assert!(schema.find_table_in_schema(Some("main"), "users").is_some());
1741        assert!(schema.find_table_in_schema(Some("aux"), "users").is_some());
1742        assert!(schema.find_table_in_schema(Some("AUX"), "USERS").is_some());
1743        assert!(
1744            schema
1745                .find_table_in_schema(Some("missing"), "users")
1746                .is_none()
1747        );
1748    }
1749
1750    #[test]
1751    fn test_table_find_column() {
1752        let schema = make_schema();
1753        let users = schema.find_table("users").unwrap();
1754        assert!(users.has_column("id"));
1755        assert!(users.has_column("ID"));
1756        assert!(!users.has_column("nonexistent"));
1757    }
1758
1759    #[test]
1760    fn test_table_rowid_alias() {
1761        let schema = make_schema();
1762        let users = schema.find_table("users").unwrap();
1763        assert!(users.is_rowid_alias("rowid"));
1764        assert!(users.is_rowid_alias("_rowid_"));
1765        assert!(users.is_rowid_alias("oid"));
1766        assert!(users.is_rowid_alias("id")); // IPK
1767        assert!(!users.is_rowid_alias("name"));
1768    }
1769
1770    #[test]
1771    fn test_table_rowid_alias_respects_shadowing() {
1772        let mut schema = Schema::new();
1773        schema.add_table(TableDef {
1774            name: "shadowed".to_owned(),
1775            columns: vec![
1776                ColumnDef {
1777                    name: "rowid".to_owned(),
1778                    affinity: TypeAffinity::Text,
1779                    is_ipk: false,
1780                    not_null: false,
1781                },
1782                ColumnDef {
1783                    name: "_rowid_".to_owned(),
1784                    affinity: TypeAffinity::Text,
1785                    is_ipk: false,
1786                    not_null: false,
1787                },
1788                ColumnDef {
1789                    name: "id".to_owned(),
1790                    affinity: TypeAffinity::Integer,
1791                    is_ipk: true,
1792                    not_null: false,
1793                },
1794            ],
1795            without_rowid: false,
1796            strict: false,
1797        });
1798
1799        let shadowed = schema.find_table("shadowed").unwrap();
1800        assert!(!shadowed.is_rowid_alias("rowid"));
1801        assert!(!shadowed.is_rowid_alias("_rowid_"));
1802        assert!(shadowed.is_rowid_alias("oid"));
1803        assert!(shadowed.is_rowid_alias("id"));
1804    }
1805
1806    #[test]
1807    fn test_table_rowid_alias_disabled_for_without_rowid_tables() {
1808        let mut schema = Schema::new();
1809        schema.add_table(TableDef {
1810            name: "wr".to_owned(),
1811            columns: vec![
1812                ColumnDef {
1813                    name: "id".to_owned(),
1814                    affinity: TypeAffinity::Integer,
1815                    is_ipk: true,
1816                    not_null: true,
1817                },
1818                ColumnDef {
1819                    name: "payload".to_owned(),
1820                    affinity: TypeAffinity::Text,
1821                    is_ipk: false,
1822                    not_null: false,
1823                },
1824            ],
1825            without_rowid: true,
1826            strict: false,
1827        });
1828
1829        let wr = schema.find_table("wr").unwrap();
1830        assert!(!wr.is_rowid_alias("rowid"));
1831        assert!(!wr.is_rowid_alias("_rowid_"));
1832        assert!(!wr.is_rowid_alias("oid"));
1833        assert!(!wr.is_rowid_alias("id"));
1834        assert!(wr.has_column("id"));
1835    }
1836
1837    // ── Scope tests ──
1838
1839    #[test]
1840    fn test_scope_resolve_qualified_column() {
1841        let mut scope = Scope::root();
1842        let schema = make_schema();
1843        let cols: HashSet<String> = ["id", "name", "email"]
1844            .iter()
1845            .map(ToString::to_string)
1846            .collect();
1847        scope.add_alias("u", "users", Some(cols));
1848
1849        assert_eq!(
1850            scope.resolve_column(&schema, Some("u"), "id"),
1851            ResolveResult::Resolved("u".to_string())
1852        );
1853        assert_eq!(
1854            scope.resolve_column(&schema, Some("u"), "nonexistent"),
1855            ResolveResult::ColumnNotFound
1856        );
1857        assert_eq!(
1858            scope.resolve_column(&schema, Some("x"), "id"),
1859            ResolveResult::TableNotFound
1860        );
1861    }
1862
1863    #[test]
1864    fn test_scope_resolve_unqualified_column() {
1865        let mut scope = Scope::root();
1866        let schema = make_schema();
1867        scope.add_alias(
1868            "u",
1869            "users",
1870            Some(["id", "name"].iter().map(ToString::to_string).collect()),
1871        );
1872        scope.add_alias(
1873            "o",
1874            "orders",
1875            Some(["id", "user_id"].iter().map(ToString::to_string).collect()),
1876        );
1877
1878        // "name" is unique → resolved to "u"
1879        assert_eq!(
1880            scope.resolve_column(&schema, None, "name"),
1881            ResolveResult::Resolved("u".to_string())
1882        );
1883
1884        // "user_id" is unique → resolved to "o"
1885        assert_eq!(
1886            scope.resolve_column(&schema, None, "user_id"),
1887            ResolveResult::Resolved("o".to_string())
1888        );
1889
1890        // "id" is ambiguous
1891        match scope.resolve_column(&schema, None, "id") {
1892            ResolveResult::Ambiguous(candidates) => {
1893                assert_eq!(candidates.len(), 2);
1894            }
1895            other => panic!("expected Ambiguous, got {other:?}"),
1896        }
1897
1898        // "nonexistent" not found
1899        assert_eq!(
1900            scope.resolve_column(&schema, None, "nonexistent"),
1901            ResolveResult::ColumnNotFound
1902        );
1903    }
1904
1905    #[test]
1906    fn test_scope_child_inherits_parent() {
1907        let mut parent = Scope::root();
1908        let schema = make_schema();
1909        parent.add_alias(
1910            "u",
1911            "users",
1912            Some(["id", "name"].iter().map(ToString::to_string).collect()),
1913        );
1914        let child = Scope::child(parent);
1915
1916        // Child can see parent's columns.
1917        assert_eq!(
1918            child.resolve_column(&schema, Some("u"), "id"),
1919            ResolveResult::Resolved("u".to_string())
1920        );
1921    }
1922
1923    // ── Resolver tests ──
1924
1925    #[test]
1926    fn test_resolve_simple_select() {
1927        let schema = make_schema();
1928        let stmt = parse_one("SELECT id, name FROM users");
1929        let mut resolver = Resolver::new(&schema);
1930        let errors = resolver.resolve_statement(&stmt);
1931        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
1932        assert_eq!(resolver.tables_resolved, 1);
1933        assert_eq!(resolver.columns_bound, 2);
1934    }
1935
1936    #[test]
1937    fn test_resolve_qualified_column() {
1938        let schema = make_schema();
1939        let stmt = parse_one("SELECT u.id, u.name FROM users u");
1940        let mut resolver = Resolver::new(&schema);
1941        let errors = resolver.resolve_statement(&stmt);
1942        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
1943        assert_eq!(resolver.tables_resolved, 1);
1944        assert_eq!(resolver.columns_bound, 2);
1945    }
1946
1947    #[test]
1948    fn test_resolve_select_from_named_namespace() {
1949        let mut schema = make_schema();
1950        schema.add_table_in_schema(
1951            "aux",
1952            TableDef {
1953                name: "users".to_owned(),
1954                columns: vec![
1955                    ColumnDef {
1956                        name: "id".to_owned(),
1957                        affinity: TypeAffinity::Integer,
1958                        is_ipk: true,
1959                        not_null: true,
1960                    },
1961                    ColumnDef {
1962                        name: "nickname".to_owned(),
1963                        affinity: TypeAffinity::Text,
1964                        is_ipk: false,
1965                        not_null: false,
1966                    },
1967                ],
1968                without_rowid: false,
1969                strict: false,
1970            },
1971        );
1972
1973        let stmt = parse_one("SELECT nickname FROM aux.users");
1974        let mut resolver = Resolver::new(&schema);
1975        let errors = resolver.resolve_statement(&stmt);
1976        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
1977        assert_eq!(resolver.tables_resolved, 1);
1978        assert_eq!(resolver.columns_bound, 1);
1979    }
1980
1981    #[test]
1982    fn test_resolve_named_namespace_does_not_fall_back_to_main_schema() {
1983        let mut schema = make_schema();
1984        schema.add_table_in_schema(
1985            "aux",
1986            TableDef {
1987                name: "users".to_owned(),
1988                columns: vec![
1989                    ColumnDef {
1990                        name: "id".to_owned(),
1991                        affinity: TypeAffinity::Integer,
1992                        is_ipk: true,
1993                        not_null: true,
1994                    },
1995                    ColumnDef {
1996                        name: "nickname".to_owned(),
1997                        affinity: TypeAffinity::Text,
1998                        is_ipk: false,
1999                        not_null: false,
2000                    },
2001                ],
2002                without_rowid: false,
2003                strict: false,
2004            },
2005        );
2006
2007        let stmt = parse_one("SELECT name FROM aux.users");
2008        let mut resolver = Resolver::new(&schema);
2009        let errors = resolver.resolve_statement(&stmt);
2010        assert_eq!(errors.len(), 1, "expected unresolved aux.users.name");
2011        assert!(matches!(
2012            errors[0].kind,
2013            SemanticErrorKind::UnresolvedColumn { .. }
2014        ));
2015    }
2016
2017    #[test]
2018    fn test_resolve_join() {
2019        let schema = make_schema();
2020        let stmt =
2021            parse_one("SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id");
2022        let mut resolver = Resolver::new(&schema);
2023        let errors = resolver.resolve_statement(&stmt);
2024        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2025        assert_eq!(resolver.tables_resolved, 2);
2026        assert_eq!(resolver.columns_bound, 4); // u.name, o.amount, u.id, o.user_id
2027    }
2028
2029    #[test]
2030    fn test_resolve_join_using() {
2031        let schema = make_schema();
2032        let stmt = parse_one("SELECT u.name, o.amount FROM users u JOIN orders o USING (id)");
2033        let mut resolver = Resolver::new(&schema);
2034        let errors = resolver.resolve_statement(&stmt);
2035        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2036        assert_eq!(resolver.tables_resolved, 2);
2037        assert_eq!(resolver.columns_bound, 3); // u.name, o.amount, id (resolved redundantly but bounded once)
2038    }
2039
2040    #[test]
2041    fn test_resolve_unresolved_table() {
2042        let schema = make_schema();
2043        let stmt = parse_one("SELECT * FROM nonexistent");
2044        let mut resolver = Resolver::new(&schema);
2045        let errors = resolver.resolve_statement(&stmt);
2046        assert_eq!(errors.len(), 1);
2047        assert!(matches!(
2048            errors[0].kind,
2049            SemanticErrorKind::UnresolvedTable { .. }
2050        ));
2051    }
2052
2053    #[test]
2054    fn test_resolve_unresolved_column() {
2055        let schema = make_schema();
2056        let stmt = parse_one("SELECT nonexistent FROM users");
2057        let mut resolver = Resolver::new(&schema);
2058        let errors = resolver.resolve_statement(&stmt);
2059        assert_eq!(errors.len(), 1);
2060        assert!(matches!(
2061            errors[0].kind,
2062            SemanticErrorKind::UnresolvedColumn { .. }
2063        ));
2064    }
2065
2066    #[test]
2067    fn test_unaliased_subqueries() {
2068        let schema = make_schema();
2069        // Since there are two unknown subqueries and a is not known, "a" should be reported as unresolved
2070        let stmt = parse_one("SELECT a FROM (SELECT 1), (SELECT 2)");
2071        let mut resolver = Resolver::new(&schema);
2072        let errors = resolver.resolve_statement(&stmt);
2073        assert_eq!(errors.len(), 1, "Expected unresolved column error!");
2074        assert!(matches!(
2075            errors[0].kind,
2076            SemanticErrorKind::UnresolvedColumn { .. }
2077        ));
2078    }
2079
2080    #[test]
2081    fn test_resolve_ambiguous_column() {
2082        let schema = make_schema();
2083        let stmt = parse_one("SELECT id FROM users, orders");
2084        let mut resolver = Resolver::new(&schema);
2085        let errors = resolver.resolve_statement(&stmt);
2086        assert_eq!(errors.len(), 1);
2087        assert!(matches!(
2088            errors[0].kind,
2089            SemanticErrorKind::AmbiguousColumn { .. }
2090        ));
2091    }
2092
2093    #[test]
2094    fn test_resolve_where_clause() {
2095        let schema = make_schema();
2096        let stmt = parse_one("SELECT name FROM users WHERE id > 10");
2097        let mut resolver = Resolver::new(&schema);
2098        let errors = resolver.resolve_statement(&stmt);
2099        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2100        assert_eq!(resolver.columns_bound, 2); // name, id
2101    }
2102
2103    #[test]
2104    fn test_resolve_star_select() {
2105        let schema = make_schema();
2106        let stmt = parse_one("SELECT * FROM users");
2107        let mut resolver = Resolver::new(&schema);
2108        let errors = resolver.resolve_statement(&stmt);
2109        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2110        assert_eq!(resolver.tables_resolved, 1);
2111    }
2112
2113    #[test]
2114    fn test_resolve_schema_qualified_table_star() {
2115        let mut schema = make_schema();
2116        schema.add_table_in_schema(
2117            "aux",
2118            TableDef {
2119                name: "users".to_owned(),
2120                columns: vec![
2121                    ColumnDef {
2122                        name: "id".to_owned(),
2123                        affinity: TypeAffinity::Integer,
2124                        is_ipk: true,
2125                        not_null: true,
2126                    },
2127                    ColumnDef {
2128                        name: "nickname".to_owned(),
2129                        affinity: TypeAffinity::Text,
2130                        is_ipk: false,
2131                        not_null: false,
2132                    },
2133                ],
2134                without_rowid: false,
2135                strict: false,
2136            },
2137        );
2138
2139        let stmt = parse_one("SELECT aux.users.* FROM aux.users");
2140        let mut resolver = Resolver::new(&schema);
2141        let errors = resolver.resolve_statement(&stmt);
2142        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2143        assert_eq!(resolver.tables_resolved, 1);
2144    }
2145
2146    #[test]
2147    fn test_resolve_star_in_subquery_without_tables() {
2148        let schema = make_schema();
2149        let stmt = parse_one("SELECT (SELECT *) FROM users");
2150        let mut resolver = Resolver::new(&schema);
2151        let errors = resolver.resolve_statement(&stmt);
2152        assert_eq!(errors.len(), 1);
2153        assert!(matches!(
2154            errors[0].kind,
2155            SemanticErrorKind::NoTablesSpecifiedForStar
2156        ));
2157    }
2158
2159    #[test]
2160    fn test_resolve_insert_checks_table() {
2161        let schema = make_schema();
2162        let stmt = parse_one("INSERT INTO nonexistent VALUES (1)");
2163        let mut resolver = Resolver::new(&schema);
2164        let errors = resolver.resolve_statement(&stmt);
2165        assert_eq!(errors.len(), 1);
2166        assert!(matches!(
2167            errors[0].kind,
2168            SemanticErrorKind::UnresolvedTable { .. }
2169        ));
2170    }
2171
2172    #[test]
2173    fn test_resolve_rowid_column() {
2174        let schema = make_schema();
2175        let stmt = parse_one("SELECT rowid, _rowid_, oid FROM users");
2176        let mut resolver = Resolver::new(&schema);
2177        let errors = resolver.resolve_statement(&stmt);
2178        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2179    }
2180
2181    #[test]
2182    fn test_order_by_select_alias_shadowing() {
2183        let mut schema = Schema::new();
2184        schema.add_table(TableDef {
2185            name: "tbl".to_owned(),
2186            columns: vec![ColumnDef {
2187                name: "a".to_owned(),
2188                affinity: TypeAffinity::Integer,
2189                is_ipk: false,
2190                not_null: false,
2191            }],
2192            without_rowid: false,
2193            strict: false,
2194        });
2195
2196        // "a" is both an alias and a column in the table.
2197        let stmt = parse_one("SELECT 1 AS a FROM tbl ORDER BY a");
2198        let mut resolver = Resolver::new(&schema);
2199        let errors = resolver.resolve_statement(&stmt);
2200
2201        // SQLite permits ORDER BY to resolve the SELECT-list alias here rather
2202        // than treating the alias/column name overlap as ambiguous.
2203        if !errors.is_empty() {
2204            panic!("Expected no errors, but got: {:?}", errors);
2205        }
2206    }
2207
2208    #[test]
2209    fn test_compound_order_by_can_resolve_alias_from_later_arm() {
2210        let schema = make_schema();
2211        let stmt = parse_one("SELECT 1 AS a UNION SELECT 2 AS b ORDER BY b");
2212        let mut resolver = Resolver::new(&schema);
2213        let errors = resolver.resolve_statement(&stmt);
2214        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2215    }
2216
2217    #[test]
2218    fn test_compound_order_by_can_match_output_expression_from_later_arm() {
2219        let mut schema = Schema::new();
2220        schema.add_table(TableDef {
2221            name: "tbl".to_owned(),
2222            columns: vec![
2223                ColumnDef {
2224                    name: "a".to_owned(),
2225                    affinity: TypeAffinity::Integer,
2226                    is_ipk: false,
2227                    not_null: false,
2228                },
2229                ColumnDef {
2230                    name: "b".to_owned(),
2231                    affinity: TypeAffinity::Integer,
2232                    is_ipk: false,
2233                    not_null: false,
2234                },
2235            ],
2236            without_rowid: false,
2237            strict: false,
2238        });
2239
2240        let stmt = parse_one("SELECT a + 1 FROM tbl UNION SELECT b + 1 FROM tbl ORDER BY b + 1");
2241        let mut resolver = Resolver::new(&schema);
2242        let errors = resolver.resolve_statement(&stmt);
2243        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2244    }
2245
2246    // ── Metrics tests ──
2247
2248    #[test]
2249    fn test_semantic_metrics() {
2250        // Delta-based assertion: never call reset_semantic_metrics() in tests
2251        // as it races with parallel tests.
2252        let before = semantic_metrics_snapshot();
2253        let schema = make_schema();
2254
2255        // Trigger an error.
2256        let stmt = parse_one("SELECT nonexistent FROM users");
2257        let mut resolver = Resolver::new(&schema);
2258        let _ = resolver.resolve_statement(&stmt);
2259
2260        let after = semantic_metrics_snapshot();
2261        assert!(
2262            after.fsqlite_semantic_errors_total > before.fsqlite_semantic_errors_total,
2263            "expected at least 1 new semantic error, before={}, after={}",
2264            before.fsqlite_semantic_errors_total,
2265            after.fsqlite_semantic_errors_total,
2266        );
2267    }
2268
2269    #[test]
2270    fn test_resolve_function_arity() {
2271        let schema = make_schema();
2272        let stmt = parse_one("SELECT sum(1, 2)");
2273        let mut resolver = Resolver::new(&schema);
2274        let errors = resolver.resolve_statement(&stmt);
2275        assert_eq!(errors.len(), 1);
2276        assert!(matches!(
2277            errors[0].kind,
2278            SemanticErrorKind::FunctionArityMismatch { .. }
2279        ));
2280    }
2281
2282    #[test]
2283    fn test_resolve_group_by_alias() {
2284        let schema = make_schema();
2285        let stmt = parse_one("SELECT id AS x FROM users GROUP BY x");
2286        let mut resolver = Resolver::new(&schema);
2287        let errors = resolver.resolve_statement(&stmt);
2288        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
2289    }
2290
2291    #[test]
2292    fn test_resolve_escape_on_non_like() {
2293        let schema = make_schema();
2294        // LIKE with ESCAPE is valid.
2295        let stmt_like = parse_one("SELECT 1 LIKE 2 ESCAPE 3");
2296        let mut resolver_like = Resolver::new(&schema);
2297        let errors_like = resolver_like.resolve_statement(&stmt_like);
2298        assert!(errors_like.is_empty(), "LIKE ESCAPE should be valid");
2299
2300        // GLOB with ESCAPE is invalid.
2301        let stmt_glob = parse_one("SELECT 1 GLOB 2 ESCAPE 3");
2302        let mut resolver_glob = Resolver::new(&schema);
2303        let errors_glob = resolver_glob.resolve_statement(&stmt_glob);
2304        assert_eq!(errors_glob.len(), 1);
2305        assert!(matches!(
2306            errors_glob[0].kind,
2307            SemanticErrorKind::FunctionArityMismatch { .. }
2308        ));
2309    }
2310
2311    #[test]
2312    fn test_update_assignment_target_strict() {
2313        let schema = make_schema();
2314        // The outer query has a table `orders` with `amount`.
2315        // The inner query updates `users`.
2316        // `users` does not have `amount`.
2317        // If the assignment target incorrectly resolves against the outer scope, no error is emitted.
2318        // It SHOULD emit an error because `amount` is not in `users`.
2319        let stmt = parse_one("WITH cte(amount) AS (SELECT 1) UPDATE users SET amount = 1 FROM cte");
2320        let mut resolver = Resolver::new(&schema);
2321        let errors = resolver.resolve_statement(&stmt);
2322        assert_eq!(
2323            errors.len(),
2324            1,
2325            "Should report amount as unresolved for users table, instead got: {:?}",
2326            errors
2327        );
2328    }
2329
2330    #[test]
2331    fn test_rowid_resolution() {
2332        let schema = make_schema();
2333        let mut p = Parser::from_sql("SELECT rowid FROM users");
2334        let (stmts, _) = p.parse_all();
2335        let stmt = stmts.into_iter().next().unwrap();
2336        let mut resolver = Resolver::new(&schema);
2337        let errors = resolver.resolve_statement(&stmt);
2338        assert!(errors.is_empty(), "errors: {:?}", errors);
2339    }
2340}