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