Skip to main content

polyglot_sql/
validation.rs

1//! Schema-aware and semantic SQL validation.
2//!
3//! This module extends syntax validation with:
4//! - schema checks (unknown tables/columns)
5//! - optional semantic warnings (SELECT *, LIMIT without ORDER BY, etc.)
6
7use crate::dialects::{Dialect, DialectType};
8use crate::error::{ValidationError, ValidationResult};
9use crate::expressions::{
10    Column, DataType, Expression, Function, Insert, JoinKind, OracleDataType, TableRef, Update,
11};
12use crate::function_catalog::FunctionCatalog;
13#[cfg(any(
14    feature = "function-catalog-clickhouse",
15    feature = "function-catalog-duckdb",
16    feature = "function-catalog-all-dialects"
17))]
18use crate::function_catalog::{
19    FunctionNameCase as CoreFunctionNameCase, FunctionSignature as CoreFunctionSignature,
20    HashMapFunctionCatalog,
21};
22use crate::function_registry::canonical_typed_function_name_upper;
23use crate::optimizer::annotate_types::annotate_types;
24use crate::optimizer::normalize_identifiers::{get_normalization_strategy, normalize_identifier};
25use crate::optimizer::qualify_columns::normalize_dotted_columns_in_scope;
26use crate::resolver::Resolver;
27use crate::schema::{MappingSchema, Schema as SqlSchema};
28use crate::scope::{build_scope, walk_in_scope};
29use crate::traversal::ExpressionWalk;
30use serde::{Deserialize, Serialize};
31use std::collections::{HashMap, HashSet};
32use std::sync::Arc;
33
34#[cfg(any(
35    feature = "function-catalog-clickhouse",
36    feature = "function-catalog-duckdb",
37    feature = "function-catalog-all-dialects"
38))]
39use std::sync::LazyLock;
40
41/// Column definition used for schema-aware validation.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44pub struct SchemaColumn {
45    /// Column name.
46    pub name: String,
47    /// Optional column data type used by type inference and validation.
48    #[serde(default, rename = "type")]
49    pub data_type: String,
50    /// Whether the column allows NULL values.
51    #[serde(default)]
52    pub nullable: Option<bool>,
53    /// Whether this column is part of a primary key.
54    #[serde(default, rename = "primaryKey")]
55    pub primary_key: bool,
56    /// Whether this column has a uniqueness constraint.
57    #[serde(default)]
58    pub unique: bool,
59    /// Optional column-level foreign key reference.
60    #[serde(default)]
61    pub references: Option<SchemaColumnReference>,
62}
63
64/// Column-level foreign key reference metadata.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct SchemaColumnReference {
68    /// Referenced table name.
69    pub table: String,
70    /// Referenced column name.
71    pub column: String,
72    /// Optional schema/namespace of referenced table.
73    #[serde(default)]
74    pub schema: Option<String>,
75}
76
77/// Table-level foreign key reference metadata.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct SchemaForeignKey {
81    /// Optional FK name.
82    #[serde(default)]
83    pub name: Option<String>,
84    /// Source columns in the current table.
85    pub columns: Vec<String>,
86    /// Referenced target table + columns.
87    pub references: SchemaTableReference,
88}
89
90/// Target of a table-level foreign key.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct SchemaTableReference {
94    /// Referenced table name.
95    pub table: String,
96    /// Referenced target columns.
97    pub columns: Vec<String>,
98    /// Optional schema/namespace of referenced table.
99    #[serde(default)]
100    pub schema: Option<String>,
101}
102
103/// Table definition used for schema-aware validation.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(deny_unknown_fields)]
106pub struct SchemaTable {
107    /// Table name.
108    pub name: String,
109    /// Optional schema/namespace name.
110    #[serde(default)]
111    pub schema: Option<String>,
112    /// Column definitions.
113    pub columns: Vec<SchemaColumn>,
114    /// Optional aliases that should resolve to this table.
115    #[serde(default)]
116    pub aliases: Vec<String>,
117    /// Optional primary key column list.
118    #[serde(default, rename = "primaryKey")]
119    pub primary_key: Vec<String>,
120    /// Optional unique key groups.
121    #[serde(default, rename = "uniqueKeys")]
122    pub unique_keys: Vec<Vec<String>>,
123    /// Optional table-level foreign keys.
124    #[serde(default, rename = "foreignKeys")]
125    pub foreign_keys: Vec<SchemaForeignKey>,
126}
127
128/// Schema payload used for schema-aware validation.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(deny_unknown_fields)]
131pub struct ValidationSchema {
132    /// Known tables.
133    pub tables: Vec<SchemaTable>,
134    /// Default strict mode for unknown identifiers.
135    #[serde(default)]
136    pub strict: Option<bool>,
137}
138
139/// Options for schema-aware validation.
140/// JSON accepts snake_case names and camelCase aliases for compound names;
141/// unrecognized options are rejected to avoid silently disabling checks.
142#[derive(Clone, Serialize, Deserialize, Default)]
143#[serde(deny_unknown_fields)]
144pub struct SchemaValidationOptions {
145    /// Per-call parser limits, shared by syntax and schema validation.
146    #[serde(default, alias = "complexityGuard")]
147    pub complexity_guard: Option<crate::ComplexityGuardOptions>,
148    /// Enables type compatibility checks for expressions, DML assignments, and set operations.
149    #[serde(default, alias = "checkTypes")]
150    pub check_types: bool,
151    /// Enables FK/reference integrity checks and query-level reference quality checks.
152    #[serde(default, alias = "checkReferences")]
153    pub check_references: bool,
154    /// If true/false, overrides schema.strict.
155    #[serde(default)]
156    pub strict: Option<bool>,
157    /// Enables semantic correctness errors (E230..E232) and quality warnings (W001..W004).
158    #[serde(default)]
159    pub semantic: bool,
160    /// Enables strict syntax checks (e.g. rejects trailing commas before clause boundaries).
161    #[serde(default, alias = "strictSyntax")]
162    pub strict_syntax: bool,
163    /// Optional external function catalog plugin for dialect-specific function validation.
164    #[serde(skip, default)]
165    pub function_catalog: Option<Arc<dyn FunctionCatalog>>,
166}
167
168#[cfg(any(
169    feature = "function-catalog-clickhouse",
170    feature = "function-catalog-duckdb",
171    feature = "function-catalog-all-dialects"
172))]
173fn to_core_name_case(
174    case: polyglot_sql_function_catalogs::FunctionNameCase,
175) -> CoreFunctionNameCase {
176    match case {
177        polyglot_sql_function_catalogs::FunctionNameCase::Insensitive => {
178            CoreFunctionNameCase::Insensitive
179        }
180        polyglot_sql_function_catalogs::FunctionNameCase::Sensitive => {
181            CoreFunctionNameCase::Sensitive
182        }
183    }
184}
185
186#[cfg(any(
187    feature = "function-catalog-clickhouse",
188    feature = "function-catalog-duckdb",
189    feature = "function-catalog-all-dialects"
190))]
191fn to_core_signatures(
192    signatures: Vec<polyglot_sql_function_catalogs::FunctionSignature>,
193) -> Vec<CoreFunctionSignature> {
194    signatures
195        .into_iter()
196        .map(|signature| CoreFunctionSignature {
197            min_arity: signature.min_arity,
198            max_arity: signature.max_arity,
199        })
200        .collect()
201}
202
203#[cfg(any(
204    feature = "function-catalog-clickhouse",
205    feature = "function-catalog-duckdb",
206    feature = "function-catalog-all-dialects"
207))]
208struct EmbeddedCatalogSink<'a> {
209    catalog: &'a mut HashMapFunctionCatalog,
210    dialect_cache: HashMap<&'static str, Option<DialectType>>,
211}
212
213#[cfg(any(
214    feature = "function-catalog-clickhouse",
215    feature = "function-catalog-duckdb",
216    feature = "function-catalog-all-dialects"
217))]
218impl<'a> EmbeddedCatalogSink<'a> {
219    fn resolve_dialect(&mut self, dialect: &'static str) -> Option<DialectType> {
220        if let Some(cached) = self.dialect_cache.get(dialect) {
221            return *cached;
222        }
223        let parsed = dialect.parse::<DialectType>().ok();
224        self.dialect_cache.insert(dialect, parsed);
225        parsed
226    }
227}
228
229#[cfg(any(
230    feature = "function-catalog-clickhouse",
231    feature = "function-catalog-duckdb",
232    feature = "function-catalog-all-dialects"
233))]
234impl<'a> polyglot_sql_function_catalogs::CatalogSink for EmbeddedCatalogSink<'a> {
235    fn set_dialect_name_case(
236        &mut self,
237        dialect: &'static str,
238        name_case: polyglot_sql_function_catalogs::FunctionNameCase,
239    ) {
240        if let Some(core_dialect) = self.resolve_dialect(dialect) {
241            self.catalog
242                .set_dialect_name_case(core_dialect, to_core_name_case(name_case));
243        }
244    }
245
246    fn set_function_name_case(
247        &mut self,
248        dialect: &'static str,
249        function_name: &str,
250        name_case: polyglot_sql_function_catalogs::FunctionNameCase,
251    ) {
252        if let Some(core_dialect) = self.resolve_dialect(dialect) {
253            self.catalog.set_function_name_case(
254                core_dialect,
255                function_name,
256                to_core_name_case(name_case),
257            );
258        }
259    }
260
261    fn register(
262        &mut self,
263        dialect: &'static str,
264        function_name: &str,
265        signatures: Vec<polyglot_sql_function_catalogs::FunctionSignature>,
266    ) {
267        if let Some(core_dialect) = self.resolve_dialect(dialect) {
268            self.catalog
269                .register(core_dialect, function_name, to_core_signatures(signatures));
270        }
271    }
272}
273
274#[cfg(any(
275    feature = "function-catalog-clickhouse",
276    feature = "function-catalog-duckdb",
277    feature = "function-catalog-all-dialects"
278))]
279fn embedded_function_catalog_arc() -> Arc<dyn FunctionCatalog> {
280    static EMBEDDED: LazyLock<Arc<HashMapFunctionCatalog>> = LazyLock::new(|| {
281        let mut catalog = HashMapFunctionCatalog::default();
282        let mut sink = EmbeddedCatalogSink {
283            catalog: &mut catalog,
284            dialect_cache: HashMap::new(),
285        };
286        polyglot_sql_function_catalogs::register_enabled_catalogs(&mut sink);
287        Arc::new(catalog)
288    });
289
290    EMBEDDED.clone()
291}
292
293#[cfg(any(
294    feature = "function-catalog-clickhouse",
295    feature = "function-catalog-duckdb",
296    feature = "function-catalog-all-dialects"
297))]
298fn default_embedded_function_catalog() -> Option<Arc<dyn FunctionCatalog>> {
299    Some(embedded_function_catalog_arc())
300}
301
302#[cfg(not(any(
303    feature = "function-catalog-clickhouse",
304    feature = "function-catalog-duckdb",
305    feature = "function-catalog-all-dialects"
306)))]
307fn default_embedded_function_catalog() -> Option<Arc<dyn FunctionCatalog>> {
308    None
309}
310
311/// Validation error/warning codes used by schema-aware validation.
312pub mod validation_codes {
313    // Existing schema and semantic checks.
314    pub const E_PARSE_OR_OPTIONS: &str = "E000";
315    pub const E_UNKNOWN_TABLE: &str = "E200";
316    pub const E_UNKNOWN_COLUMN: &str = "E201";
317    pub const E_UNKNOWN_FUNCTION: &str = "E202";
318    pub const E_INVALID_FUNCTION_ARITY: &str = "E203";
319    pub const E_INVALID_GROUPING: &str = "E230";
320    pub const E_INVALID_AGGREGATE: &str = "E231";
321    pub const E_INVALID_WINDOW: &str = "E232";
322
323    pub const W_SELECT_STAR: &str = "W001";
324    pub const W_AGGREGATE_WITHOUT_GROUP_BY: &str = "W002";
325    pub const W_DISTINCT_ORDER_BY: &str = "W003";
326    pub const W_LIMIT_WITHOUT_ORDER_BY: &str = "W004";
327
328    // Phase 2 (type checks): E210-E219, W210-W219.
329    pub const E_TYPE_MISMATCH: &str = "E210";
330    pub const E_INVALID_PREDICATE_TYPE: &str = "E211";
331    pub const E_INVALID_ARITHMETIC_TYPE: &str = "E212";
332    pub const E_INVALID_FUNCTION_ARGUMENT_TYPE: &str = "E213";
333    pub const E_INVALID_ASSIGNMENT_TYPE: &str = "E214";
334    pub const E_SETOP_TYPE_MISMATCH: &str = "E215";
335    pub const E_SETOP_ARITY_MISMATCH: &str = "E216";
336    pub const E_INCOMPATIBLE_COMPARISON_TYPES: &str = "E217";
337    pub const E_INVALID_CAST: &str = "E218";
338    pub const E_UNKNOWN_INFERRED_TYPE: &str = "E219";
339
340    pub const W_IMPLICIT_CAST_COMPARISON: &str = "W210";
341    pub const W_IMPLICIT_CAST_ARITHMETIC: &str = "W211";
342    pub const W_IMPLICIT_CAST_ASSIGNMENT: &str = "W212";
343    pub const W_LOSSY_CAST: &str = "W213";
344    pub const W_SETOP_IMPLICIT_COERCION: &str = "W214";
345    pub const W_PREDICATE_NULLABILITY: &str = "W215";
346    pub const W_FUNCTION_ARGUMENT_COERCION: &str = "W216";
347    pub const W_AGGREGATE_TYPE_COERCION: &str = "W217";
348    pub const W_POSSIBLE_OVERFLOW: &str = "W218";
349    pub const W_POSSIBLE_TRUNCATION: &str = "W219";
350
351    // Phase 2 (reference checks): E220-E229, W220-W229.
352    pub const E_INVALID_FOREIGN_KEY_REFERENCE: &str = "E220";
353    pub const E_AMBIGUOUS_COLUMN_REFERENCE: &str = "E221";
354    pub const E_UNRESOLVED_REFERENCE: &str = "E222";
355    pub const E_CTE_COLUMN_COUNT_MISMATCH: &str = "E223";
356    pub const E_MISSING_REFERENCE_TARGET: &str = "E224";
357
358    pub const W_CARTESIAN_JOIN: &str = "W220";
359    pub const W_JOIN_NOT_USING_DECLARED_REFERENCE: &str = "W221";
360    pub const W_WEAK_REFERENCE_INTEGRITY: &str = "W222";
361}
362
363/// Canonical type family used by schema/type checks.
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
365#[serde(rename_all = "snake_case")]
366pub enum TypeFamily {
367    Unknown,
368    Boolean,
369    Integer,
370    Numeric,
371    String,
372    Binary,
373    Date,
374    Time,
375    Timestamp,
376    Interval,
377    Json,
378    Uuid,
379    Array,
380    Map,
381    Struct,
382}
383
384impl TypeFamily {
385    pub fn is_numeric(self) -> bool {
386        matches!(self, TypeFamily::Integer | TypeFamily::Numeric)
387    }
388
389    pub fn is_temporal(self) -> bool {
390        matches!(
391            self,
392            TypeFamily::Date | TypeFamily::Time | TypeFamily::Timestamp | TypeFamily::Interval
393        )
394    }
395}
396
397#[derive(Debug, Clone)]
398struct TableSchemaEntry {
399    columns: HashMap<String, TypeFamily>,
400    column_order: Vec<String>,
401}
402
403fn lower(s: &str) -> String {
404    s.to_lowercase()
405}
406
407fn split_type_args(data_type: &str) -> Option<(&str, &str)> {
408    let open = data_type.find('(')?;
409    if !data_type.ends_with(')') || open + 1 >= data_type.len() {
410        return None;
411    }
412    let base = data_type[..open].trim();
413    let inner = data_type[open + 1..data_type.len() - 1].trim();
414    Some((base, inner))
415}
416
417/// Canonicalize a schema type string into a stable `TypeFamily`.
418pub fn canonical_type_family(data_type: &str) -> TypeFamily {
419    let trimmed = data_type
420        .trim()
421        .trim_matches(|c| c == '"' || c == '\'' || c == '`');
422    if trimmed.is_empty() {
423        return TypeFamily::Unknown;
424    }
425
426    // Normalize whitespace and lowercase for matching.
427    let normalized = trimmed
428        .split_whitespace()
429        .collect::<Vec<_>>()
430        .join(" ")
431        .to_lowercase();
432
433    // Strip common wrappers first.
434    if let Some((base, inner)) = split_type_args(&normalized) {
435        match base {
436            "nullable" | "lowcardinality" => return canonical_type_family(inner),
437            "array" | "list" => return TypeFamily::Array,
438            "map" => return TypeFamily::Map,
439            "struct" | "row" | "record" => return TypeFamily::Struct,
440            _ => {}
441        }
442    }
443
444    if normalized.starts_with("array<") || normalized.starts_with("list<") {
445        return TypeFamily::Array;
446    }
447    if normalized.starts_with("map<") {
448        return TypeFamily::Map;
449    }
450    if normalized.starts_with("struct<")
451        || normalized.starts_with("row<")
452        || normalized.starts_with("record<")
453        || normalized.starts_with("object<")
454    {
455        return TypeFamily::Struct;
456    }
457
458    if normalized.ends_with("[]") {
459        return TypeFamily::Array;
460    }
461
462    // Remove parameter list if present, e.g. VARCHAR(255), DECIMAL(10,2).
463    let mut base = normalized
464        .split('(')
465        .next()
466        .unwrap_or("")
467        .trim()
468        .to_string();
469    if base.is_empty() {
470        return TypeFamily::Unknown;
471    }
472
473    base = base.strip_prefix("unsigned ").unwrap_or(&base).to_string();
474    base = base.strip_suffix(" unsigned").unwrap_or(&base).to_string();
475
476    match base.as_str() {
477        "bool" | "boolean" => TypeFamily::Boolean,
478        "tinyint" | "smallint" | "int2" | "int" | "integer" | "int4" | "int8" | "bigint"
479        | "serial" | "smallserial" | "bigserial" | "utinyint" | "usmallint" | "uinteger"
480        | "ubigint" | "uint8" | "uint16" | "uint32" | "uint64" | "int16" | "int32" | "int64"
481        | "hugeint" | "int128" | "largeint" | "uhugeint" | "uint128" => TypeFamily::Integer,
482        "numeric" | "decimal" | "dec" | "number" | "float" | "float4" | "float8" | "real"
483        | "double" | "double precision" | "bfloat16" | "float16" | "float32" | "float64" => {
484            TypeFamily::Numeric
485        }
486        "char" | "character" | "varchar" | "character varying" | "nchar" | "nvarchar" | "text"
487        | "string" | "clob" => TypeFamily::String,
488        "binary" | "varbinary" | "blob" | "bytea" | "bytes" => TypeFamily::Binary,
489        "date" => TypeFamily::Date,
490        "time" => TypeFamily::Time,
491        "timestamp"
492        | "timestamptz"
493        | "datetime"
494        | "datetime2"
495        | "smalldatetime"
496        | "timestamp with time zone"
497        | "timestamp without time zone" => TypeFamily::Timestamp,
498        "interval" => TypeFamily::Interval,
499        "json" | "jsonb" | "variant" => TypeFamily::Json,
500        "uuid" | "uniqueidentifier" => TypeFamily::Uuid,
501        "array" | "list" => TypeFamily::Array,
502        "map" => TypeFamily::Map,
503        "struct" | "row" | "record" | "object" => TypeFamily::Struct,
504        _ => TypeFamily::Unknown,
505    }
506}
507
508fn build_schema_map(schema: &ValidationSchema) -> HashMap<String, TableSchemaEntry> {
509    let mut map = HashMap::new();
510
511    for table in &schema.tables {
512        let column_order: Vec<String> = table.columns.iter().map(|c| lower(&c.name)).collect();
513        let columns: HashMap<String, TypeFamily> = table
514            .columns
515            .iter()
516            .map(|c| (lower(&c.name), canonical_type_family(&c.data_type)))
517            .collect();
518        let entry = TableSchemaEntry {
519            columns,
520            column_order,
521        };
522
523        let simple_name = lower(&table.name);
524        map.insert(simple_name, entry.clone());
525
526        if let Some(table_schema) = &table.schema {
527            map.insert(
528                format!("{}.{}", lower(table_schema), lower(&table.name)),
529                entry.clone(),
530            );
531        }
532
533        for alias in &table.aliases {
534            map.insert(lower(alias), entry.clone());
535        }
536    }
537
538    map
539}
540
541fn type_family_to_data_type(family: TypeFamily) -> DataType {
542    match family {
543        TypeFamily::Unknown => DataType::Unknown,
544        TypeFamily::Boolean => DataType::Boolean,
545        TypeFamily::Integer => DataType::Int {
546            length: None,
547            integer_spelling: false,
548        },
549        TypeFamily::Numeric => DataType::Double {
550            precision: None,
551            scale: None,
552        },
553        TypeFamily::String => DataType::VarChar {
554            length: None,
555            parenthesized_length: false,
556        },
557        TypeFamily::Binary => DataType::VarBinary { length: None },
558        TypeFamily::Date => DataType::Date,
559        TypeFamily::Time => DataType::Time {
560            precision: None,
561            timezone: false,
562        },
563        TypeFamily::Timestamp => DataType::Timestamp {
564            precision: None,
565            timezone: false,
566        },
567        TypeFamily::Interval => DataType::Interval {
568            unit: None,
569            to: None,
570        },
571        TypeFamily::Json => DataType::Json,
572        TypeFamily::Uuid => DataType::Uuid,
573        TypeFamily::Array => DataType::Array {
574            element_type: Box::new(DataType::Unknown),
575            dimension: None,
576        },
577        TypeFamily::Map => DataType::Map {
578            key_type: Box::new(DataType::Unknown),
579            value_type: Box::new(DataType::Unknown),
580        },
581        TypeFamily::Struct => DataType::Struct {
582            fields: Vec::new(),
583            nested: false,
584        },
585    }
586}
587
588fn build_resolver_schema(schema: &ValidationSchema) -> MappingSchema {
589    let mut mapping = MappingSchema::new();
590
591    for table in &schema.tables {
592        let columns: Vec<(String, DataType)> = table
593            .columns
594            .iter()
595            .map(|column| {
596                (
597                    lower(&column.name),
598                    type_family_to_data_type(canonical_type_family(&column.data_type)),
599                )
600            })
601            .collect();
602
603        let mut table_names = Vec::new();
604        table_names.push(lower(&table.name));
605        if let Some(table_schema) = &table.schema {
606            table_names.push(format!("{}.{}", lower(table_schema), lower(&table.name)));
607        }
608        for alias in &table.aliases {
609            table_names.push(lower(alias));
610        }
611
612        let mut dedup = HashSet::new();
613        for table_name in table_names {
614            if dedup.insert(table_name.clone()) {
615                let _ = mapping.add_table(&table_name, &columns, None);
616            }
617        }
618    }
619
620    mapping
621}
622
623/// Build a `MappingSchema` from a `ValidationSchema` payload.
624///
625/// This is useful for APIs that already accept `ValidationSchema`-shaped input
626/// (for example JSON wrappers) and need to run schema-aware lineage or other
627/// resolver-based analysis.
628pub fn mapping_schema_from_validation_schema(schema: &ValidationSchema) -> MappingSchema {
629    build_resolver_schema(schema)
630}
631
632/// Build a dialect-aware `MappingSchema` while preserving nested types such
633/// as ARRAY, MAP, and STRUCT. If a type spelling is not understood by the
634/// selected dialect, this falls back to the validation resolver's broad type
635/// family so existing best-effort behavior is retained.
636pub fn mapping_schema_from_validation_schema_with_dialect(
637    schema: &ValidationSchema,
638    dialect: DialectType,
639) -> MappingSchema {
640    let broad_schema = build_resolver_schema(schema);
641    let dialect_impl = Dialect::get(dialect);
642    let mut mapping = MappingSchema::with_dialect(dialect);
643
644    for table in &schema.tables {
645        let fallback_table = lower(&table.name);
646        let columns: Vec<(String, DataType)> = table
647            .columns
648            .iter()
649            .map(|column| {
650                let data_type = dialect_impl
651                    .parse_data_type(column.data_type.trim())
652                    .unwrap_or_else(|_| {
653                        broad_schema
654                            .get_column_type(&fallback_table, &column.name)
655                            .unwrap_or(DataType::Unknown)
656                    });
657                (column.name.clone(), data_type)
658            })
659            .collect();
660
661        let mut table_names = vec![table.name.clone()];
662        if let Some(table_schema) = &table.schema {
663            table_names.push(format!("{}.{}", table_schema, table.name));
664        }
665        table_names.extend(table.aliases.iter().cloned());
666
667        let mut seen = HashSet::new();
668        for table_name in table_names {
669            if seen.insert(lower(&table_name)) {
670                let _ = mapping.add_table(&table_name, &columns, Some(dialect));
671            }
672        }
673    }
674
675    mapping
676}
677
678fn table_ref_candidates(table: &TableRef) -> Vec<String> {
679    let name = lower(&table.name.name);
680    let schema = table.schema.as_ref().map(|s| lower(&s.name));
681    let catalog = table.catalog.as_ref().map(|c| lower(&c.name));
682
683    let mut candidates = Vec::new();
684    if let (Some(catalog), Some(schema)) = (&catalog, &schema) {
685        candidates.push(format!("{}.{}.{}", catalog, schema, name));
686    }
687    if let Some(schema) = &schema {
688        candidates.push(format!("{}.{}", schema, name));
689    }
690    candidates.push(name);
691    candidates
692}
693
694fn table_ref_display_name(table: &TableRef) -> String {
695    let mut parts = Vec::new();
696    if let Some(catalog) = &table.catalog {
697        parts.push(catalog.name.clone());
698    }
699    if let Some(schema) = &table.schema {
700        parts.push(schema.name.clone());
701    }
702    parts.push(table.name.name.clone());
703    parts.join(".")
704}
705
706#[derive(Debug, Default, Clone)]
707struct TypeCheckContext {
708    referenced_tables: HashSet<String>,
709    table_aliases: HashMap<String, String>,
710}
711
712fn type_family_name(family: TypeFamily) -> &'static str {
713    match family {
714        TypeFamily::Unknown => "unknown",
715        TypeFamily::Boolean => "boolean",
716        TypeFamily::Integer => "integer",
717        TypeFamily::Numeric => "numeric",
718        TypeFamily::String => "string",
719        TypeFamily::Binary => "binary",
720        TypeFamily::Date => "date",
721        TypeFamily::Time => "time",
722        TypeFamily::Timestamp => "timestamp",
723        TypeFamily::Interval => "interval",
724        TypeFamily::Json => "json",
725        TypeFamily::Uuid => "uuid",
726        TypeFamily::Array => "array",
727        TypeFamily::Map => "map",
728        TypeFamily::Struct => "struct",
729    }
730}
731
732fn is_string_like(family: TypeFamily) -> bool {
733    matches!(family, TypeFamily::String)
734}
735
736fn is_string_or_binary(family: TypeFamily) -> bool {
737    matches!(family, TypeFamily::String | TypeFamily::Binary)
738}
739
740fn type_issue(
741    strict: bool,
742    error_code: &str,
743    warning_code: &str,
744    message: impl Into<String>,
745) -> ValidationError {
746    if strict {
747        ValidationError::error(message.into(), error_code)
748    } else {
749        ValidationError::warning(message.into(), warning_code)
750    }
751}
752
753fn data_type_family(data_type: &DataType) -> TypeFamily {
754    match data_type {
755        DataType::Boolean => TypeFamily::Boolean,
756        DataType::TinyInt { .. }
757        | DataType::SmallInt { .. }
758        | DataType::Int { .. }
759        | DataType::BigInt { .. }
760        | DataType::Int128
761        | DataType::UInt8
762        | DataType::UInt16
763        | DataType::UInt32
764        | DataType::UInt64
765        | DataType::UInt128 => TypeFamily::Integer,
766        DataType::Float { .. } | DataType::Double { .. } | DataType::Decimal { .. } => {
767            TypeFamily::Numeric
768        }
769        DataType::Oracle { oracle_type } => match oracle_type {
770            OracleDataType::Number { .. }
771            | OracleDataType::BinaryFloat
772            | OracleDataType::BinaryDouble
773            | OracleDataType::Float { .. } => TypeFamily::Numeric,
774            OracleDataType::Character { .. }
775            | OracleDataType::Clob { .. }
776            | OracleDataType::Long { raw: false }
777            | OracleDataType::RowId => TypeFamily::String,
778            OracleDataType::Blob
779            | OracleDataType::Raw { .. }
780            | OracleDataType::Long { raw: true } => TypeFamily::Binary,
781            OracleDataType::Date => TypeFamily::Date,
782            OracleDataType::Timestamp { .. } => TypeFamily::Timestamp,
783            OracleDataType::IntervalYearToMonth { .. }
784            | OracleDataType::IntervalDayToSecond { .. } => TypeFamily::Interval,
785        },
786        DataType::Char { .. }
787        | DataType::VarChar { .. }
788        | DataType::String { .. }
789        | DataType::Text
790        | DataType::TextWithLength { .. }
791        | DataType::CharacterSet { .. } => TypeFamily::String,
792        DataType::Binary { .. } | DataType::VarBinary { .. } | DataType::Blob => TypeFamily::Binary,
793        DataType::Date => TypeFamily::Date,
794        DataType::Time { .. } => TypeFamily::Time,
795        DataType::Timestamp { .. } => TypeFamily::Timestamp,
796        DataType::Interval { .. } => TypeFamily::Interval,
797        DataType::Json | DataType::JsonB => TypeFamily::Json,
798        DataType::Uuid => TypeFamily::Uuid,
799        DataType::Array { .. } | DataType::List { .. } => TypeFamily::Array,
800        DataType::Map { .. } => TypeFamily::Map,
801        DataType::Struct { .. } | DataType::Object { .. } | DataType::Union { .. } => {
802            TypeFamily::Struct
803        }
804        DataType::Nullable { inner } => data_type_family(inner),
805        DataType::Custom { name } => canonical_type_family(name),
806        DataType::Unknown => TypeFamily::Unknown,
807        DataType::Bit { .. } | DataType::VarBit { .. } => TypeFamily::Binary,
808        DataType::Enum { .. } | DataType::Set { .. } => TypeFamily::String,
809        DataType::Vector { .. } => TypeFamily::Array,
810        DataType::Geometry { .. } | DataType::Geography { .. } => TypeFamily::Struct,
811    }
812}
813
814fn collect_type_check_context(
815    stmt: &Expression,
816    schema_map: &HashMap<String, TableSchemaEntry>,
817) -> TypeCheckContext {
818    fn add_table_to_context(
819        table: &TableRef,
820        schema_map: &HashMap<String, TableSchemaEntry>,
821        context: &mut TypeCheckContext,
822    ) {
823        let resolved_key = table_ref_candidates(table)
824            .into_iter()
825            .find(|k| schema_map.contains_key(k));
826
827        let Some(table_key) = resolved_key else {
828            return;
829        };
830
831        context.referenced_tables.insert(table_key.clone());
832        context
833            .table_aliases
834            .insert(lower(&table.name.name), table_key.clone());
835        if let Some(alias) = &table.alias {
836            context
837                .table_aliases
838                .insert(lower(&alias.name), table_key.clone());
839        }
840    }
841
842    let mut context = TypeCheckContext::default();
843    let cte_aliases: HashSet<_> = build_scope(stmt)
844        .cte_sources
845        .keys()
846        .map(|name| lower(name))
847        .collect();
848
849    for node in walk_in_scope(stmt, false) {
850        let Expression::Table(table) = node else {
851            continue;
852        };
853
854        if cte_aliases.contains(&lower(&table.name.name)) {
855            continue;
856        }
857
858        add_table_to_context(table, schema_map, &mut context);
859    }
860
861    // Seed DML target tables explicitly because they are struct fields and may
862    // not appear as standalone Expression::Table nodes in traversal output.
863    match stmt {
864        Expression::Insert(insert) => {
865            add_table_to_context(&insert.table, schema_map, &mut context);
866        }
867        Expression::Update(update) => {
868            add_table_to_context(&update.table, schema_map, &mut context);
869            for table in &update.extra_tables {
870                add_table_to_context(table, schema_map, &mut context);
871            }
872        }
873        Expression::Delete(delete) => {
874            add_table_to_context(&delete.table, schema_map, &mut context);
875            for table in &delete.using {
876                add_table_to_context(table, schema_map, &mut context);
877            }
878            for table in &delete.tables {
879                add_table_to_context(table, schema_map, &mut context);
880            }
881        }
882        _ => {}
883    }
884
885    context
886}
887
888fn resolve_table_schema_entry<'a>(
889    table: &TableRef,
890    schema_map: &'a HashMap<String, TableSchemaEntry>,
891) -> Option<(String, &'a TableSchemaEntry)> {
892    let key = table_ref_candidates(table)
893        .into_iter()
894        .find(|k| schema_map.contains_key(k))?;
895    let entry = schema_map.get(&key)?;
896    Some((key, entry))
897}
898
899fn reference_issue(strict: bool, message: impl Into<String>) -> ValidationError {
900    if strict {
901        ValidationError::error(
902            message.into(),
903            validation_codes::E_INVALID_FOREIGN_KEY_REFERENCE,
904        )
905    } else {
906        ValidationError::warning(message.into(), validation_codes::W_WEAK_REFERENCE_INTEGRITY)
907    }
908}
909
910fn reference_table_candidates(
911    table_name: &str,
912    explicit_schema: Option<&str>,
913    source_schema: Option<&str>,
914) -> Vec<String> {
915    let mut candidates = Vec::new();
916    let raw = lower(table_name);
917
918    if let Some(schema) = explicit_schema {
919        candidates.push(format!("{}.{}", lower(schema), raw));
920    }
921
922    if raw.contains('.') {
923        candidates.push(raw.clone());
924        if let Some(last) = raw.rsplit('.').next() {
925            candidates.push(last.to_string());
926        }
927    } else {
928        if let Some(schema) = source_schema {
929            candidates.push(format!("{}.{}", lower(schema), raw));
930        }
931        candidates.push(raw);
932    }
933
934    let mut dedup = HashSet::new();
935    candidates
936        .into_iter()
937        .filter(|c| dedup.insert(c.clone()))
938        .collect()
939}
940
941fn resolve_reference_table_key(
942    table_name: &str,
943    explicit_schema: Option<&str>,
944    source_schema: Option<&str>,
945    schema_map: &HashMap<String, TableSchemaEntry>,
946) -> Option<String> {
947    reference_table_candidates(table_name, explicit_schema, source_schema)
948        .into_iter()
949        .find(|candidate| schema_map.contains_key(candidate))
950}
951
952fn key_types_compatible(source: TypeFamily, target: TypeFamily) -> bool {
953    if source == TypeFamily::Unknown || target == TypeFamily::Unknown {
954        return true;
955    }
956    if source == target {
957        return true;
958    }
959    if source.is_numeric() && target.is_numeric() {
960        return true;
961    }
962    if source.is_temporal() && target.is_temporal() {
963        return true;
964    }
965    false
966}
967
968fn table_key_hints(table: &SchemaTable) -> HashSet<String> {
969    let mut hints = HashSet::new();
970    for column in &table.columns {
971        if column.primary_key || column.unique {
972            hints.insert(lower(&column.name));
973        }
974    }
975    for key_col in &table.primary_key {
976        hints.insert(lower(key_col));
977    }
978    for group in &table.unique_keys {
979        if group.len() == 1 {
980            if let Some(col) = group.first() {
981                hints.insert(lower(col));
982            }
983        }
984    }
985    hints
986}
987
988fn check_reference_integrity(
989    schema: &ValidationSchema,
990    schema_map: &HashMap<String, TableSchemaEntry>,
991    strict: bool,
992) -> Vec<ValidationError> {
993    let mut errors = Vec::new();
994
995    let mut key_hints_lookup: HashMap<String, HashSet<String>> = HashMap::new();
996    for table in &schema.tables {
997        let simple = lower(&table.name);
998        key_hints_lookup.insert(simple, table_key_hints(table));
999        if let Some(schema_name) = &table.schema {
1000            let qualified = format!("{}.{}", lower(schema_name), lower(&table.name));
1001            key_hints_lookup.insert(qualified, table_key_hints(table));
1002        }
1003    }
1004
1005    for table in &schema.tables {
1006        let source_table_display = if let Some(schema_name) = &table.schema {
1007            format!("{}.{}", schema_name, table.name)
1008        } else {
1009            table.name.clone()
1010        };
1011        let source_schema = table.schema.as_deref();
1012        let source_columns: HashMap<String, TypeFamily> = table
1013            .columns
1014            .iter()
1015            .map(|col| (lower(&col.name), canonical_type_family(&col.data_type)))
1016            .collect();
1017
1018        for source_col in &table.columns {
1019            let Some(reference) = &source_col.references else {
1020                continue;
1021            };
1022            let source_type = canonical_type_family(&source_col.data_type);
1023
1024            let Some(target_key) = resolve_reference_table_key(
1025                &reference.table,
1026                reference.schema.as_deref(),
1027                source_schema,
1028                schema_map,
1029            ) else {
1030                errors.push(reference_issue(
1031                    strict,
1032                    format!(
1033                        "Foreign key reference '{}.{}' points to unknown table '{}'",
1034                        source_table_display, source_col.name, reference.table
1035                    ),
1036                ));
1037                continue;
1038            };
1039
1040            let target_column = lower(&reference.column);
1041            let Some(target_entry) = schema_map.get(&target_key) else {
1042                errors.push(reference_issue(
1043                    strict,
1044                    format!(
1045                        "Foreign key reference '{}.{}' points to unknown table '{}'",
1046                        source_table_display, source_col.name, reference.table
1047                    ),
1048                ));
1049                continue;
1050            };
1051
1052            let Some(target_type) = target_entry.columns.get(&target_column).copied() else {
1053                errors.push(reference_issue(
1054                    strict,
1055                    format!(
1056                        "Foreign key reference '{}.{}' points to unknown column '{}.{}'",
1057                        source_table_display, source_col.name, target_key, reference.column
1058                    ),
1059                ));
1060                continue;
1061            };
1062
1063            if !key_types_compatible(source_type, target_type) {
1064                errors.push(reference_issue(
1065                    strict,
1066                    format!(
1067                        "Foreign key type mismatch for '{}.{}' -> '{}.{}': {} vs {}",
1068                        source_table_display,
1069                        source_col.name,
1070                        target_key,
1071                        reference.column,
1072                        type_family_name(source_type),
1073                        type_family_name(target_type)
1074                    ),
1075                ));
1076            }
1077
1078            if let Some(target_key_hints) = key_hints_lookup.get(&target_key) {
1079                if !target_key_hints.contains(&target_column) {
1080                    errors.push(ValidationError::warning(
1081                        format!(
1082                            "Referenced column '{}.{}' is not marked as primary/unique key",
1083                            target_key, reference.column
1084                        ),
1085                        validation_codes::W_WEAK_REFERENCE_INTEGRITY,
1086                    ));
1087                }
1088            }
1089        }
1090
1091        for foreign_key in &table.foreign_keys {
1092            if foreign_key.columns.is_empty() || foreign_key.references.columns.is_empty() {
1093                errors.push(reference_issue(
1094                    strict,
1095                    format!(
1096                        "Table-level foreign key on '{}' has empty source or target column list",
1097                        source_table_display
1098                    ),
1099                ));
1100                continue;
1101            }
1102            if foreign_key.columns.len() != foreign_key.references.columns.len() {
1103                errors.push(reference_issue(
1104                    strict,
1105                    format!(
1106                        "Table-level foreign key on '{}' has {} source columns but {} target columns",
1107                        source_table_display,
1108                        foreign_key.columns.len(),
1109                        foreign_key.references.columns.len()
1110                    ),
1111                ));
1112                continue;
1113            }
1114
1115            let Some(target_key) = resolve_reference_table_key(
1116                &foreign_key.references.table,
1117                foreign_key.references.schema.as_deref(),
1118                source_schema,
1119                schema_map,
1120            ) else {
1121                errors.push(reference_issue(
1122                    strict,
1123                    format!(
1124                        "Table-level foreign key on '{}' points to unknown table '{}'",
1125                        source_table_display, foreign_key.references.table
1126                    ),
1127                ));
1128                continue;
1129            };
1130
1131            let Some(target_entry) = schema_map.get(&target_key) else {
1132                errors.push(reference_issue(
1133                    strict,
1134                    format!(
1135                        "Table-level foreign key on '{}' points to unknown table '{}'",
1136                        source_table_display, foreign_key.references.table
1137                    ),
1138                ));
1139                continue;
1140            };
1141
1142            for (source_col, target_col) in foreign_key
1143                .columns
1144                .iter()
1145                .zip(foreign_key.references.columns.iter())
1146            {
1147                let source_col_name = lower(source_col);
1148                let target_col_name = lower(target_col);
1149
1150                let Some(source_type) = source_columns.get(&source_col_name).copied() else {
1151                    errors.push(reference_issue(
1152                        strict,
1153                        format!(
1154                            "Table-level foreign key on '{}' references unknown source column '{}'",
1155                            source_table_display, source_col
1156                        ),
1157                    ));
1158                    continue;
1159                };
1160
1161                let Some(target_type) = target_entry.columns.get(&target_col_name).copied() else {
1162                    errors.push(reference_issue(
1163                        strict,
1164                        format!(
1165                            "Table-level foreign key on '{}' references unknown target column '{}.{}'",
1166                            source_table_display, target_key, target_col
1167                        ),
1168                    ));
1169                    continue;
1170                };
1171
1172                if !key_types_compatible(source_type, target_type) {
1173                    errors.push(reference_issue(
1174                        strict,
1175                        format!(
1176                            "Table-level foreign key type mismatch '{}.{}' -> '{}.{}': {} vs {}",
1177                            source_table_display,
1178                            source_col,
1179                            target_key,
1180                            target_col,
1181                            type_family_name(source_type),
1182                            type_family_name(target_type)
1183                        ),
1184                    ));
1185                }
1186
1187                if let Some(target_key_hints) = key_hints_lookup.get(&target_key) {
1188                    if !target_key_hints.contains(&target_col_name) {
1189                        errors.push(ValidationError::warning(
1190                            format!(
1191                                "Referenced column '{}.{}' is not marked as primary/unique key",
1192                                target_key, target_col
1193                            ),
1194                            validation_codes::W_WEAK_REFERENCE_INTEGRITY,
1195                        ));
1196                    }
1197                }
1198            }
1199        }
1200    }
1201
1202    errors
1203}
1204
1205fn infer_expression_type_family(
1206    expr: &Expression,
1207    schema_map: &HashMap<String, TableSchemaEntry>,
1208    context: &TypeCheckContext,
1209) -> TypeFamily {
1210    // Scoped annotation wins, including Unknown; never rebind an annotated
1211    // reference against unrelated sources in the statement-wide catalogue.
1212    if let Some(data_type) = expr.inferred_type() {
1213        return data_type_family(data_type);
1214    }
1215    infer_expression_type_family_fallback(expr, schema_map, context)
1216}
1217
1218fn infer_expression_type_family_fallback(
1219    expr: &Expression,
1220    schema_map: &HashMap<String, TableSchemaEntry>,
1221    context: &TypeCheckContext,
1222) -> TypeFamily {
1223    match expr {
1224        Expression::Literal(literal) => match literal.as_ref() {
1225            crate::expressions::Literal::Number(value) => {
1226                if value.contains('.') || value.contains('e') || value.contains('E') {
1227                    TypeFamily::Numeric
1228                } else {
1229                    TypeFamily::Integer
1230                }
1231            }
1232            crate::expressions::Literal::HexNumber(_) => TypeFamily::Integer,
1233            crate::expressions::Literal::Date(_) => TypeFamily::Date,
1234            crate::expressions::Literal::Time(_) => TypeFamily::Time,
1235            crate::expressions::Literal::Timestamp(_)
1236            | crate::expressions::Literal::Datetime(_) => TypeFamily::Timestamp,
1237            crate::expressions::Literal::HexString(_)
1238            | crate::expressions::Literal::BitString(_)
1239            | crate::expressions::Literal::ByteString(_) => TypeFamily::Binary,
1240            _ => TypeFamily::String,
1241        },
1242        Expression::Boolean(_) => TypeFamily::Boolean,
1243        Expression::Null(_) => TypeFamily::Unknown,
1244        // The shared annotator already resolved every lexical input. Missing
1245        // annotations must not trigger a second, statement-global lookup.
1246        Expression::Column(_) => TypeFamily::Unknown,
1247        Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
1248            data_type_family(&cast.to)
1249        }
1250        Expression::Alias(alias) => {
1251            infer_expression_type_family_fallback(&alias.this, schema_map, context)
1252        }
1253        Expression::Neg(unary) => {
1254            infer_expression_type_family_fallback(&unary.this, schema_map, context)
1255        }
1256        Expression::Add(op) | Expression::Sub(op) | Expression::Mul(op) => {
1257            let left = infer_expression_type_family_fallback(&op.left, schema_map, context);
1258            let right = infer_expression_type_family_fallback(&op.right, schema_map, context);
1259            if left == TypeFamily::Unknown || right == TypeFamily::Unknown {
1260                TypeFamily::Unknown
1261            } else if left == TypeFamily::Integer && right == TypeFamily::Integer {
1262                TypeFamily::Integer
1263            } else if left.is_numeric() && right.is_numeric() {
1264                TypeFamily::Numeric
1265            } else if left.is_temporal() || right.is_temporal() {
1266                left
1267            } else {
1268                TypeFamily::Unknown
1269            }
1270        }
1271        Expression::Div(_) | Expression::Mod(_) => TypeFamily::Numeric,
1272        Expression::Concat(_) => TypeFamily::String,
1273        Expression::Eq(_)
1274        | Expression::Neq(_)
1275        | Expression::Lt(_)
1276        | Expression::Lte(_)
1277        | Expression::Gt(_)
1278        | Expression::Gte(_)
1279        | Expression::Like(_)
1280        | Expression::ILike(_)
1281        | Expression::And(_)
1282        | Expression::Or(_)
1283        | Expression::Not(_)
1284        | Expression::Between(_)
1285        | Expression::In(_)
1286        | Expression::IsNull(_)
1287        | Expression::IsTrue(_)
1288        | Expression::IsFalse(_)
1289        | Expression::Is(_) => TypeFamily::Boolean,
1290        Expression::Length(_) => TypeFamily::Integer,
1291        Expression::Upper(_)
1292        | Expression::Lower(_)
1293        | Expression::Trim(_)
1294        | Expression::LTrim(_)
1295        | Expression::RTrim(_)
1296        | Expression::Replace(_)
1297        | Expression::Substring(_)
1298        | Expression::Left(_)
1299        | Expression::Right(_)
1300        | Expression::Repeat(_)
1301        | Expression::Lpad(_)
1302        | Expression::Rpad(_)
1303        | Expression::ConcatWs(_) => TypeFamily::String,
1304        Expression::Abs(_)
1305        | Expression::Round(_)
1306        | Expression::Floor(_)
1307        | Expression::Ceil(_)
1308        | Expression::Power(_)
1309        | Expression::Sqrt(_)
1310        | Expression::Cbrt(_)
1311        | Expression::Ln(_)
1312        | Expression::Log(_)
1313        | Expression::Exp(_) => TypeFamily::Numeric,
1314        Expression::DateAdd(_) | Expression::DateSub(_) | Expression::ToDate(_) => TypeFamily::Date,
1315        Expression::ToTimestamp(_) => TypeFamily::Timestamp,
1316        Expression::DateDiff(_) | Expression::Extract(_) => TypeFamily::Integer,
1317        Expression::CurrentDate(_) => TypeFamily::Date,
1318        Expression::CurrentTime(_) => TypeFamily::Time,
1319        Expression::CurrentTimestamp(_) | Expression::CurrentTimestampLTZ(_) => {
1320            TypeFamily::Timestamp
1321        }
1322        Expression::Interval(_) => TypeFamily::Interval,
1323        _ => TypeFamily::Unknown,
1324    }
1325}
1326
1327fn are_comparable(left: TypeFamily, right: TypeFamily) -> bool {
1328    if left == TypeFamily::Unknown || right == TypeFamily::Unknown {
1329        return true;
1330    }
1331    if left == right {
1332        return true;
1333    }
1334    if left.is_numeric() && right.is_numeric() {
1335        return true;
1336    }
1337    if left.is_temporal() && right.is_temporal() {
1338        return true;
1339    }
1340    false
1341}
1342
1343fn check_function_argument(
1344    errors: &mut Vec<ValidationError>,
1345    strict: bool,
1346    function_name: &str,
1347    arg_index: usize,
1348    family: TypeFamily,
1349    expected: &str,
1350    valid: bool,
1351) {
1352    if family == TypeFamily::Unknown || valid {
1353        return;
1354    }
1355
1356    errors.push(type_issue(
1357        strict,
1358        validation_codes::E_INVALID_FUNCTION_ARGUMENT_TYPE,
1359        validation_codes::W_FUNCTION_ARGUMENT_COERCION,
1360        format!(
1361            "Function '{}' argument {} expects {}, found {}",
1362            function_name,
1363            arg_index + 1,
1364            expected,
1365            type_family_name(family)
1366        ),
1367    ));
1368}
1369
1370fn function_dispatch_name(name: &str) -> String {
1371    let upper = name
1372        .rsplit('.')
1373        .next()
1374        .unwrap_or(name)
1375        .trim()
1376        .to_uppercase();
1377    lower(canonical_typed_function_name_upper(&upper))
1378}
1379
1380fn function_base_name(name: &str) -> &str {
1381    name.rsplit('.').next().unwrap_or(name).trim()
1382}
1383
1384fn check_generic_function(
1385    function: &Function,
1386    schema_map: &HashMap<String, TableSchemaEntry>,
1387    context: &TypeCheckContext,
1388    strict: bool,
1389    errors: &mut Vec<ValidationError>,
1390) {
1391    let name = function_dispatch_name(&function.name);
1392
1393    let arg_family = |index: usize| -> Option<TypeFamily> {
1394        function
1395            .args
1396            .get(index)
1397            .map(|arg| infer_expression_type_family(arg, schema_map, context))
1398    };
1399
1400    match name.as_str() {
1401        "abs" | "sqrt" | "cbrt" | "ln" | "exp" => {
1402            if let Some(family) = arg_family(0) {
1403                check_function_argument(
1404                    errors,
1405                    strict,
1406                    &name,
1407                    0,
1408                    family,
1409                    "a numeric argument",
1410                    family.is_numeric(),
1411                );
1412            }
1413        }
1414        "round" | "floor" | "ceil" | "ceiling" => {
1415            if let Some(family) = arg_family(0) {
1416                check_function_argument(
1417                    errors,
1418                    strict,
1419                    &name,
1420                    0,
1421                    family,
1422                    "a numeric argument",
1423                    family.is_numeric(),
1424                );
1425            }
1426            if let Some(family) = arg_family(1) {
1427                check_function_argument(
1428                    errors,
1429                    strict,
1430                    &name,
1431                    1,
1432                    family,
1433                    "a numeric argument",
1434                    family.is_numeric(),
1435                );
1436            }
1437        }
1438        "power" | "pow" => {
1439            for i in [0_usize, 1_usize] {
1440                if let Some(family) = arg_family(i) {
1441                    check_function_argument(
1442                        errors,
1443                        strict,
1444                        &name,
1445                        i,
1446                        family,
1447                        "a numeric argument",
1448                        family.is_numeric(),
1449                    );
1450                }
1451            }
1452        }
1453        "length" | "char_length" | "character_length" => {
1454            if let Some(family) = arg_family(0) {
1455                check_function_argument(
1456                    errors,
1457                    strict,
1458                    &name,
1459                    0,
1460                    family,
1461                    "a string or binary argument",
1462                    is_string_or_binary(family),
1463                );
1464            }
1465        }
1466        "upper" | "lower" | "trim" | "ltrim" | "rtrim" | "reverse" => {
1467            if let Some(family) = arg_family(0) {
1468                check_function_argument(
1469                    errors,
1470                    strict,
1471                    &name,
1472                    0,
1473                    family,
1474                    "a string argument",
1475                    is_string_like(family),
1476                );
1477            }
1478        }
1479        "substring" | "substr" => {
1480            if let Some(family) = arg_family(0) {
1481                check_function_argument(
1482                    errors,
1483                    strict,
1484                    &name,
1485                    0,
1486                    family,
1487                    "a string argument",
1488                    is_string_like(family),
1489                );
1490            }
1491            if let Some(family) = arg_family(1) {
1492                check_function_argument(
1493                    errors,
1494                    strict,
1495                    &name,
1496                    1,
1497                    family,
1498                    "a numeric argument",
1499                    family.is_numeric(),
1500                );
1501            }
1502            if let Some(family) = arg_family(2) {
1503                check_function_argument(
1504                    errors,
1505                    strict,
1506                    &name,
1507                    2,
1508                    family,
1509                    "a numeric argument",
1510                    family.is_numeric(),
1511                );
1512            }
1513        }
1514        "replace" => {
1515            for i in [0_usize, 1_usize, 2_usize] {
1516                if let Some(family) = arg_family(i) {
1517                    check_function_argument(
1518                        errors,
1519                        strict,
1520                        &name,
1521                        i,
1522                        family,
1523                        "a string argument",
1524                        is_string_like(family),
1525                    );
1526                }
1527            }
1528        }
1529        "left" | "right" | "repeat" | "lpad" | "rpad" => {
1530            if let Some(family) = arg_family(0) {
1531                check_function_argument(
1532                    errors,
1533                    strict,
1534                    &name,
1535                    0,
1536                    family,
1537                    "a string argument",
1538                    is_string_like(family),
1539                );
1540            }
1541            if let Some(family) = arg_family(1) {
1542                check_function_argument(
1543                    errors,
1544                    strict,
1545                    &name,
1546                    1,
1547                    family,
1548                    "a numeric argument",
1549                    family.is_numeric(),
1550                );
1551            }
1552            if (name == "lpad" || name == "rpad") && function.args.len() > 2 {
1553                if let Some(family) = arg_family(2) {
1554                    check_function_argument(
1555                        errors,
1556                        strict,
1557                        &name,
1558                        2,
1559                        family,
1560                        "a string argument",
1561                        is_string_like(family),
1562                    );
1563                }
1564            }
1565        }
1566        _ => {}
1567    }
1568}
1569
1570fn check_function_catalog(
1571    function: &Function,
1572    dialect: DialectType,
1573    function_catalog: Option<&dyn FunctionCatalog>,
1574    strict: bool,
1575    errors: &mut Vec<ValidationError>,
1576) {
1577    let Some(catalog) = function_catalog else {
1578        return;
1579    };
1580
1581    let raw_name = function_base_name(&function.name);
1582    let normalized_name = function_dispatch_name(&function.name);
1583    let arity = function.args.len();
1584    let Some(signatures) = catalog.lookup(dialect, raw_name, &normalized_name) else {
1585        errors.push(if strict {
1586            ValidationError::error(
1587                format!(
1588                    "Unknown function '{}' for dialect {:?}",
1589                    function.name, dialect
1590                ),
1591                validation_codes::E_UNKNOWN_FUNCTION,
1592            )
1593        } else {
1594            ValidationError::warning(
1595                format!(
1596                    "Unknown function '{}' for dialect {:?}",
1597                    function.name, dialect
1598                ),
1599                validation_codes::E_UNKNOWN_FUNCTION,
1600            )
1601        });
1602        return;
1603    };
1604
1605    if signatures.iter().any(|sig| sig.matches_arity(arity)) {
1606        return;
1607    }
1608
1609    let expected = signatures
1610        .iter()
1611        .map(|sig| sig.describe_arity())
1612        .collect::<Vec<_>>()
1613        .join(", ");
1614    errors.push(if strict {
1615        ValidationError::error(
1616            format!(
1617                "Invalid arity for function '{}': got {}, expected {}",
1618                function.name, arity, expected
1619            ),
1620            validation_codes::E_INVALID_FUNCTION_ARITY,
1621        )
1622    } else {
1623        ValidationError::warning(
1624            format!(
1625                "Invalid arity for function '{}': got {}, expected {}",
1626                function.name, arity, expected
1627            ),
1628            validation_codes::E_INVALID_FUNCTION_ARITY,
1629        )
1630    });
1631}
1632
1633#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1634struct DeclaredRelationship {
1635    source_table: String,
1636    source_column: String,
1637    target_table: String,
1638    target_column: String,
1639}
1640
1641fn build_declared_relationships(
1642    schema: &ValidationSchema,
1643    schema_map: &HashMap<String, TableSchemaEntry>,
1644) -> Vec<DeclaredRelationship> {
1645    let mut relationships = HashSet::new();
1646
1647    for table in &schema.tables {
1648        let Some(source_key) =
1649            resolve_reference_table_key(&table.name, table.schema.as_deref(), None, schema_map)
1650        else {
1651            continue;
1652        };
1653
1654        for column in &table.columns {
1655            let Some(reference) = &column.references else {
1656                continue;
1657            };
1658            let Some(target_key) = resolve_reference_table_key(
1659                &reference.table,
1660                reference.schema.as_deref(),
1661                table.schema.as_deref(),
1662                schema_map,
1663            ) else {
1664                continue;
1665            };
1666
1667            relationships.insert(DeclaredRelationship {
1668                source_table: source_key.clone(),
1669                source_column: lower(&column.name),
1670                target_table: target_key,
1671                target_column: lower(&reference.column),
1672            });
1673        }
1674
1675        for foreign_key in &table.foreign_keys {
1676            if foreign_key.columns.len() != foreign_key.references.columns.len() {
1677                continue;
1678            }
1679            let Some(target_key) = resolve_reference_table_key(
1680                &foreign_key.references.table,
1681                foreign_key.references.schema.as_deref(),
1682                table.schema.as_deref(),
1683                schema_map,
1684            ) else {
1685                continue;
1686            };
1687
1688            for (source_col, target_col) in foreign_key
1689                .columns
1690                .iter()
1691                .zip(foreign_key.references.columns.iter())
1692            {
1693                relationships.insert(DeclaredRelationship {
1694                    source_table: source_key.clone(),
1695                    source_column: lower(source_col),
1696                    target_table: target_key.clone(),
1697                    target_column: lower(target_col),
1698                });
1699            }
1700        }
1701    }
1702
1703    relationships.into_iter().collect()
1704}
1705
1706fn resolve_column_binding(
1707    column: &Column,
1708    schema_map: &HashMap<String, TableSchemaEntry>,
1709    context: &TypeCheckContext,
1710    resolver: &mut Resolver<'_>,
1711) -> Option<(String, String)> {
1712    let column_name = lower(&column.name.name);
1713    if column_name.is_empty() {
1714        return None;
1715    }
1716
1717    if let Some(table) = &column.table {
1718        let mut table_key = lower(&table.name);
1719        if let Some(mapped) = context.table_aliases.get(&table_key) {
1720            table_key = mapped.clone();
1721        }
1722        if schema_map.contains_key(&table_key) {
1723            return Some((table_key, column_name));
1724        }
1725        return None;
1726    }
1727
1728    if let Some(resolved_source) = resolver.get_table(&column_name) {
1729        let mut table_key = lower(&resolved_source);
1730        if let Some(mapped) = context.table_aliases.get(&table_key) {
1731            table_key = mapped.clone();
1732        }
1733        if schema_map.contains_key(&table_key) {
1734            return Some((table_key, column_name));
1735        }
1736    }
1737
1738    let candidates: Vec<String> = context
1739        .referenced_tables
1740        .iter()
1741        .filter_map(|table_name| {
1742            schema_map
1743                .get(table_name)
1744                .filter(|entry| entry.columns.contains_key(&column_name))
1745                .map(|_| table_name.clone())
1746        })
1747        .collect();
1748    if candidates.len() == 1 {
1749        return Some((candidates[0].clone(), column_name));
1750    }
1751    None
1752}
1753
1754fn extract_join_equality_pairs(
1755    expr: &Expression,
1756    schema_map: &HashMap<String, TableSchemaEntry>,
1757    context: &TypeCheckContext,
1758    resolver: &mut Resolver<'_>,
1759    pairs: &mut Vec<((String, String), (String, String))>,
1760) {
1761    match expr {
1762        Expression::And(op) => {
1763            extract_join_equality_pairs(&op.left, schema_map, context, resolver, pairs);
1764            extract_join_equality_pairs(&op.right, schema_map, context, resolver, pairs);
1765        }
1766        Expression::Paren(paren) => {
1767            extract_join_equality_pairs(&paren.this, schema_map, context, resolver, pairs);
1768        }
1769        Expression::Eq(op) => {
1770            let (Expression::Column(left_col), Expression::Column(right_col)) =
1771                (&op.left, &op.right)
1772            else {
1773                return;
1774            };
1775            let Some(left) = resolve_column_binding(left_col, schema_map, context, resolver) else {
1776                return;
1777            };
1778            let Some(right) = resolve_column_binding(right_col, schema_map, context, resolver)
1779            else {
1780                return;
1781            };
1782            pairs.push((left, right));
1783        }
1784        _ => {}
1785    }
1786}
1787
1788fn relationship_matches_pair(
1789    relationship: &DeclaredRelationship,
1790    left_table: &str,
1791    left_column: &str,
1792    right_table: &str,
1793    right_column: &str,
1794) -> bool {
1795    (relationship.source_table == left_table
1796        && relationship.source_column == left_column
1797        && relationship.target_table == right_table
1798        && relationship.target_column == right_column)
1799        || (relationship.source_table == right_table
1800            && relationship.source_column == right_column
1801            && relationship.target_table == left_table
1802            && relationship.target_column == left_column)
1803}
1804
1805fn resolved_table_key_from_expr(
1806    expr: &Expression,
1807    schema_map: &HashMap<String, TableSchemaEntry>,
1808) -> Option<String> {
1809    match expr {
1810        Expression::Table(table) => resolve_table_schema_entry(table, schema_map).map(|(k, _)| k),
1811        Expression::Alias(alias) => resolved_table_key_from_expr(&alias.this, schema_map),
1812        _ => None,
1813    }
1814}
1815
1816fn select_from_table_keys(
1817    select: &crate::expressions::Select,
1818    schema_map: &HashMap<String, TableSchemaEntry>,
1819) -> HashSet<String> {
1820    let mut keys = HashSet::new();
1821    if let Some(from_clause) = &select.from {
1822        for expr in &from_clause.expressions {
1823            if let Some(key) = resolved_table_key_from_expr(expr, schema_map) {
1824                keys.insert(key);
1825            }
1826        }
1827    }
1828    keys
1829}
1830
1831fn is_natural_or_implied_join(kind: JoinKind) -> bool {
1832    matches!(
1833        kind,
1834        JoinKind::Natural
1835            | JoinKind::NaturalLeft
1836            | JoinKind::NaturalRight
1837            | JoinKind::NaturalFull
1838            | JoinKind::CrossApply
1839            | JoinKind::OuterApply
1840            | JoinKind::AsOf
1841            | JoinKind::AsOfLeft
1842            | JoinKind::AsOfRight
1843            | JoinKind::Lateral
1844            | JoinKind::LeftLateral
1845    )
1846}
1847
1848fn check_query_reference_quality(
1849    stmt: &Expression,
1850    schema_map: &HashMap<String, TableSchemaEntry>,
1851    resolver_schema: &MappingSchema,
1852    relationships: &[DeclaredRelationship],
1853) -> Vec<ValidationError> {
1854    let mut errors = Vec::new();
1855
1856    for node in stmt.dfs() {
1857        let Expression::Select(select) = node else {
1858            continue;
1859        };
1860
1861        let select_expr = Expression::Select(select.clone());
1862        let context = collect_type_check_context(&select_expr, schema_map);
1863        let scope = build_scope(&select_expr);
1864        let mut resolver = Resolver::new(&scope, resolver_schema, true);
1865
1866        let mut cumulative_left_tables = select_from_table_keys(select, schema_map);
1867
1868        for join in &select.joins {
1869            let right_table_key = resolved_table_key_from_expr(&join.this, schema_map);
1870            let has_explicit_condition = join.on.is_some() || !join.using.is_empty();
1871            let cartesian_like_kind = matches!(
1872                join.kind,
1873                JoinKind::Cross
1874                    | JoinKind::Implicit
1875                    | JoinKind::Array
1876                    | JoinKind::LeftArray
1877                    | JoinKind::Paste
1878            );
1879
1880            if right_table_key.is_some()
1881                && (cartesian_like_kind
1882                    || (!has_explicit_condition && !is_natural_or_implied_join(join.kind)))
1883            {
1884                errors.push(ValidationError::warning(
1885                    "Potential cartesian join: JOIN without ON/USING condition",
1886                    validation_codes::W_CARTESIAN_JOIN,
1887                ));
1888            }
1889
1890            if let (Some(on_expr), Some(right_key)) = (&join.on, right_table_key.clone()) {
1891                if join.using.is_empty() {
1892                    let mut eq_pairs = Vec::new();
1893                    extract_join_equality_pairs(
1894                        on_expr,
1895                        schema_map,
1896                        &context,
1897                        &mut resolver,
1898                        &mut eq_pairs,
1899                    );
1900
1901                    let relevant_relationships: Vec<&DeclaredRelationship> = relationships
1902                        .iter()
1903                        .filter(|rel| {
1904                            cumulative_left_tables.contains(&rel.source_table)
1905                                && rel.target_table == right_key
1906                                || (cumulative_left_tables.contains(&rel.target_table)
1907                                    && rel.source_table == right_key)
1908                        })
1909                        .collect();
1910
1911                    if !relevant_relationships.is_empty() {
1912                        let uses_declared_fk = eq_pairs.iter().any(|((lt, lc), (rt, rc))| {
1913                            relevant_relationships
1914                                .iter()
1915                                .any(|rel| relationship_matches_pair(rel, lt, lc, rt, rc))
1916                        });
1917                        if !uses_declared_fk {
1918                            errors.push(ValidationError::warning(
1919                                "JOIN predicate does not use declared foreign-key relationship columns",
1920                                validation_codes::W_JOIN_NOT_USING_DECLARED_REFERENCE,
1921                            ));
1922                        }
1923                    }
1924                }
1925            }
1926
1927            if let Some(right_key) = right_table_key {
1928                cumulative_left_tables.insert(right_key);
1929            }
1930        }
1931    }
1932
1933    errors
1934}
1935
1936fn are_setop_compatible(left: TypeFamily, right: TypeFamily) -> bool {
1937    if left == TypeFamily::Unknown || right == TypeFamily::Unknown {
1938        return true;
1939    }
1940    if left == right {
1941        return true;
1942    }
1943    if left.is_numeric() && right.is_numeric() {
1944        return true;
1945    }
1946    if left.is_temporal() && right.is_temporal() {
1947        return true;
1948    }
1949    false
1950}
1951
1952fn merged_setop_family(left: TypeFamily, right: TypeFamily) -> TypeFamily {
1953    if left == TypeFamily::Unknown {
1954        return right;
1955    }
1956    if right == TypeFamily::Unknown {
1957        return left;
1958    }
1959    if left == right {
1960        return left;
1961    }
1962    if left.is_numeric() && right.is_numeric() {
1963        if left == TypeFamily::Numeric || right == TypeFamily::Numeric {
1964            return TypeFamily::Numeric;
1965        }
1966        return TypeFamily::Integer;
1967    }
1968    if left.is_temporal() && right.is_temporal() {
1969        if left == TypeFamily::Timestamp || right == TypeFamily::Timestamp {
1970            return TypeFamily::Timestamp;
1971        }
1972        if left == TypeFamily::Date || right == TypeFamily::Date {
1973            return TypeFamily::Date;
1974        }
1975        return TypeFamily::Time;
1976    }
1977    TypeFamily::Unknown
1978}
1979
1980fn are_assignment_compatible(target: TypeFamily, source: TypeFamily) -> bool {
1981    if target == TypeFamily::Unknown || source == TypeFamily::Unknown {
1982        return true;
1983    }
1984    if target == source {
1985        return true;
1986    }
1987
1988    match target {
1989        TypeFamily::Boolean => source == TypeFamily::Boolean,
1990        TypeFamily::Integer | TypeFamily::Numeric => source.is_numeric(),
1991        TypeFamily::Date | TypeFamily::Time | TypeFamily::Timestamp | TypeFamily::Interval => {
1992            source.is_temporal()
1993        }
1994        TypeFamily::String => true,
1995        TypeFamily::Binary => matches!(source, TypeFamily::Binary | TypeFamily::String),
1996        TypeFamily::Json => matches!(source, TypeFamily::Json | TypeFamily::String),
1997        TypeFamily::Uuid => matches!(source, TypeFamily::Uuid | TypeFamily::String),
1998        TypeFamily::Array => source == TypeFamily::Array,
1999        TypeFamily::Map => source == TypeFamily::Map,
2000        TypeFamily::Struct => source == TypeFamily::Struct,
2001        TypeFamily::Unknown => true,
2002    }
2003}
2004
2005fn projection_families(
2006    query: &Expression,
2007    schema_map: &HashMap<String, TableSchemaEntry>,
2008    dialect: DialectType,
2009) -> Option<Vec<TypeFamily>> {
2010    let pair = match query {
2011        Expression::Union(query) => Some((&query.left, &query.right)),
2012        Expression::Intersect(query) => Some((&query.left, &query.right)),
2013        Expression::Except(query) => Some((&query.left, &query.right)),
2014        _ => None,
2015    };
2016    if let Some((left, right)) = pair {
2017        let left = projection_families(left, schema_map, dialect)?;
2018        let right = projection_families(right, schema_map, dialect)?;
2019        return match crate::set_operation::set_operation_layout(query, Some(dialect)).ok()? {
2020            Some(layout) => Some(
2021                layout
2022                    .outputs
2023                    .iter()
2024                    .map(|output| {
2025                        merged_setop_family(
2026                            output
2027                                .left_ordinal
2028                                .and_then(|i| left.get(i).copied())
2029                                .unwrap_or(TypeFamily::Unknown),
2030                            output
2031                                .right_ordinal
2032                                .and_then(|i| right.get(i).copied())
2033                                .unwrap_or(TypeFamily::Unknown),
2034                        )
2035                    })
2036                    .collect(),
2037            ),
2038            None if left.len() == right.len() => Some(
2039                left.into_iter()
2040                    .zip(right)
2041                    .map(|(l, r)| merged_setop_family(l, r))
2042                    .collect(),
2043            ),
2044            _ => None,
2045        };
2046    }
2047    match query {
2048        Expression::Select(select) => {
2049            if select
2050                .expressions
2051                .iter()
2052                .any(|e| matches!(e, Expression::Star(_) | Expression::BracedWildcard(_)))
2053            {
2054                return None;
2055            }
2056            let context = collect_type_check_context(query, schema_map);
2057            Some(
2058                select
2059                    .expressions
2060                    .iter()
2061                    .map(|e| infer_expression_type_family(e, schema_map, &context))
2062                    .collect(),
2063            )
2064        }
2065        Expression::Subquery(query) => projection_families(&query.this, schema_map, dialect),
2066        Expression::Paren(query) => projection_families(&query.this, schema_map, dialect),
2067        Expression::Values(values) => Some(
2068            values
2069                .expressions
2070                .first()?
2071                .expressions
2072                .iter()
2073                .map(|e| infer_expression_type_family(e, schema_map, &TypeCheckContext::default()))
2074                .collect(),
2075        ),
2076        _ => None,
2077    }
2078}
2079
2080fn check_set_operation_compatibility(
2081    query: &Expression,
2082    dialect: DialectType,
2083    op_name: &str,
2084    left_expr: &Expression,
2085    right_expr: &Expression,
2086    schema_map: &HashMap<String, TableSchemaEntry>,
2087    strict: bool,
2088    errors: &mut Vec<ValidationError>,
2089) {
2090    let Some(mut left_projection) = projection_families(left_expr, schema_map, dialect) else {
2091        return;
2092    };
2093    let Some(mut right_projection) = projection_families(right_expr, schema_map, dialect) else {
2094        return;
2095    };
2096
2097    match crate::set_operation::set_operation_layout(query, Some(dialect)) {
2098        Ok(Some(layout)) => {
2099            let left = left_projection;
2100            let right = right_projection;
2101            left_projection = layout
2102                .outputs
2103                .iter()
2104                .map(|output| {
2105                    output
2106                        .left_ordinal
2107                        .and_then(|i| left.get(i).copied())
2108                        .unwrap_or(TypeFamily::Unknown)
2109                })
2110                .collect();
2111            right_projection = layout
2112                .outputs
2113                .iter()
2114                .map(|output| {
2115                    output
2116                        .right_ordinal
2117                        .and_then(|i| right.get(i).copied())
2118                        .unwrap_or(TypeFamily::Unknown)
2119                })
2120                .collect();
2121        }
2122        Err(error) => {
2123            if !error.is_indeterminate() {
2124                errors.push(type_issue(
2125                    strict,
2126                    validation_codes::E_SETOP_ARITY_MISMATCH,
2127                    validation_codes::W_SETOP_IMPLICIT_COERCION,
2128                    error.to_string(),
2129                ));
2130            }
2131            return;
2132        }
2133        Ok(None) => {}
2134    }
2135    if left_projection.len() != right_projection.len() {
2136        errors.push(type_issue(
2137            strict,
2138            validation_codes::E_SETOP_ARITY_MISMATCH,
2139            validation_codes::W_SETOP_IMPLICIT_COERCION,
2140            format!(
2141                "{} operands return different column counts: left {}, right {}",
2142                op_name,
2143                left_projection.len(),
2144                right_projection.len()
2145            ),
2146        ));
2147        return;
2148    }
2149
2150    for (idx, (left, right)) in left_projection
2151        .into_iter()
2152        .zip(right_projection)
2153        .enumerate()
2154    {
2155        if !are_setop_compatible(left, right) {
2156            errors.push(type_issue(
2157                strict,
2158                validation_codes::E_SETOP_TYPE_MISMATCH,
2159                validation_codes::W_SETOP_IMPLICIT_COERCION,
2160                format!(
2161                    "{} column {} has incompatible types: {} vs {}",
2162                    op_name,
2163                    idx + 1,
2164                    type_family_name(left),
2165                    type_family_name(right)
2166                ),
2167            ));
2168        }
2169    }
2170}
2171
2172fn check_insert_assignments(
2173    dialect: DialectType,
2174    stmt: &Expression,
2175    insert: &Insert,
2176    schema_map: &HashMap<String, TableSchemaEntry>,
2177    strict: bool,
2178    errors: &mut Vec<ValidationError>,
2179) {
2180    let Some((target_table_key, table_schema)) =
2181        resolve_table_schema_entry(&insert.table, schema_map)
2182    else {
2183        return;
2184    };
2185
2186    let mut target_columns = Vec::new();
2187    if insert.columns.is_empty() {
2188        target_columns.extend(table_schema.column_order.iter().cloned());
2189    } else {
2190        for column in &insert.columns {
2191            let col_name = lower(&column.name);
2192            if table_schema.columns.contains_key(&col_name) {
2193                target_columns.push(col_name);
2194            } else {
2195                errors.push(if strict {
2196                    ValidationError::error(
2197                        format!(
2198                            "Unknown column '{}' in table '{}'",
2199                            column.name, target_table_key
2200                        ),
2201                        validation_codes::E_UNKNOWN_COLUMN,
2202                    )
2203                } else {
2204                    ValidationError::warning(
2205                        format!(
2206                            "Unknown column '{}' in table '{}'",
2207                            column.name, target_table_key
2208                        ),
2209                        validation_codes::E_UNKNOWN_COLUMN,
2210                    )
2211                });
2212            }
2213        }
2214    }
2215
2216    if target_columns.is_empty() {
2217        return;
2218    }
2219
2220    let context = collect_type_check_context(stmt, schema_map);
2221
2222    if !insert.default_values {
2223        for (row_idx, row) in insert.values.iter().enumerate() {
2224            if row.len() != target_columns.len() {
2225                errors.push(type_issue(
2226                    strict,
2227                    validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2228                    validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2229                    format!(
2230                        "INSERT row {} has {} values but target has {} columns",
2231                        row_idx + 1,
2232                        row.len(),
2233                        target_columns.len()
2234                    ),
2235                ));
2236                continue;
2237            }
2238
2239            for (value, target_column) in row.iter().zip(target_columns.iter()) {
2240                let Some(target_family) = table_schema.columns.get(target_column).copied() else {
2241                    continue;
2242                };
2243                let source_family = infer_expression_type_family(value, schema_map, &context);
2244                if !are_assignment_compatible(target_family, source_family) {
2245                    errors.push(type_issue(
2246                        strict,
2247                        validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2248                        validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2249                        format!(
2250                            "INSERT assignment type mismatch for '{}.{}': expected {}, found {}",
2251                            target_table_key,
2252                            target_column,
2253                            type_family_name(target_family),
2254                            type_family_name(source_family)
2255                        ),
2256                    ));
2257                }
2258            }
2259        }
2260    }
2261
2262    if let Some(query) = &insert.query {
2263        // DuckDB BY NAME maps source columns by name, not position.
2264        if insert.by_name {
2265            return;
2266        }
2267
2268        let Some(source_projection) = projection_families(query, schema_map, dialect) else {
2269            return;
2270        };
2271
2272        if source_projection.len() != target_columns.len() {
2273            errors.push(type_issue(
2274                strict,
2275                validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2276                validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2277                format!(
2278                    "INSERT source query has {} columns but target has {} columns",
2279                    source_projection.len(),
2280                    target_columns.len()
2281                ),
2282            ));
2283            return;
2284        }
2285
2286        for (source_family, target_column) in
2287            source_projection.into_iter().zip(target_columns.iter())
2288        {
2289            let Some(target_family) = table_schema.columns.get(target_column).copied() else {
2290                continue;
2291            };
2292            if !are_assignment_compatible(target_family, source_family) {
2293                errors.push(type_issue(
2294                    strict,
2295                    validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2296                    validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2297                    format!(
2298                        "INSERT assignment type mismatch for '{}.{}': expected {}, found {}",
2299                        target_table_key,
2300                        target_column,
2301                        type_family_name(target_family),
2302                        type_family_name(source_family)
2303                    ),
2304                ));
2305            }
2306        }
2307    }
2308}
2309
2310fn check_update_assignments(
2311    stmt: &Expression,
2312    update: &Update,
2313    schema_map: &HashMap<String, TableSchemaEntry>,
2314    strict: bool,
2315    errors: &mut Vec<ValidationError>,
2316) {
2317    let Some((target_table_key, table_schema)) =
2318        resolve_table_schema_entry(&update.table, schema_map)
2319    else {
2320        return;
2321    };
2322
2323    let context = collect_type_check_context(stmt, schema_map);
2324
2325    for (column, value) in &update.set {
2326        let col_name = lower(&column.name);
2327        let Some(target_family) = table_schema.columns.get(&col_name).copied() else {
2328            errors.push(if strict {
2329                ValidationError::error(
2330                    format!(
2331                        "Unknown column '{}' in table '{}'",
2332                        column.name, target_table_key
2333                    ),
2334                    validation_codes::E_UNKNOWN_COLUMN,
2335                )
2336            } else {
2337                ValidationError::warning(
2338                    format!(
2339                        "Unknown column '{}' in table '{}'",
2340                        column.name, target_table_key
2341                    ),
2342                    validation_codes::E_UNKNOWN_COLUMN,
2343                )
2344            });
2345            continue;
2346        };
2347
2348        let source_family = infer_expression_type_family(value, schema_map, &context);
2349        if !are_assignment_compatible(target_family, source_family) {
2350            errors.push(type_issue(
2351                strict,
2352                validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2353                validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2354                format!(
2355                    "UPDATE assignment type mismatch for '{}.{}': expected {}, found {}",
2356                    target_table_key,
2357                    col_name,
2358                    type_family_name(target_family),
2359                    type_family_name(source_family)
2360                ),
2361            ));
2362        }
2363    }
2364}
2365
2366fn check_types(
2367    stmt: &Expression,
2368    dialect: DialectType,
2369    schema_map: &HashMap<String, TableSchemaEntry>,
2370    function_catalog: Option<&dyn FunctionCatalog>,
2371    strict: bool,
2372) -> Vec<ValidationError> {
2373    let mut errors = Vec::new();
2374    let context = collect_type_check_context(stmt, schema_map);
2375
2376    for node in stmt.dfs() {
2377        for (clause, predicate) in crate::binding::predicates(node) {
2378            let family = infer_expression_type_family(predicate, schema_map, &context);
2379            if !predicate_compatible(family, dialect) {
2380                errors.push(type_issue(
2381                    strict,
2382                    validation_codes::E_INVALID_PREDICATE_TYPE,
2383                    validation_codes::W_PREDICATE_NULLABILITY,
2384                    format!(
2385                        "{clause} expects a boolean predicate, found {}",
2386                        type_family_name(family)
2387                    ),
2388                ));
2389            }
2390        }
2391        match node {
2392            Expression::Insert(insert) => {
2393                check_insert_assignments(dialect, stmt, insert, schema_map, strict, &mut errors);
2394            }
2395            Expression::Update(update) => {
2396                check_update_assignments(stmt, update, schema_map, strict, &mut errors);
2397            }
2398            Expression::Union(union) => {
2399                check_set_operation_compatibility(
2400                    node,
2401                    dialect,
2402                    "UNION",
2403                    &union.left,
2404                    &union.right,
2405                    schema_map,
2406                    strict,
2407                    &mut errors,
2408                );
2409            }
2410            Expression::Intersect(intersect) => {
2411                check_set_operation_compatibility(
2412                    node,
2413                    dialect,
2414                    "INTERSECT",
2415                    &intersect.left,
2416                    &intersect.right,
2417                    schema_map,
2418                    strict,
2419                    &mut errors,
2420                );
2421            }
2422            Expression::Except(except) => {
2423                check_set_operation_compatibility(
2424                    node,
2425                    dialect,
2426                    "EXCEPT",
2427                    &except.left,
2428                    &except.right,
2429                    schema_map,
2430                    strict,
2431                    &mut errors,
2432                );
2433            }
2434            Expression::And(op) | Expression::Or(op) => {
2435                for (side, expr) in [("left", &op.left), ("right", &op.right)] {
2436                    let family = infer_expression_type_family(expr, schema_map, &context);
2437                    if !predicate_compatible(family, dialect) {
2438                        errors.push(type_issue(
2439                            strict,
2440                            validation_codes::E_INVALID_PREDICATE_TYPE,
2441                            validation_codes::W_PREDICATE_NULLABILITY,
2442                            format!(
2443                                "Logical {} operand expects boolean, found {}",
2444                                side,
2445                                type_family_name(family)
2446                            ),
2447                        ));
2448                    }
2449                }
2450            }
2451            Expression::Not(unary) => {
2452                let family = infer_expression_type_family(&unary.this, schema_map, &context);
2453                if !predicate_compatible(family, dialect) {
2454                    errors.push(type_issue(
2455                        strict,
2456                        validation_codes::E_INVALID_PREDICATE_TYPE,
2457                        validation_codes::W_PREDICATE_NULLABILITY,
2458                        format!("NOT expects boolean, found {}", type_family_name(family)),
2459                    ));
2460                }
2461            }
2462            Expression::Eq(op)
2463            | Expression::Neq(op)
2464            | Expression::Lt(op)
2465            | Expression::Lte(op)
2466            | Expression::Gt(op)
2467            | Expression::Gte(op) => {
2468                let left = infer_expression_type_family(&op.left, schema_map, &context);
2469                let right = infer_expression_type_family(&op.right, schema_map, &context);
2470                if !are_comparable(left, right) {
2471                    errors.push(type_issue(
2472                        strict,
2473                        validation_codes::E_INCOMPATIBLE_COMPARISON_TYPES,
2474                        validation_codes::W_IMPLICIT_CAST_COMPARISON,
2475                        format!(
2476                            "Incompatible comparison between {} and {}",
2477                            type_family_name(left),
2478                            type_family_name(right)
2479                        ),
2480                    ));
2481                }
2482            }
2483            Expression::Like(op) | Expression::ILike(op) => {
2484                let left = infer_expression_type_family(&op.left, schema_map, &context);
2485                let right = infer_expression_type_family(&op.right, schema_map, &context);
2486                if left != TypeFamily::Unknown
2487                    && right != TypeFamily::Unknown
2488                    && (!is_string_like(left) || !is_string_like(right))
2489                {
2490                    errors.push(type_issue(
2491                        strict,
2492                        validation_codes::E_INCOMPATIBLE_COMPARISON_TYPES,
2493                        validation_codes::W_IMPLICIT_CAST_COMPARISON,
2494                        format!(
2495                            "LIKE/ILIKE expects string operands, found {} and {}",
2496                            type_family_name(left),
2497                            type_family_name(right)
2498                        ),
2499                    ));
2500                }
2501            }
2502            Expression::Between(between) => {
2503                let this_family = infer_expression_type_family(&between.this, schema_map, &context);
2504                let low_family = infer_expression_type_family(&between.low, schema_map, &context);
2505                let high_family = infer_expression_type_family(&between.high, schema_map, &context);
2506
2507                if !are_comparable(this_family, low_family)
2508                    || !are_comparable(this_family, high_family)
2509                {
2510                    errors.push(type_issue(
2511                        strict,
2512                        validation_codes::E_INCOMPATIBLE_COMPARISON_TYPES,
2513                        validation_codes::W_IMPLICIT_CAST_COMPARISON,
2514                        format!(
2515                            "BETWEEN bounds are incompatible with {} (found {} and {})",
2516                            type_family_name(this_family),
2517                            type_family_name(low_family),
2518                            type_family_name(high_family)
2519                        ),
2520                    ));
2521                }
2522            }
2523            Expression::In(in_expr) => {
2524                let this_family = infer_expression_type_family(&in_expr.this, schema_map, &context);
2525                for value in &in_expr.expressions {
2526                    let item_family = infer_expression_type_family(value, schema_map, &context);
2527                    if !are_comparable(this_family, item_family) {
2528                        errors.push(type_issue(
2529                            strict,
2530                            validation_codes::E_INCOMPATIBLE_COMPARISON_TYPES,
2531                            validation_codes::W_IMPLICIT_CAST_COMPARISON,
2532                            format!(
2533                                "IN item type {} is incompatible with {}",
2534                                type_family_name(item_family),
2535                                type_family_name(this_family)
2536                            ),
2537                        ));
2538                        break;
2539                    }
2540                }
2541            }
2542            Expression::Add(op)
2543            | Expression::Sub(op)
2544            | Expression::Mul(op)
2545            | Expression::Div(op)
2546            | Expression::Mod(op) => {
2547                let left = infer_expression_type_family(&op.left, schema_map, &context);
2548                let right = infer_expression_type_family(&op.right, schema_map, &context);
2549
2550                if left == TypeFamily::Unknown || right == TypeFamily::Unknown {
2551                    continue;
2552                }
2553
2554                let temporal_ok = matches!(node, Expression::Add(_) | Expression::Sub(_))
2555                    && ((left.is_temporal() && right.is_numeric())
2556                        || (right.is_temporal() && left.is_numeric())
2557                        || (matches!(node, Expression::Sub(_))
2558                            && left.is_temporal()
2559                            && right.is_temporal()));
2560
2561                if !(left.is_numeric() && right.is_numeric()) && !temporal_ok {
2562                    errors.push(type_issue(
2563                        strict,
2564                        validation_codes::E_INVALID_ARITHMETIC_TYPE,
2565                        validation_codes::W_IMPLICIT_CAST_ARITHMETIC,
2566                        format!(
2567                            "Arithmetic operation expects numeric-compatible operands, found {} and {}",
2568                            type_family_name(left),
2569                            type_family_name(right)
2570                        ),
2571                    ));
2572                }
2573            }
2574            Expression::Function(function) => {
2575                check_function_catalog(function, dialect, function_catalog, strict, &mut errors);
2576                check_generic_function(function, schema_map, &context, strict, &mut errors);
2577            }
2578            Expression::Upper(func)
2579            | Expression::Lower(func)
2580            | Expression::LTrim(func)
2581            | Expression::RTrim(func)
2582            | Expression::Reverse(func) => {
2583                let family = infer_expression_type_family(&func.this, schema_map, &context);
2584                check_function_argument(
2585                    &mut errors,
2586                    strict,
2587                    "string_function",
2588                    0,
2589                    family,
2590                    "a string argument",
2591                    is_string_like(family),
2592                );
2593            }
2594            Expression::Length(func) => {
2595                let family = infer_expression_type_family(&func.this, schema_map, &context);
2596                check_function_argument(
2597                    &mut errors,
2598                    strict,
2599                    "length",
2600                    0,
2601                    family,
2602                    "a string or binary argument",
2603                    is_string_or_binary(family),
2604                );
2605            }
2606            Expression::Trim(func) => {
2607                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2608                check_function_argument(
2609                    &mut errors,
2610                    strict,
2611                    "trim",
2612                    0,
2613                    this_family,
2614                    "a string argument",
2615                    is_string_like(this_family),
2616                );
2617                if let Some(chars) = &func.characters {
2618                    let chars_family = infer_expression_type_family(chars, schema_map, &context);
2619                    check_function_argument(
2620                        &mut errors,
2621                        strict,
2622                        "trim",
2623                        1,
2624                        chars_family,
2625                        "a string argument",
2626                        is_string_like(chars_family),
2627                    );
2628                }
2629            }
2630            Expression::Substring(func) => {
2631                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2632                check_function_argument(
2633                    &mut errors,
2634                    strict,
2635                    "substring",
2636                    0,
2637                    this_family,
2638                    "a string argument",
2639                    is_string_like(this_family),
2640                );
2641
2642                let start_family = infer_expression_type_family(&func.start, schema_map, &context);
2643                check_function_argument(
2644                    &mut errors,
2645                    strict,
2646                    "substring",
2647                    1,
2648                    start_family,
2649                    "a numeric argument",
2650                    start_family.is_numeric(),
2651                );
2652                if let Some(length) = &func.length {
2653                    let length_family = infer_expression_type_family(length, schema_map, &context);
2654                    check_function_argument(
2655                        &mut errors,
2656                        strict,
2657                        "substring",
2658                        2,
2659                        length_family,
2660                        "a numeric argument",
2661                        length_family.is_numeric(),
2662                    );
2663                }
2664            }
2665            Expression::Replace(func) => {
2666                for (arg, idx) in [
2667                    (&func.this, 0_usize),
2668                    (&func.old, 1_usize),
2669                    (&func.new, 2_usize),
2670                ] {
2671                    let family = infer_expression_type_family(arg, schema_map, &context);
2672                    check_function_argument(
2673                        &mut errors,
2674                        strict,
2675                        "replace",
2676                        idx,
2677                        family,
2678                        "a string argument",
2679                        is_string_like(family),
2680                    );
2681                }
2682            }
2683            Expression::Left(func) | Expression::Right(func) => {
2684                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2685                check_function_argument(
2686                    &mut errors,
2687                    strict,
2688                    "left_right",
2689                    0,
2690                    this_family,
2691                    "a string argument",
2692                    is_string_like(this_family),
2693                );
2694                let length_family =
2695                    infer_expression_type_family(&func.length, schema_map, &context);
2696                check_function_argument(
2697                    &mut errors,
2698                    strict,
2699                    "left_right",
2700                    1,
2701                    length_family,
2702                    "a numeric argument",
2703                    length_family.is_numeric(),
2704                );
2705            }
2706            Expression::Repeat(func) => {
2707                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2708                check_function_argument(
2709                    &mut errors,
2710                    strict,
2711                    "repeat",
2712                    0,
2713                    this_family,
2714                    "a string argument",
2715                    is_string_like(this_family),
2716                );
2717                let times_family = infer_expression_type_family(&func.times, schema_map, &context);
2718                check_function_argument(
2719                    &mut errors,
2720                    strict,
2721                    "repeat",
2722                    1,
2723                    times_family,
2724                    "a numeric argument",
2725                    times_family.is_numeric(),
2726                );
2727            }
2728            Expression::Lpad(func) | Expression::Rpad(func) => {
2729                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2730                check_function_argument(
2731                    &mut errors,
2732                    strict,
2733                    "pad",
2734                    0,
2735                    this_family,
2736                    "a string argument",
2737                    is_string_like(this_family),
2738                );
2739                let length_family =
2740                    infer_expression_type_family(&func.length, schema_map, &context);
2741                check_function_argument(
2742                    &mut errors,
2743                    strict,
2744                    "pad",
2745                    1,
2746                    length_family,
2747                    "a numeric argument",
2748                    length_family.is_numeric(),
2749                );
2750                if let Some(fill) = &func.fill {
2751                    let fill_family = infer_expression_type_family(fill, schema_map, &context);
2752                    check_function_argument(
2753                        &mut errors,
2754                        strict,
2755                        "pad",
2756                        2,
2757                        fill_family,
2758                        "a string argument",
2759                        is_string_like(fill_family),
2760                    );
2761                }
2762            }
2763            Expression::Abs(func)
2764            | Expression::Sqrt(func)
2765            | Expression::Cbrt(func)
2766            | Expression::Ln(func)
2767            | Expression::Exp(func) => {
2768                let family = infer_expression_type_family(&func.this, schema_map, &context);
2769                check_function_argument(
2770                    &mut errors,
2771                    strict,
2772                    "numeric_function",
2773                    0,
2774                    family,
2775                    "a numeric argument",
2776                    family.is_numeric(),
2777                );
2778            }
2779            Expression::Round(func) => {
2780                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2781                check_function_argument(
2782                    &mut errors,
2783                    strict,
2784                    "round",
2785                    0,
2786                    this_family,
2787                    "a numeric argument",
2788                    this_family.is_numeric(),
2789                );
2790                if let Some(decimals) = &func.decimals {
2791                    let decimals_family =
2792                        infer_expression_type_family(decimals, schema_map, &context);
2793                    check_function_argument(
2794                        &mut errors,
2795                        strict,
2796                        "round",
2797                        1,
2798                        decimals_family,
2799                        "a numeric argument",
2800                        decimals_family.is_numeric(),
2801                    );
2802                }
2803            }
2804            Expression::Floor(func) => {
2805                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2806                check_function_argument(
2807                    &mut errors,
2808                    strict,
2809                    "floor",
2810                    0,
2811                    this_family,
2812                    "a numeric argument",
2813                    this_family.is_numeric(),
2814                );
2815                if let Some(scale) = &func.scale {
2816                    let scale_family = infer_expression_type_family(scale, schema_map, &context);
2817                    check_function_argument(
2818                        &mut errors,
2819                        strict,
2820                        "floor",
2821                        1,
2822                        scale_family,
2823                        "a numeric argument",
2824                        scale_family.is_numeric(),
2825                    );
2826                }
2827            }
2828            Expression::Ceil(func) => {
2829                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2830                check_function_argument(
2831                    &mut errors,
2832                    strict,
2833                    "ceil",
2834                    0,
2835                    this_family,
2836                    "a numeric argument",
2837                    this_family.is_numeric(),
2838                );
2839                if let Some(decimals) = &func.decimals {
2840                    let decimals_family =
2841                        infer_expression_type_family(decimals, schema_map, &context);
2842                    check_function_argument(
2843                        &mut errors,
2844                        strict,
2845                        "ceil",
2846                        1,
2847                        decimals_family,
2848                        "a numeric argument",
2849                        decimals_family.is_numeric(),
2850                    );
2851                }
2852            }
2853            Expression::Power(func) => {
2854                let left_family = infer_expression_type_family(&func.this, schema_map, &context);
2855                check_function_argument(
2856                    &mut errors,
2857                    strict,
2858                    "power",
2859                    0,
2860                    left_family,
2861                    "a numeric argument",
2862                    left_family.is_numeric(),
2863                );
2864                let right_family =
2865                    infer_expression_type_family(&func.expression, schema_map, &context);
2866                check_function_argument(
2867                    &mut errors,
2868                    strict,
2869                    "power",
2870                    1,
2871                    right_family,
2872                    "a numeric argument",
2873                    right_family.is_numeric(),
2874                );
2875            }
2876            Expression::Log(func) => {
2877                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2878                check_function_argument(
2879                    &mut errors,
2880                    strict,
2881                    "log",
2882                    0,
2883                    this_family,
2884                    "a numeric argument",
2885                    this_family.is_numeric(),
2886                );
2887                if let Some(base) = &func.base {
2888                    let base_family = infer_expression_type_family(base, schema_map, &context);
2889                    check_function_argument(
2890                        &mut errors,
2891                        strict,
2892                        "log",
2893                        1,
2894                        base_family,
2895                        "a numeric argument",
2896                        base_family.is_numeric(),
2897                    );
2898                }
2899            }
2900            _ => {}
2901        }
2902    }
2903
2904    errors
2905}
2906
2907fn predicate_compatible(family: TypeFamily, dialect: DialectType) -> bool {
2908    matches!(family, TypeFamily::Unknown | TypeFamily::Boolean)
2909        || (matches!(
2910            dialect,
2911            DialectType::MySQL | DialectType::SQLite | DialectType::DuckDB
2912        ) && family.is_numeric())
2913}
2914
2915mod semantics;
2916pub(crate) use semantics::check_semantics;
2917
2918fn resolve_scope_source_name(scope: &crate::scope::Scope, name: &str) -> Option<String> {
2919    scope
2920        .sources
2921        .get_key_value(name)
2922        .map(|(k, _)| k.clone())
2923        .or_else(|| {
2924            scope
2925                .sources
2926                .keys()
2927                .find(|source| source.eq_ignore_ascii_case(name))
2928                .cloned()
2929        })
2930}
2931
2932fn source_has_column(
2933    scope: &crate::scope::Scope,
2934    source: &str,
2935    columns: &[String],
2936    column: &crate::expressions::Identifier,
2937    dialect: DialectType,
2938) -> bool {
2939    let strategy = get_normalization_strategy(Some(dialect));
2940    let expected = normalize_identifier(column.clone(), strategy).name;
2941    columns.iter().any(|name| {
2942        if name == "*" {
2943            return true;
2944        }
2945        let identifier = scope
2946            .sources
2947            .get(source)
2948            .and_then(|source| source_output_identifier(&source.expression, name))
2949            .cloned()
2950            .unwrap_or_else(|| crate::binding::schema_identifier(name));
2951        normalize_identifier(identifier, strategy).name == expected
2952    })
2953}
2954
2955fn source_display_name(scope: &crate::scope::Scope, source_name: &str) -> String {
2956    scope
2957        .sources
2958        .get(source_name)
2959        .map(|source| match &source.expression {
2960            Expression::Table(table) => lower(&table_ref_display_name(table)),
2961            _ => lower(source_name),
2962        })
2963        .unwrap_or_else(|| lower(source_name))
2964}
2965
2966/// Attach original token locations without manufacturing offsets for synthetic ASTs.
2967fn reference_diagnostic(
2968    message: String,
2969    code: &str,
2970    strict: bool,
2971    span: Option<crate::tokens::Span>,
2972) -> ValidationError {
2973    let mut error = if strict {
2974        ValidationError::error(message, code)
2975    } else {
2976        ValidationError::warning(message, code)
2977    };
2978    if let Some(span) = span {
2979        error = error
2980            .with_location(span.line, span.column)
2981            .with_span(Some(span.start), Some(span.end));
2982    }
2983    error
2984}
2985
2986use crate::scope::{scope_query, selected_reference_scope as selected_validation_scope};
2987
2988/// Validation starts from SQL, so source ranges identify individual references
2989/// across the scope tree's AST copies. Never bind synthetic/unpositioned columns.
2990/// Types, not defining expressions, are stored to keep alias chains linear in size.
2991type ProjectionAliasBindings = HashMap<(usize, usize), DataType>;
2992
2993fn projection_reference_key(column: &Column) -> Option<(usize, usize)> {
2994    if column.table.is_some() {
2995        return None;
2996    }
2997    column
2998        .name
2999        .span
3000        .or(column.span)
3001        .map(|span| (span.start, span.end))
3002}
3003
3004use crate::binding::bound_identifier as validation_bound_identifier;
3005
3006/// Apply already resolved bindings to the private validation AST. The canonical
3007/// visitor keeps this stack-safe and covers references inside typed functions.
3008fn apply_projection_alias_bindings(
3009    expression: Expression,
3010    bindings: &ProjectionAliasBindings,
3011) -> Expression {
3012    if bindings.is_empty() {
3013        return expression;
3014    }
3015    enum Task {
3016        Visit(Expression),
3017        Finish(Expression, usize),
3018    }
3019    let mut pending = vec![Task::Visit(expression)];
3020    let mut results = Vec::new();
3021    while let Some(task) = pending.pop() {
3022        match task {
3023            Task::Visit(mut expression) => {
3024                if let Expression::Column(column) = &expression {
3025                    if let Some(data_type) =
3026                        projection_reference_key(column).and_then(|key| bindings.get(&key))
3027                    {
3028                        results.push(validation_bound_identifier(column.name.clone(), data_type));
3029                        continue;
3030                    }
3031                }
3032                let mut children = Vec::new();
3033                crate::ast_children::for_each_child_mut(&mut expression, |child| {
3034                    children.push(std::mem::replace(
3035                        child,
3036                        Expression::Null(crate::expressions::Null),
3037                    ));
3038                });
3039                pending.push(Task::Finish(expression, children.len()));
3040                pending.extend(children.into_iter().rev().map(Task::Visit));
3041            }
3042            Task::Finish(mut expression, count) => {
3043                let mut children = results.split_off(results.len() - count).into_iter();
3044                crate::ast_children::for_each_child_mut(&mut expression, |child| {
3045                    *child = children.next().expect("alias binding child");
3046                });
3047                results.push(expression);
3048            }
3049        }
3050    }
3051    results.pop().expect("alias binding result")
3052}
3053
3054fn bind_scope_projection_aliases(
3055    scope: &mut crate::scope::Scope,
3056    ancestors: &[&crate::scope::Scope],
3057    resolver_schema: &MappingSchema,
3058    dialect: DialectType,
3059    check_types: bool,
3060    bindings: &mut ProjectionAliasBindings,
3061) {
3062    let Expression::Select(select) = &scope.expression else {
3063        return;
3064    };
3065    if !select
3066        .expressions
3067        .iter()
3068        .any(|expression| matches!(expression, Expression::Alias(_)))
3069    {
3070        return;
3071    }
3072
3073    // A real input wins over a lateral alias. Ambiguous inputs must also remain
3074    // columns so the ordinary reference checker can diagnose them. Open sources
3075    // do not provide enough information to safely choose an alias instead.
3076    let strategy = get_normalization_strategy(Some(dialect));
3077    let mut inputs = HashSet::new();
3078    let mut open = false;
3079    for source_scope in std::iter::once(&*scope).chain(ancestors.iter().copied()) {
3080        let mut resolver = Resolver::new(source_scope, resolver_schema, true);
3081        for source in source_scope.sources.keys() {
3082            let columns = resolver.get_source_columns(source).unwrap_or_default();
3083            open |= columns.is_empty() || columns.iter().any(|name| name == "*");
3084            let expression = &source_scope.sources[source].expression;
3085            for name in columns {
3086                let identifier = source_output_identifier(expression, &name)
3087                    .cloned()
3088                    .unwrap_or_else(|| crate::expressions::Identifier::new(name));
3089                inputs.insert(normalize_identifier(identifier, strategy).name);
3090            }
3091        }
3092    }
3093    if open {
3094        return;
3095    }
3096
3097    // A child SELECT can use CTEs declared in a containing WITH. Reconstruct
3098    // that lexical catalogue for type inference, in original declaration order.
3099    let mut ctes: Vec<_> = if check_types {
3100        scope
3101            .cte_sources
3102            .values()
3103            .filter_map(|source| match &source.expression {
3104                Expression::Cte(cte) => Some(cte.as_ref().clone()),
3105                _ => None,
3106            })
3107            .collect()
3108    } else {
3109        Vec::new()
3110    };
3111    ctes.sort_by_key(|cte| cte.alias.span.map(|span| span.start));
3112    let mut aliases = HashMap::<String, Option<DataType>>::new();
3113    let Expression::Select(select) = &mut scope.expression else {
3114        unreachable!()
3115    };
3116    // Keep the original sources/CTEs for scoped type inference, without copying
3117    // the entire projection list for each alias. Bound uses are constant-size.
3118    let mut projections = std::mem::take(&mut select.expressions);
3119    let mut type_select = select.clone();
3120    if !ctes.is_empty() {
3121        type_select.with = Some(crate::expressions::With {
3122            ctes,
3123            recursive: false,
3124            leading_comments: Vec::new(),
3125            search: None,
3126        });
3127    }
3128    for projection in projections.iter_mut() {
3129        let mut references = ProjectionAliasBindings::new();
3130        for node in walk_in_scope(projection, false) {
3131            if !crate::binding::lateral_aliases(dialect) {
3132                break;
3133            }
3134            let Expression::Column(column) = node else {
3135                continue;
3136            };
3137            let Some(key) = projection_reference_key(column) else {
3138                continue;
3139            };
3140            let name = normalize_identifier(column.name.clone(), strategy).name;
3141            if inputs.contains(&name) {
3142                continue;
3143            }
3144            if let Some(Some(data_type)) = aliases.get(&name) {
3145                references.insert(key, data_type.clone());
3146            }
3147        }
3148        let original = std::mem::replace(projection, Expression::Null(crate::expressions::Null));
3149        *projection = apply_projection_alias_bindings(original, &references);
3150        bindings.extend(references);
3151        if let Expression::Alias(alias) = projection {
3152            let name = normalize_identifier(alias.alias.clone(), strategy).name;
3153            // Duplicate names are not evidence of an unambiguous binding.
3154            if aliases.contains_key(&name) {
3155                aliases.insert(name, None);
3156                continue;
3157            }
3158            if !check_types {
3159                aliases.insert(name, Some(DataType::Unknown));
3160                continue;
3161            }
3162            let mut typed = Expression::Select(type_select.clone());
3163            if let Expression::Select(query) = &mut typed {
3164                query.expressions.push(alias.this.clone());
3165            }
3166            // Child queries were bound first. Their aliases must contribute
3167            // their actual types, not remain unresolved inputs in this copy.
3168            typed = apply_projection_alias_bindings(typed, bindings);
3169            annotate_types(&mut typed, Some(resolver_schema), Some(dialect));
3170            let data_type = match typed {
3171                Expression::Select(query) => query.expressions[0]
3172                    .inferred_type()
3173                    .cloned()
3174                    .unwrap_or(DataType::Unknown),
3175                _ => unreachable!(),
3176            };
3177            aliases.insert(name, Some(data_type));
3178        }
3179    }
3180    select.expressions = projections;
3181    // Only clause-specific output visibility is enabled. JOIN ON remains an
3182    // input scope, and aliases never become visible in a nested query.
3183    use crate::binding::{clause_aliases, AliasClause};
3184    let mut clauses = Vec::new();
3185    if clause_aliases(dialect, AliasClause::Where) {
3186        if let Some(clause) = &mut select.where_clause {
3187            clauses.push(&mut clause.this);
3188        }
3189    }
3190    if clause_aliases(dialect, AliasClause::Group) {
3191        if let Some(clause) = &mut select.group_by {
3192            clauses.extend(clause.expressions.iter_mut());
3193        }
3194    }
3195    if clause_aliases(dialect, AliasClause::Having) {
3196        if let Some(clause) = &mut select.having {
3197            clauses.push(&mut clause.this);
3198        }
3199    }
3200    if clause_aliases(dialect, AliasClause::Qualify) {
3201        if let Some(clause) = &mut select.qualify {
3202            clauses.push(&mut clause.this);
3203        }
3204    }
3205    for clause in clauses {
3206        let mut references = ProjectionAliasBindings::new();
3207        for node in walk_in_scope(clause, false) {
3208            let Expression::Column(column) = node else {
3209                continue;
3210            };
3211            let Some(key) = projection_reference_key(column) else {
3212                continue;
3213            };
3214            let name = normalize_identifier(column.name.clone(), strategy).name;
3215            if !inputs.contains(&name) {
3216                if let Some(Some(data_type)) = aliases.get(&name) {
3217                    references.insert(key, data_type.clone());
3218                }
3219            }
3220        }
3221        let original = std::mem::replace(clause, Expression::Null(crate::expressions::Null));
3222        *clause = apply_projection_alias_bindings(original, &references);
3223        bindings.extend(references);
3224    }
3225}
3226
3227/// Resolver output names are strings. Retain quoting when those names were
3228/// declared by a CTE/derived table, rather than folding a quoted output name.
3229fn source_output_identifier<'a>(
3230    expression: &'a Expression,
3231    name: &str,
3232) -> Option<&'a crate::expressions::Identifier> {
3233    let columns: &[crate::expressions::Identifier] = match expression {
3234        Expression::Cte(cte) => &cte.columns,
3235        Expression::Subquery(query) => &query.column_aliases,
3236        Expression::Alias(alias) => &alias.column_aliases,
3237        Expression::Table(table) => &table.column_aliases,
3238        Expression::Paren(paren) => return source_output_identifier(&paren.this, name),
3239        _ => &[],
3240    };
3241    if !columns.is_empty() {
3242        return columns.iter().find(|column| column.name == name);
3243    }
3244    match scope_query(expression) {
3245        Expression::Select(select) => select.expressions.iter().find_map(|projection| {
3246            let identifier = match projection {
3247                Expression::Alias(alias) => &alias.alias,
3248                Expression::Column(column) => &column.name,
3249                Expression::Identifier(identifier) => identifier,
3250                _ => return None,
3251            };
3252            (identifier.name == name).then_some(identifier)
3253        }),
3254        Expression::Union(query) => source_output_identifier(&query.left, name),
3255        Expression::Intersect(query) => source_output_identifier(&query.left, name),
3256        Expression::Except(query) => source_output_identifier(&query.left, name),
3257        _ => None,
3258    }
3259}
3260
3261/// Prepare the private validation AST, not the public parser/serialized AST.
3262/// Bound parameters become identifiers (optionally typed), leaving only free
3263/// column references for schema resolution. Doing this once, before dotted
3264/// normalization, also prevents type checks from borrowing a shadowed table
3265/// column's type. Nested query scopes do not inherit lambda parameters.
3266use crate::binding::bind_lambdas as bind_validation_lambdas;
3267
3268#[derive(Clone, Copy)]
3269struct ReferenceValidationOptions {
3270    dialect: DialectType,
3271    strict: bool,
3272    check_references: bool,
3273    check_types: bool,
3274}
3275
3276#[derive(Clone, Copy, PartialEq, Eq)]
3277enum OutputNameResolution {
3278    InputOnly,
3279    OutputFirst,
3280    InputFirst,
3281}
3282
3283/// Collect ordering references together with whether they can resolve to the
3284/// current SELECT's outputs. Other clauses use the ordinary input-source walk.
3285fn order_by_validation_columns(
3286    order_by: &crate::expressions::OrderBy,
3287    dialect: DialectType,
3288) -> Vec<(&Column, OutputNameResolution)> {
3289    // Only enable aliases inside scalar expressions for dialects whose rules
3290    // support them. In particular, PostgreSQL and T-SQL require standalone
3291    // output names; `ORDER BY alias + 1` must resolve against input columns.
3292    let scalar_resolution = match dialect {
3293        // DuckDB's scalar binder prefers inputs inside ordering expressions,
3294        // unlike its standalone ORDER BY output-name lookup.
3295        DialectType::DuckDB => OutputNameResolution::InputFirst,
3296        DialectType::Snowflake | DialectType::BigQuery => OutputNameResolution::OutputFirst,
3297        _ => OutputNameResolution::InputOnly,
3298    };
3299    let mut columns = Vec::new();
3300    let mut pending: Vec<_> = order_by
3301        .expressions
3302        .iter()
3303        .rev()
3304        .map(|ordered| (&ordered.this, OutputNameResolution::OutputFirst))
3305        .collect();
3306    while let Some((node, resolution)) = pending.pop() {
3307        match node {
3308            // Child query scopes are validated separately and must not inherit
3309            // their parent's SELECT-list aliases.
3310            Expression::Select(_)
3311            | Expression::Union(_)
3312            | Expression::Intersect(_)
3313            | Expression::Except(_)
3314            | Expression::Subquery(_)
3315            | Expression::Exists(_)
3316            | Expression::Cte(_) => continue,
3317            Expression::Column(column) => columns.push((column.as_ref(), resolution)),
3318            Expression::Paren(paren) => pending.push((&paren.this, resolution)),
3319            _ => {
3320                let input_only = resolution == OutputNameResolution::InputOnly
3321                    || matches!(
3322                        node,
3323                        Expression::WindowFunction(_)
3324                            | Expression::WithinGroup(_)
3325                            | Expression::Filter(_)
3326                    )
3327                    || crate::traversal::is_aggregate(node);
3328                if input_only {
3329                    columns.extend(walk_in_scope(node, false).filter_map(|node| match node {
3330                        Expression::Column(column) => {
3331                            Some((column.as_ref(), OutputNameResolution::InputOnly))
3332                        }
3333                        _ => None,
3334                    }));
3335                } else {
3336                    pending.extend(
3337                        node.children()
3338                            .into_iter()
3339                            .rev()
3340                            .map(|child| (child, scalar_resolution)),
3341                    );
3342                }
3343            }
3344        }
3345    }
3346    columns
3347}
3348
3349fn validate_scope_columns(
3350    scope: &crate::scope::Scope,
3351    ancestors: &[&crate::scope::Scope],
3352    schema_map: &HashMap<String, TableSchemaEntry>,
3353    resolver_schema: &MappingSchema,
3354    options: ReferenceValidationOptions,
3355) -> Vec<ValidationError> {
3356    let ReferenceValidationOptions {
3357        dialect,
3358        strict,
3359        check_references,
3360        ..
3361    } = options;
3362    let mut errors = Vec::new();
3363    let Expression::Select(select) = &scope.expression else {
3364        return errors;
3365    };
3366
3367    // Normalize struct access with the actual lexical context. A real outer
3368    // qualifier must not be reinterpreted as a column of an open local table.
3369    let mut visible = crate::scope::Scope::new(scope.expression.clone());
3370    for source_scope in std::iter::once(scope).chain(ancestors.iter().copied()) {
3371        for (name, source) in &source_scope.sources {
3372            if resolve_scope_source_name(&visible, name).is_none() {
3373                visible.sources.insert(name.clone(), source.clone());
3374            }
3375        }
3376    }
3377    let mut normalized = select.clone();
3378    let mut normalizer = Resolver::new(&visible, resolver_schema, true);
3379    let _ = normalize_dotted_columns_in_scope(&mut normalized, &visible, &mut normalizer);
3380    let strategy = get_normalization_strategy(Some(dialect));
3381    let mut output_names = HashMap::<String, usize>::new();
3382    for projection in &normalized.expressions {
3383        let identifier = match projection {
3384            // Until wildcards are expanded, output-name uniqueness is not
3385            // known. Preserve source validation instead of letting an explicit
3386            // alias hide a conflicting column contributed by a wildcard.
3387            Expression::Star(_) => {
3388                output_names.clear();
3389                break;
3390            }
3391            Expression::Alias(alias) => &alias.alias,
3392            Expression::Column(column) if column.name.name != "*" => &column.name,
3393            Expression::Column(_) => {
3394                output_names.clear();
3395                break;
3396            }
3397            _ => continue,
3398        };
3399        let name = normalize_identifier(identifier.clone(), strategy).name;
3400        *output_names.entry(name).or_default() += 1;
3401    }
3402    // Separate query-level ordering from the ordinary scope walk. This avoids
3403    // accidentally exempting a same-named reference in another clause, window,
3404    // or nested query, and leaves the caller's AST untouched.
3405    let order_by = normalized.order_by.take();
3406    let expression = Expression::Select(normalized);
3407    let mut resolvers: Vec<_> = std::iter::once(scope)
3408        .chain(ancestors.iter().copied())
3409        .map(|scope| (scope, Resolver::new(scope, resolver_schema, true)))
3410        .collect();
3411    let using_columns: HashSet<String> = select
3412        .joins
3413        .iter()
3414        .flat_map(|join| join.using.iter().map(|id| lower(&id.name)))
3415        .collect();
3416
3417    let input_columns = walk_in_scope(&expression, false).filter_map(|node| match node {
3418        Expression::Column(column) => Some((column.as_ref(), OutputNameResolution::InputOnly)),
3419        _ => None,
3420    });
3421    let order_columns = order_by
3422        .as_ref()
3423        .into_iter()
3424        .flat_map(|order| order_by_validation_columns(order, dialect));
3425    for (column, resolution) in input_columns.chain(order_columns) {
3426        let output_matches =
3427            if resolution != OutputNameResolution::InputOnly && column.table.is_none() {
3428                let name = normalize_identifier(column.name.clone(), strategy).name;
3429                // Only an unambiguous output name takes precedence over sources.
3430                // Duplicate output names retain the existing reference checks.
3431                output_names.get(&name) == Some(&1)
3432            } else {
3433                false
3434            };
3435        if output_matches && resolution == OutputNameResolution::OutputFirst {
3436            continue;
3437        }
3438        let name = lower(&column.name.name);
3439        if name.is_empty() || name == "*" {
3440            continue;
3441        }
3442        let span = column.name.span.or(column.span);
3443        if let Some(qualifier) = &column.table {
3444            let resolved = resolvers.iter_mut().find_map(|(scope, resolver)| {
3445                resolve_scope_source_name(scope, &qualifier.name).map(|source| {
3446                    (
3447                        *scope,
3448                        resolver.get_source_columns(&source).unwrap_or_default(),
3449                        source,
3450                    )
3451                })
3452            });
3453            if let Some((source_scope, columns, source)) = resolved {
3454                if !columns.is_empty()
3455                    && !source_has_column(source_scope, &source, &columns, &column.name, dialect)
3456                {
3457                    errors.push(reference_diagnostic(
3458                        format!(
3459                            "Unknown column '{}' in table '{}'",
3460                            name,
3461                            source_display_name(source_scope, &source)
3462                        ),
3463                        validation_codes::E_UNKNOWN_COLUMN,
3464                        strict,
3465                        span,
3466                    ));
3467                }
3468            } else {
3469                errors.push(reference_diagnostic(
3470                    format!(
3471                        "Unknown table or alias '{}' referenced by column '{}'",
3472                        qualifier.name, name
3473                    ),
3474                    validation_codes::E_UNRESOLVED_REFERENCE,
3475                    strict,
3476                    qualifier.span.or(column.span),
3477                ));
3478            }
3479            continue;
3480        }
3481
3482        let mut found = false;
3483        for (source_scope, resolver) in &mut resolvers {
3484            let mut matches = 0;
3485            let mut open = false;
3486            for source in source_scope.sources.keys() {
3487                let columns = resolver.get_source_columns(source).unwrap_or_default();
3488                open |= columns.is_empty() || columns.iter().any(|name| name == "*");
3489                // Wildcards are not evidence of a definite ambiguity.
3490                matches += usize::from(
3491                    !columns.iter().any(|name| name == "*")
3492                        && source_has_column(source_scope, source, &columns, &column.name, dialect),
3493                );
3494            }
3495            if matches > 0 || open {
3496                if matches > 1 && check_references && !using_columns.contains(&name) {
3497                    errors.push(reference_diagnostic(
3498                        format!(
3499                            "Ambiguous unqualified column '{}' found in {} referenced tables",
3500                            name, matches
3501                        ),
3502                        if strict {
3503                            validation_codes::E_AMBIGUOUS_COLUMN_REFERENCE
3504                        } else {
3505                            validation_codes::W_WEAK_REFERENCE_INTEGRITY
3506                        },
3507                        strict,
3508                        span,
3509                    ));
3510                }
3511                found = true;
3512                break;
3513            }
3514        }
3515        if !found {
3516            if output_matches && resolution == OutputNameResolution::InputFirst {
3517                continue;
3518            }
3519            // Preserve the existing schema-only lookup for standalone column
3520            // expressions (SELECT id). Never consult unrelated tables when
3521            // the query has actual sources or an enclosing query context.
3522            if scope.sources.is_empty()
3523                && ancestors.is_empty()
3524                && (schema_map.is_empty()
3525                    || schema_map
3526                        .values()
3527                        .any(|table| table.columns.contains_key(&name)))
3528            {
3529                continue;
3530            }
3531            let message = if scope.sources.len() == 1 {
3532                let source = scope.sources.keys().next().unwrap();
3533                format!(
3534                    "Unknown column '{}' in table '{}'",
3535                    name,
3536                    source_display_name(scope, source)
3537                )
3538            } else {
3539                format!(
3540                    "Unknown column '{}' (not found in any referenced table)",
3541                    name
3542                )
3543            };
3544            errors.push(reference_diagnostic(
3545                message,
3546                validation_codes::E_UNKNOWN_COLUMN,
3547                strict,
3548                span,
3549            ));
3550        }
3551    }
3552    errors
3553}
3554
3555fn validate_scope_tree(
3556    scope: &crate::scope::Scope,
3557    ancestors: &[&crate::scope::Scope],
3558    schema_map: &HashMap<String, TableSchemaEntry>,
3559    resolver_schema: &MappingSchema,
3560    options: ReferenceValidationOptions,
3561    errors: &mut Vec<ValidationError>,
3562    bindings: &mut ProjectionAliasBindings,
3563) {
3564    let mut selected = selected_validation_scope(scope);
3565    // Bind children before consumers, but retain the existing parent-first
3566    // diagnostic order. Only physical sources are inherited, never output aliases.
3567    let mut child_errors = Vec::new();
3568    // CTEs and ordinary derived tables cannot see their containing SELECT's
3569    // sources, but may retain ancestors of an enclosing correlated subquery.
3570    for child in scope
3571        .cte_scopes
3572        .iter()
3573        .chain(&scope.derived_table_scopes)
3574        .chain(&scope.union_scopes)
3575    {
3576        validate_scope_tree(
3577            child,
3578            ancestors,
3579            schema_map,
3580            resolver_schema,
3581            options,
3582            &mut child_errors,
3583            bindings,
3584        );
3585    }
3586    let outer: Vec<_> = std::iter::once(&selected)
3587        .chain(ancestors.iter().copied())
3588        .collect();
3589    for child in scope.subquery_scopes.iter().chain(&scope.udtf_scopes) {
3590        validate_scope_tree(
3591            child,
3592            &outer,
3593            schema_map,
3594            resolver_schema,
3595            options,
3596            &mut child_errors,
3597            bindings,
3598        );
3599    }
3600    bind_scope_projection_aliases(
3601        &mut selected,
3602        ancestors,
3603        resolver_schema,
3604        options.dialect,
3605        options.check_types,
3606        bindings,
3607    );
3608    for node in walk_in_scope(&selected.expression, false) {
3609        let Expression::Table(table) = node else {
3610            continue;
3611        };
3612        let is_cte = table.schema.is_none()
3613            && table.catalog.is_none()
3614            && selected
3615                .cte_sources
3616                .keys()
3617                .any(|name| name.eq_ignore_ascii_case(&table.name.name));
3618        if !is_cte {
3619            validate_schema_table(table, schema_map, options.strict, errors);
3620        }
3621    }
3622    errors.extend(validate_scope_columns(
3623        &selected,
3624        ancestors,
3625        schema_map,
3626        resolver_schema,
3627        options,
3628    ));
3629    errors.extend(child_errors);
3630}
3631
3632fn validate_schema_table(
3633    table: &TableRef,
3634    schema_map: &HashMap<String, TableSchemaEntry>,
3635    strict: bool,
3636    errors: &mut Vec<ValidationError>,
3637) {
3638    if !table_ref_candidates(table)
3639        .iter()
3640        .any(|key| schema_map.contains_key(key))
3641    {
3642        errors.push(reference_diagnostic(
3643            format!("Unknown table '{}'", table_ref_display_name(table)),
3644            validation_codes::E_UNKNOWN_TABLE,
3645            strict,
3646            table.name.span,
3647        ));
3648    }
3649}
3650
3651fn validate_statement_with_schema(
3652    stmt: &Expression,
3653    schema_map: &HashMap<String, TableSchemaEntry>,
3654    resolver_schema: &MappingSchema,
3655    options: ReferenceValidationOptions,
3656    bindings: &mut ProjectionAliasBindings,
3657) -> Vec<ValidationError> {
3658    let mut errors = Vec::new();
3659    // Visit each query tree once, including queries embedded in DML/DDL.
3660    let mut pending = vec![stmt];
3661    while let Some(expression) = pending.pop() {
3662        if let Some(select) = crate::binding::dml_scope(expression) {
3663            let query = Expression::Select(Box::new(select));
3664            let scope = build_scope(&query);
3665            validate_scope_tree(
3666                &scope,
3667                &[],
3668                schema_map,
3669                resolver_schema,
3670                options,
3671                &mut errors,
3672                bindings,
3673            );
3674            let target_columns = match expression {
3675                Expression::Insert(insert) => {
3676                    Some((&insert.table, insert.columns.iter().collect::<Vec<_>>()))
3677                }
3678                Expression::Update(update) => Some((
3679                    &update.table,
3680                    update.set.iter().map(|(column, _)| column).collect(),
3681                )),
3682                _ => None,
3683            };
3684            if let Some((table, columns)) = target_columns {
3685                if let Some((_, entry)) = resolve_table_schema_entry(table, schema_map) {
3686                    for column in columns {
3687                        if !entry.columns.is_empty()
3688                            && !entry.columns.contains_key("*")
3689                            && !entry.columns.contains_key(&lower(&column.name))
3690                        {
3691                            errors.push(reference_diagnostic(
3692                                format!(
3693                                    "Unknown column '{}' in table '{}'",
3694                                    column.name, table.name.name
3695                                ),
3696                                validation_codes::E_UNKNOWN_COLUMN,
3697                                options.strict,
3698                                column.span,
3699                            ));
3700                        }
3701                    }
3702                }
3703            }
3704            if let Expression::Insert(insert) = expression {
3705                // VALUES has no implicit target-table input scope.
3706                for value in insert.values.iter().flatten() {
3707                    for node in walk_in_scope(value, false) {
3708                        if let Expression::Column(column) = node {
3709                            errors.push(reference_diagnostic(
3710                                format!("Unknown column '{}' in INSERT VALUES", column.name.name),
3711                                validation_codes::E_UNKNOWN_COLUMN,
3712                                options.strict,
3713                                column.name.span.or(column.span),
3714                            ));
3715                        }
3716                    }
3717                    pending.extend(value.children().into_iter().filter(|child| {
3718                        matches!(child, Expression::Subquery(_) | Expression::Select(_))
3719                    }));
3720                }
3721                if let Some(query) = &insert.query {
3722                    // A leading WITH belongs to both the INSERT and its source.
3723                    let mut source = crate::expressions::Select::new();
3724                    source.with = insert.with.clone();
3725                    source.expressions.push(query.clone());
3726                    let scope = build_scope(&Expression::Select(Box::new(source)));
3727                    validate_scope_tree(
3728                        &scope,
3729                        &[],
3730                        schema_map,
3731                        resolver_schema,
3732                        options,
3733                        &mut errors,
3734                        bindings,
3735                    );
3736                }
3737            }
3738            if let Expression::Merge(merge) = expression {
3739                let target = match merge.this.as_ref() {
3740                    Expression::Table(table) => Some(table.as_ref()),
3741                    Expression::Alias(alias) => match &alias.this {
3742                        Expression::Table(table) => Some(table.as_ref()),
3743                        _ => None,
3744                    },
3745                    _ => None,
3746                };
3747                if let Some((table, (_, entry))) = target.and_then(|table| {
3748                    resolve_table_schema_entry(table, schema_map).map(|entry| (table, entry))
3749                }) {
3750                    if let Some(whens) = &merge.whens {
3751                        for node in whens.dfs() {
3752                            let Expression::When(when) = node else {
3753                                continue;
3754                            };
3755                            let Expression::Tuple(action) = when.then.as_ref() else {
3756                                continue;
3757                            };
3758                            let Some(Expression::Var(kind)) = action.expressions.first() else {
3759                                continue;
3760                            };
3761                            let Some(Expression::Tuple(assignments)) = action.expressions.get(1)
3762                            else {
3763                                continue;
3764                            };
3765                            for assignment in &assignments.expressions {
3766                                let target = if kind.this.eq_ignore_ascii_case("UPDATE") {
3767                                    if let Expression::Eq(eq) = assignment {
3768                                        &eq.left
3769                                    } else {
3770                                        continue;
3771                                    }
3772                                } else if kind.this.eq_ignore_ascii_case("INSERT") {
3773                                    assignment
3774                                } else {
3775                                    continue;
3776                                };
3777                                let identifier = match target {
3778                                    Expression::Identifier(id) => id,
3779                                    Expression::Column(column) => &column.name,
3780                                    _ => continue,
3781                                };
3782                                if !entry.columns.is_empty()
3783                                    && !entry.columns.contains_key("*")
3784                                    && !entry.columns.contains_key(&lower(&identifier.name))
3785                                {
3786                                    errors.push(reference_diagnostic(
3787                                        format!(
3788                                            "Unknown column '{}' in table '{}'",
3789                                            identifier.name, table.name.name
3790                                        ),
3791                                        validation_codes::E_UNKNOWN_COLUMN,
3792                                        options.strict,
3793                                        identifier.span,
3794                                    ));
3795                                }
3796                            }
3797                        }
3798                    }
3799                }
3800            }
3801            continue;
3802        }
3803        if matches!(
3804            scope_query(expression),
3805            Expression::Select(_)
3806                | Expression::Union(_)
3807                | Expression::Intersect(_)
3808                | Expression::Except(_)
3809        ) {
3810            let scope = build_scope(expression);
3811            validate_scope_tree(
3812                &scope,
3813                &[],
3814                schema_map,
3815                resolver_schema,
3816                options,
3817                &mut errors,
3818                bindings,
3819            );
3820        } else {
3821            // DDL may declare a new table; only its query inputs are references.
3822            if let Expression::CreateTable(create) = expression {
3823                if let Some(query) = &create.as_select {
3824                    pending.push(query);
3825                }
3826                if let Some(source) = &create.clone_source {
3827                    validate_schema_table(source, schema_map, options.strict, &mut errors);
3828                }
3829                continue;
3830            }
3831            if let Expression::Table(table) = expression {
3832                validate_schema_table(table, schema_map, options.strict, &mut errors);
3833            }
3834            pending.extend(expression.children().into_iter().rev());
3835        }
3836    }
3837    errors
3838}
3839
3840/// Validate SQL using syntax + schema-aware checks, with optional semantic checks.
3841pub fn validate_with_schema(
3842    sql: &str,
3843    dialect: DialectType,
3844    schema: &ValidationSchema,
3845    options: &SchemaValidationOptions,
3846) -> ValidationResult {
3847    let strict = options.strict.unwrap_or(schema.strict.unwrap_or(true));
3848
3849    // Parse once, preserving syntax-error precedence and the caller's guards.
3850    let d = Dialect::get(dialect);
3851    let statements = match crate::parse_for_validation(
3852        sql,
3853        &d,
3854        &crate::ValidationOptions {
3855            complexity_guard: options.complexity_guard,
3856            strict_syntax: options.strict_syntax,
3857            semantic: options.semantic,
3858        },
3859    ) {
3860        Ok(exprs) => exprs,
3861        Err(result) => return result,
3862    };
3863
3864    let schema_map = build_schema_map(schema);
3865    let resolver_schema = mapping_schema_from_validation_schema_with_dialect(schema, dialect);
3866    let mut all_errors = Vec::new();
3867    let embedded_function_catalog = if options.check_types && options.function_catalog.is_none() {
3868        default_embedded_function_catalog()
3869    } else {
3870        None
3871    };
3872    let effective_function_catalog = options
3873        .function_catalog
3874        .as_deref()
3875        .or_else(|| embedded_function_catalog.as_deref());
3876    let declared_relationships = if options.check_references {
3877        build_declared_relationships(schema, &schema_map)
3878    } else {
3879        Vec::new()
3880    };
3881
3882    if options.check_references {
3883        all_errors.extend(check_reference_integrity(schema, &schema_map, strict));
3884    }
3885
3886    for mut statement in statements {
3887        if options.semantic {
3888            all_errors.extend(check_semantics(&statement, dialect, Some(schema)));
3889        }
3890        crate::binding::bind_dml_pseudoreferences(&mut statement);
3891        let statement = bind_validation_lambdas(statement, dialect);
3892        let mut bindings = ProjectionAliasBindings::new();
3893        all_errors.extend(validate_statement_with_schema(
3894            &statement,
3895            &schema_map,
3896            &resolver_schema,
3897            ReferenceValidationOptions {
3898                dialect,
3899                strict,
3900                check_references: options.check_references,
3901                check_types: options.check_types,
3902            },
3903            &mut bindings,
3904        ));
3905        let mut statement = apply_projection_alias_bindings(statement, &bindings);
3906        if options.check_types {
3907            // Expand known stars using the same qualification path as lineage.
3908            // Open/partial schemas remain conservative if qualification fails.
3909            if statement.dfs().any(|node| {
3910                matches!(
3911                    node,
3912                    Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_)
3913                )
3914            }) && statement
3915                .dfs()
3916                .any(|node| matches!(node, Expression::Star(_)))
3917            {
3918                if let Ok(qualified) = crate::optimizer::qualify_schema_aware_expression(
3919                    statement.clone(),
3920                    &resolver_schema,
3921                    Some(dialect),
3922                ) {
3923                    statement = qualified;
3924                }
3925            }
3926            annotate_types(&mut statement, Some(&resolver_schema), Some(dialect));
3927        }
3928        let stmt = &statement;
3929        if options.check_types {
3930            all_errors.extend(check_types(
3931                stmt,
3932                dialect,
3933                &schema_map,
3934                effective_function_catalog,
3935                strict,
3936            ));
3937        }
3938        if options.check_references {
3939            all_errors.extend(check_query_reference_quality(
3940                stmt,
3941                &schema_map,
3942                &resolver_schema,
3943                &declared_relationships,
3944            ));
3945        }
3946    }
3947
3948    ValidationResult::with_errors(all_errors)
3949}
3950
3951#[cfg(test)]
3952mod tests;