Skip to main content

fsqlite_parser/
semantic.rs

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