Skip to main content

fsqlite_parser/
semantic.rs

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