Skip to main content

fsqlite_parser/
semantic.rs

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