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::Vertica { vertica_type } => match vertica_type.as_ref() {
756            crate::expressions::VerticaDataType::Array { .. }
757            | crate::expressions::VerticaDataType::Set { .. } => TypeFamily::Array,
758            crate::expressions::VerticaDataType::Row { .. } => TypeFamily::Struct,
759            crate::expressions::VerticaDataType::LongVarBinary { .. } => TypeFamily::Binary,
760            crate::expressions::VerticaDataType::Interval { .. } => TypeFamily::Interval,
761        },
762        DataType::Boolean => TypeFamily::Boolean,
763        DataType::TinyInt { .. }
764        | DataType::SmallInt { .. }
765        | DataType::Int { .. }
766        | DataType::BigInt { .. }
767        | DataType::Int128
768        | DataType::UInt8
769        | DataType::UInt16
770        | DataType::UInt32
771        | DataType::UInt64
772        | DataType::UInt128 => TypeFamily::Integer,
773        DataType::Float { .. } | DataType::Double { .. } | DataType::Decimal { .. } => {
774            TypeFamily::Numeric
775        }
776        DataType::Hana { hana_type } => match hana_type.name.as_str() {
777            "TINYINT" | "SMALLINT" | "INT" | "BIGINT" => TypeFamily::Integer,
778            "TIMESTAMP" | "SECONDDATE" => TypeFamily::Timestamp,
779            "TIME" => TypeFamily::Time,
780            "FLOAT" | "DECIMAL" | "SMALLDECIMAL" => TypeFamily::Numeric,
781            "BINTEXT" => TypeFamily::Binary,
782            "ALPHANUM" | "VARCHAR" | "NVARCHAR" | "CLOB" | "NCLOB" | "TEXT" | "SHORTTEXT" => {
783                TypeFamily::String
784            }
785            _ => TypeFamily::Unknown,
786        },
787        DataType::Oracle { oracle_type } => match oracle_type {
788            OracleDataType::Number { .. }
789            | OracleDataType::BinaryFloat
790            | OracleDataType::BinaryDouble
791            | OracleDataType::Float { .. } => TypeFamily::Numeric,
792            OracleDataType::Character { .. }
793            | OracleDataType::Clob { .. }
794            | OracleDataType::Long { raw: false }
795            | OracleDataType::RowId => TypeFamily::String,
796            OracleDataType::Blob
797            | OracleDataType::Raw { .. }
798            | OracleDataType::Long { raw: true } => TypeFamily::Binary,
799            OracleDataType::Date => TypeFamily::Date,
800            OracleDataType::Timestamp { .. } => TypeFamily::Timestamp,
801            OracleDataType::IntervalYearToMonth { .. }
802            | OracleDataType::IntervalDayToSecond { .. } => TypeFamily::Interval,
803        },
804        DataType::Char { .. }
805        | DataType::VarChar { .. }
806        | DataType::String { .. }
807        | DataType::Text
808        | DataType::TextWithLength { .. }
809        | DataType::CharacterSet { .. } => TypeFamily::String,
810        DataType::Binary { .. } | DataType::VarBinary { .. } | DataType::Blob => TypeFamily::Binary,
811        DataType::Date => TypeFamily::Date,
812        DataType::Time { .. } => TypeFamily::Time,
813        DataType::Timestamp { .. } => TypeFamily::Timestamp,
814        DataType::Interval { .. } => TypeFamily::Interval,
815        DataType::Json | DataType::JsonB => TypeFamily::Json,
816        DataType::Uuid => TypeFamily::Uuid,
817        DataType::Array { .. } | DataType::List { .. } => TypeFamily::Array,
818        DataType::Map { .. } => TypeFamily::Map,
819        DataType::Struct { .. } | DataType::Object { .. } | DataType::Union { .. } => {
820            TypeFamily::Struct
821        }
822        DataType::Nullable { inner } => data_type_family(inner),
823        DataType::Custom { name } => canonical_type_family(name),
824        DataType::Unknown => TypeFamily::Unknown,
825        DataType::Bit { .. } | DataType::VarBit { .. } => TypeFamily::Binary,
826        DataType::Enum { .. } | DataType::Set { .. } => TypeFamily::String,
827        DataType::Vector { .. } => TypeFamily::Array,
828        DataType::Geometry { .. } | DataType::Geography { .. } => TypeFamily::Struct,
829    }
830}
831
832fn collect_type_check_context(
833    stmt: &Expression,
834    schema_map: &HashMap<String, TableSchemaEntry>,
835) -> TypeCheckContext {
836    fn add_table_to_context(
837        table: &TableRef,
838        schema_map: &HashMap<String, TableSchemaEntry>,
839        context: &mut TypeCheckContext,
840    ) {
841        let resolved_key = table_ref_candidates(table)
842            .into_iter()
843            .find(|k| schema_map.contains_key(k));
844
845        let Some(table_key) = resolved_key else {
846            return;
847        };
848
849        context.referenced_tables.insert(table_key.clone());
850        context
851            .table_aliases
852            .insert(lower(&table.name.name), table_key.clone());
853        if let Some(alias) = &table.alias {
854            context
855                .table_aliases
856                .insert(lower(&alias.name), table_key.clone());
857        }
858    }
859
860    let mut context = TypeCheckContext::default();
861    let cte_aliases: HashSet<_> = build_scope(stmt)
862        .cte_sources
863        .keys()
864        .map(|name| lower(name))
865        .collect();
866
867    for node in walk_in_scope(stmt, false) {
868        let Expression::Table(table) = node else {
869            continue;
870        };
871
872        if cte_aliases.contains(&lower(&table.name.name)) {
873            continue;
874        }
875
876        add_table_to_context(table, schema_map, &mut context);
877    }
878
879    // Seed DML target tables explicitly because they are struct fields and may
880    // not appear as standalone Expression::Table nodes in traversal output.
881    match stmt {
882        Expression::Insert(insert) => {
883            add_table_to_context(&insert.table, schema_map, &mut context);
884        }
885        Expression::Update(update) => {
886            add_table_to_context(&update.table, schema_map, &mut context);
887            for table in &update.extra_tables {
888                add_table_to_context(table, schema_map, &mut context);
889            }
890        }
891        Expression::Delete(delete) => {
892            add_table_to_context(&delete.table, schema_map, &mut context);
893            for table in &delete.using {
894                add_table_to_context(table, schema_map, &mut context);
895            }
896            for table in &delete.tables {
897                add_table_to_context(table, schema_map, &mut context);
898            }
899        }
900        _ => {}
901    }
902
903    context
904}
905
906fn resolve_table_schema_entry<'a>(
907    table: &TableRef,
908    schema_map: &'a HashMap<String, TableSchemaEntry>,
909) -> Option<(String, &'a TableSchemaEntry)> {
910    let key = table_ref_candidates(table)
911        .into_iter()
912        .find(|k| schema_map.contains_key(k))?;
913    let entry = schema_map.get(&key)?;
914    Some((key, entry))
915}
916
917fn reference_issue(strict: bool, message: impl Into<String>) -> ValidationError {
918    if strict {
919        ValidationError::error(
920            message.into(),
921            validation_codes::E_INVALID_FOREIGN_KEY_REFERENCE,
922        )
923    } else {
924        ValidationError::warning(message.into(), validation_codes::W_WEAK_REFERENCE_INTEGRITY)
925    }
926}
927
928fn reference_table_candidates(
929    table_name: &str,
930    explicit_schema: Option<&str>,
931    source_schema: Option<&str>,
932) -> Vec<String> {
933    let mut candidates = Vec::new();
934    let raw = lower(table_name);
935
936    if let Some(schema) = explicit_schema {
937        candidates.push(format!("{}.{}", lower(schema), raw));
938    }
939
940    if raw.contains('.') {
941        candidates.push(raw.clone());
942        if let Some(last) = raw.rsplit('.').next() {
943            candidates.push(last.to_string());
944        }
945    } else {
946        if let Some(schema) = source_schema {
947            candidates.push(format!("{}.{}", lower(schema), raw));
948        }
949        candidates.push(raw);
950    }
951
952    let mut dedup = HashSet::new();
953    candidates
954        .into_iter()
955        .filter(|c| dedup.insert(c.clone()))
956        .collect()
957}
958
959fn resolve_reference_table_key(
960    table_name: &str,
961    explicit_schema: Option<&str>,
962    source_schema: Option<&str>,
963    schema_map: &HashMap<String, TableSchemaEntry>,
964) -> Option<String> {
965    reference_table_candidates(table_name, explicit_schema, source_schema)
966        .into_iter()
967        .find(|candidate| schema_map.contains_key(candidate))
968}
969
970fn key_types_compatible(source: TypeFamily, target: TypeFamily) -> bool {
971    if source == TypeFamily::Unknown || target == TypeFamily::Unknown {
972        return true;
973    }
974    if source == target {
975        return true;
976    }
977    if source.is_numeric() && target.is_numeric() {
978        return true;
979    }
980    if source.is_temporal() && target.is_temporal() {
981        return true;
982    }
983    false
984}
985
986fn table_key_hints(table: &SchemaTable) -> HashSet<String> {
987    let mut hints = HashSet::new();
988    for column in &table.columns {
989        if column.primary_key || column.unique {
990            hints.insert(lower(&column.name));
991        }
992    }
993    for key_col in &table.primary_key {
994        hints.insert(lower(key_col));
995    }
996    for group in &table.unique_keys {
997        if group.len() == 1 {
998            if let Some(col) = group.first() {
999                hints.insert(lower(col));
1000            }
1001        }
1002    }
1003    hints
1004}
1005
1006fn check_reference_integrity(
1007    schema: &ValidationSchema,
1008    schema_map: &HashMap<String, TableSchemaEntry>,
1009    strict: bool,
1010) -> Vec<ValidationError> {
1011    let mut errors = Vec::new();
1012
1013    let mut key_hints_lookup: HashMap<String, HashSet<String>> = HashMap::new();
1014    for table in &schema.tables {
1015        let simple = lower(&table.name);
1016        key_hints_lookup.insert(simple, table_key_hints(table));
1017        if let Some(schema_name) = &table.schema {
1018            let qualified = format!("{}.{}", lower(schema_name), lower(&table.name));
1019            key_hints_lookup.insert(qualified, table_key_hints(table));
1020        }
1021    }
1022
1023    for table in &schema.tables {
1024        let source_table_display = if let Some(schema_name) = &table.schema {
1025            format!("{}.{}", schema_name, table.name)
1026        } else {
1027            table.name.clone()
1028        };
1029        let source_schema = table.schema.as_deref();
1030        let source_columns: HashMap<String, TypeFamily> = table
1031            .columns
1032            .iter()
1033            .map(|col| (lower(&col.name), canonical_type_family(&col.data_type)))
1034            .collect();
1035
1036        for source_col in &table.columns {
1037            let Some(reference) = &source_col.references else {
1038                continue;
1039            };
1040            let source_type = canonical_type_family(&source_col.data_type);
1041
1042            let Some(target_key) = resolve_reference_table_key(
1043                &reference.table,
1044                reference.schema.as_deref(),
1045                source_schema,
1046                schema_map,
1047            ) else {
1048                errors.push(reference_issue(
1049                    strict,
1050                    format!(
1051                        "Foreign key reference '{}.{}' points to unknown table '{}'",
1052                        source_table_display, source_col.name, reference.table
1053                    ),
1054                ));
1055                continue;
1056            };
1057
1058            let target_column = lower(&reference.column);
1059            let Some(target_entry) = schema_map.get(&target_key) else {
1060                errors.push(reference_issue(
1061                    strict,
1062                    format!(
1063                        "Foreign key reference '{}.{}' points to unknown table '{}'",
1064                        source_table_display, source_col.name, reference.table
1065                    ),
1066                ));
1067                continue;
1068            };
1069
1070            let Some(target_type) = target_entry.columns.get(&target_column).copied() else {
1071                errors.push(reference_issue(
1072                    strict,
1073                    format!(
1074                        "Foreign key reference '{}.{}' points to unknown column '{}.{}'",
1075                        source_table_display, source_col.name, target_key, reference.column
1076                    ),
1077                ));
1078                continue;
1079            };
1080
1081            if !key_types_compatible(source_type, target_type) {
1082                errors.push(reference_issue(
1083                    strict,
1084                    format!(
1085                        "Foreign key type mismatch for '{}.{}' -> '{}.{}': {} vs {}",
1086                        source_table_display,
1087                        source_col.name,
1088                        target_key,
1089                        reference.column,
1090                        type_family_name(source_type),
1091                        type_family_name(target_type)
1092                    ),
1093                ));
1094            }
1095
1096            if let Some(target_key_hints) = key_hints_lookup.get(&target_key) {
1097                if !target_key_hints.contains(&target_column) {
1098                    errors.push(ValidationError::warning(
1099                        format!(
1100                            "Referenced column '{}.{}' is not marked as primary/unique key",
1101                            target_key, reference.column
1102                        ),
1103                        validation_codes::W_WEAK_REFERENCE_INTEGRITY,
1104                    ));
1105                }
1106            }
1107        }
1108
1109        for foreign_key in &table.foreign_keys {
1110            if foreign_key.columns.is_empty() || foreign_key.references.columns.is_empty() {
1111                errors.push(reference_issue(
1112                    strict,
1113                    format!(
1114                        "Table-level foreign key on '{}' has empty source or target column list",
1115                        source_table_display
1116                    ),
1117                ));
1118                continue;
1119            }
1120            if foreign_key.columns.len() != foreign_key.references.columns.len() {
1121                errors.push(reference_issue(
1122                    strict,
1123                    format!(
1124                        "Table-level foreign key on '{}' has {} source columns but {} target columns",
1125                        source_table_display,
1126                        foreign_key.columns.len(),
1127                        foreign_key.references.columns.len()
1128                    ),
1129                ));
1130                continue;
1131            }
1132
1133            let Some(target_key) = resolve_reference_table_key(
1134                &foreign_key.references.table,
1135                foreign_key.references.schema.as_deref(),
1136                source_schema,
1137                schema_map,
1138            ) else {
1139                errors.push(reference_issue(
1140                    strict,
1141                    format!(
1142                        "Table-level foreign key on '{}' points to unknown table '{}'",
1143                        source_table_display, foreign_key.references.table
1144                    ),
1145                ));
1146                continue;
1147            };
1148
1149            let Some(target_entry) = schema_map.get(&target_key) else {
1150                errors.push(reference_issue(
1151                    strict,
1152                    format!(
1153                        "Table-level foreign key on '{}' points to unknown table '{}'",
1154                        source_table_display, foreign_key.references.table
1155                    ),
1156                ));
1157                continue;
1158            };
1159
1160            for (source_col, target_col) in foreign_key
1161                .columns
1162                .iter()
1163                .zip(foreign_key.references.columns.iter())
1164            {
1165                let source_col_name = lower(source_col);
1166                let target_col_name = lower(target_col);
1167
1168                let Some(source_type) = source_columns.get(&source_col_name).copied() else {
1169                    errors.push(reference_issue(
1170                        strict,
1171                        format!(
1172                            "Table-level foreign key on '{}' references unknown source column '{}'",
1173                            source_table_display, source_col
1174                        ),
1175                    ));
1176                    continue;
1177                };
1178
1179                let Some(target_type) = target_entry.columns.get(&target_col_name).copied() else {
1180                    errors.push(reference_issue(
1181                        strict,
1182                        format!(
1183                            "Table-level foreign key on '{}' references unknown target column '{}.{}'",
1184                            source_table_display, target_key, target_col
1185                        ),
1186                    ));
1187                    continue;
1188                };
1189
1190                if !key_types_compatible(source_type, target_type) {
1191                    errors.push(reference_issue(
1192                        strict,
1193                        format!(
1194                            "Table-level foreign key type mismatch '{}.{}' -> '{}.{}': {} vs {}",
1195                            source_table_display,
1196                            source_col,
1197                            target_key,
1198                            target_col,
1199                            type_family_name(source_type),
1200                            type_family_name(target_type)
1201                        ),
1202                    ));
1203                }
1204
1205                if let Some(target_key_hints) = key_hints_lookup.get(&target_key) {
1206                    if !target_key_hints.contains(&target_col_name) {
1207                        errors.push(ValidationError::warning(
1208                            format!(
1209                                "Referenced column '{}.{}' is not marked as primary/unique key",
1210                                target_key, target_col
1211                            ),
1212                            validation_codes::W_WEAK_REFERENCE_INTEGRITY,
1213                        ));
1214                    }
1215                }
1216            }
1217        }
1218    }
1219
1220    errors
1221}
1222
1223fn infer_expression_type_family(
1224    expr: &Expression,
1225    schema_map: &HashMap<String, TableSchemaEntry>,
1226    context: &TypeCheckContext,
1227) -> TypeFamily {
1228    // Scoped annotation wins, including Unknown; never rebind an annotated
1229    // reference against unrelated sources in the statement-wide catalogue.
1230    if let Some(data_type) = expr.inferred_type() {
1231        return data_type_family(data_type);
1232    }
1233    infer_expression_type_family_fallback(expr, schema_map, context)
1234}
1235
1236fn infer_expression_type_family_fallback(
1237    expr: &Expression,
1238    schema_map: &HashMap<String, TableSchemaEntry>,
1239    context: &TypeCheckContext,
1240) -> TypeFamily {
1241    match expr {
1242        Expression::Literal(literal) => match literal.as_ref() {
1243            crate::expressions::Literal::Number(value) => {
1244                if value.contains('.') || value.contains('e') || value.contains('E') {
1245                    TypeFamily::Numeric
1246                } else {
1247                    TypeFamily::Integer
1248                }
1249            }
1250            crate::expressions::Literal::HexNumber(_) => TypeFamily::Integer,
1251            crate::expressions::Literal::Date(_) => TypeFamily::Date,
1252            crate::expressions::Literal::Time(_) => TypeFamily::Time,
1253            crate::expressions::Literal::Timestamp(_)
1254            | crate::expressions::Literal::Datetime(_) => TypeFamily::Timestamp,
1255            crate::expressions::Literal::HexString(_)
1256            | crate::expressions::Literal::BitString(_)
1257            | crate::expressions::Literal::ByteString(_) => TypeFamily::Binary,
1258            _ => TypeFamily::String,
1259        },
1260        Expression::Boolean(_) => TypeFamily::Boolean,
1261        Expression::Null(_) => TypeFamily::Unknown,
1262        // The shared annotator already resolved every lexical input. Missing
1263        // annotations must not trigger a second, statement-global lookup.
1264        Expression::Column(_) => TypeFamily::Unknown,
1265        Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
1266            data_type_family(&cast.to)
1267        }
1268        Expression::Alias(alias) => {
1269            infer_expression_type_family_fallback(&alias.this, schema_map, context)
1270        }
1271        Expression::Neg(unary) => {
1272            infer_expression_type_family_fallback(&unary.this, schema_map, context)
1273        }
1274        Expression::Add(op) | Expression::Sub(op) | Expression::Mul(op) => {
1275            let left = infer_expression_type_family_fallback(&op.left, schema_map, context);
1276            let right = infer_expression_type_family_fallback(&op.right, schema_map, context);
1277            if left == TypeFamily::Unknown || right == TypeFamily::Unknown {
1278                TypeFamily::Unknown
1279            } else if left == TypeFamily::Integer && right == TypeFamily::Integer {
1280                TypeFamily::Integer
1281            } else if left.is_numeric() && right.is_numeric() {
1282                TypeFamily::Numeric
1283            } else if left.is_temporal() || right.is_temporal() {
1284                left
1285            } else {
1286                TypeFamily::Unknown
1287            }
1288        }
1289        Expression::Div(_) | Expression::Mod(_) => TypeFamily::Numeric,
1290        Expression::Concat(_) => TypeFamily::String,
1291        Expression::Eq(_)
1292        | Expression::Neq(_)
1293        | Expression::Lt(_)
1294        | Expression::Lte(_)
1295        | Expression::Gt(_)
1296        | Expression::Gte(_)
1297        | Expression::Like(_)
1298        | Expression::ILike(_)
1299        | Expression::And(_)
1300        | Expression::Or(_)
1301        | Expression::Not(_)
1302        | Expression::Between(_)
1303        | Expression::In(_)
1304        | Expression::IsNull(_)
1305        | Expression::IsTrue(_)
1306        | Expression::IsFalse(_)
1307        | Expression::Is(_) => TypeFamily::Boolean,
1308        Expression::Length(_) => TypeFamily::Integer,
1309        Expression::Upper(_)
1310        | Expression::Lower(_)
1311        | Expression::Trim(_)
1312        | Expression::LTrim(_)
1313        | Expression::RTrim(_)
1314        | Expression::Replace(_)
1315        | Expression::Substring(_)
1316        | Expression::Left(_)
1317        | Expression::Right(_)
1318        | Expression::Repeat(_)
1319        | Expression::Lpad(_)
1320        | Expression::Rpad(_)
1321        | Expression::ConcatWs(_) => TypeFamily::String,
1322        Expression::Abs(_)
1323        | Expression::Round(_)
1324        | Expression::Floor(_)
1325        | Expression::Ceil(_)
1326        | Expression::Power(_)
1327        | Expression::Sqrt(_)
1328        | Expression::Cbrt(_)
1329        | Expression::Ln(_)
1330        | Expression::Log(_)
1331        | Expression::Exp(_) => TypeFamily::Numeric,
1332        Expression::DateAdd(_) | Expression::DateSub(_) | Expression::ToDate(_) => TypeFamily::Date,
1333        Expression::ToTimestamp(_) => TypeFamily::Timestamp,
1334        Expression::DateDiff(_) | Expression::Extract(_) => TypeFamily::Integer,
1335        Expression::CurrentDate(_) => TypeFamily::Date,
1336        Expression::CurrentTime(_) => TypeFamily::Time,
1337        Expression::CurrentTimestamp(_) | Expression::CurrentTimestampLTZ(_) => {
1338            TypeFamily::Timestamp
1339        }
1340        Expression::Interval(_) => TypeFamily::Interval,
1341        _ => TypeFamily::Unknown,
1342    }
1343}
1344
1345fn are_comparable(left: TypeFamily, right: TypeFamily) -> bool {
1346    if left == TypeFamily::Unknown || right == TypeFamily::Unknown {
1347        return true;
1348    }
1349    if left == right {
1350        return true;
1351    }
1352    if left.is_numeric() && right.is_numeric() {
1353        return true;
1354    }
1355    if left.is_temporal() && right.is_temporal() {
1356        return true;
1357    }
1358    false
1359}
1360
1361fn check_function_argument(
1362    errors: &mut Vec<ValidationError>,
1363    strict: bool,
1364    function_name: &str,
1365    arg_index: usize,
1366    family: TypeFamily,
1367    expected: &str,
1368    valid: bool,
1369) {
1370    if family == TypeFamily::Unknown || valid {
1371        return;
1372    }
1373
1374    errors.push(type_issue(
1375        strict,
1376        validation_codes::E_INVALID_FUNCTION_ARGUMENT_TYPE,
1377        validation_codes::W_FUNCTION_ARGUMENT_COERCION,
1378        format!(
1379            "Function '{}' argument {} expects {}, found {}",
1380            function_name,
1381            arg_index + 1,
1382            expected,
1383            type_family_name(family)
1384        ),
1385    ));
1386}
1387
1388fn function_dispatch_name(name: &str) -> String {
1389    let upper = name
1390        .rsplit('.')
1391        .next()
1392        .unwrap_or(name)
1393        .trim()
1394        .to_uppercase();
1395    lower(canonical_typed_function_name_upper(&upper))
1396}
1397
1398fn function_base_name(name: &str) -> &str {
1399    name.rsplit('.').next().unwrap_or(name).trim()
1400}
1401
1402fn check_generic_function(
1403    function: &Function,
1404    schema_map: &HashMap<String, TableSchemaEntry>,
1405    context: &TypeCheckContext,
1406    strict: bool,
1407    errors: &mut Vec<ValidationError>,
1408) {
1409    let name = function_dispatch_name(&function.name);
1410
1411    let arg_family = |index: usize| -> Option<TypeFamily> {
1412        function
1413            .args
1414            .get(index)
1415            .map(|arg| infer_expression_type_family(arg, schema_map, context))
1416    };
1417
1418    match name.as_str() {
1419        "abs" | "sqrt" | "cbrt" | "ln" | "exp" => {
1420            if let Some(family) = arg_family(0) {
1421                check_function_argument(
1422                    errors,
1423                    strict,
1424                    &name,
1425                    0,
1426                    family,
1427                    "a numeric argument",
1428                    family.is_numeric(),
1429                );
1430            }
1431        }
1432        "round" | "floor" | "ceil" | "ceiling" => {
1433            if let Some(family) = arg_family(0) {
1434                check_function_argument(
1435                    errors,
1436                    strict,
1437                    &name,
1438                    0,
1439                    family,
1440                    "a numeric argument",
1441                    family.is_numeric(),
1442                );
1443            }
1444            if let Some(family) = arg_family(1) {
1445                check_function_argument(
1446                    errors,
1447                    strict,
1448                    &name,
1449                    1,
1450                    family,
1451                    "a numeric argument",
1452                    family.is_numeric(),
1453                );
1454            }
1455        }
1456        "power" | "pow" => {
1457            for i in [0_usize, 1_usize] {
1458                if let Some(family) = arg_family(i) {
1459                    check_function_argument(
1460                        errors,
1461                        strict,
1462                        &name,
1463                        i,
1464                        family,
1465                        "a numeric argument",
1466                        family.is_numeric(),
1467                    );
1468                }
1469            }
1470        }
1471        "length" | "char_length" | "character_length" => {
1472            if let Some(family) = arg_family(0) {
1473                check_function_argument(
1474                    errors,
1475                    strict,
1476                    &name,
1477                    0,
1478                    family,
1479                    "a string or binary argument",
1480                    is_string_or_binary(family),
1481                );
1482            }
1483        }
1484        "upper" | "lower" | "trim" | "ltrim" | "rtrim" | "reverse" => {
1485            if let Some(family) = arg_family(0) {
1486                check_function_argument(
1487                    errors,
1488                    strict,
1489                    &name,
1490                    0,
1491                    family,
1492                    "a string argument",
1493                    is_string_like(family),
1494                );
1495            }
1496        }
1497        "substring" | "substr" => {
1498            if let Some(family) = arg_family(0) {
1499                check_function_argument(
1500                    errors,
1501                    strict,
1502                    &name,
1503                    0,
1504                    family,
1505                    "a string argument",
1506                    is_string_like(family),
1507                );
1508            }
1509            if let Some(family) = arg_family(1) {
1510                check_function_argument(
1511                    errors,
1512                    strict,
1513                    &name,
1514                    1,
1515                    family,
1516                    "a numeric argument",
1517                    family.is_numeric(),
1518                );
1519            }
1520            if let Some(family) = arg_family(2) {
1521                check_function_argument(
1522                    errors,
1523                    strict,
1524                    &name,
1525                    2,
1526                    family,
1527                    "a numeric argument",
1528                    family.is_numeric(),
1529                );
1530            }
1531        }
1532        "replace" => {
1533            for i in [0_usize, 1_usize, 2_usize] {
1534                if let Some(family) = arg_family(i) {
1535                    check_function_argument(
1536                        errors,
1537                        strict,
1538                        &name,
1539                        i,
1540                        family,
1541                        "a string argument",
1542                        is_string_like(family),
1543                    );
1544                }
1545            }
1546        }
1547        "left" | "right" | "repeat" | "lpad" | "rpad" => {
1548            if let Some(family) = arg_family(0) {
1549                check_function_argument(
1550                    errors,
1551                    strict,
1552                    &name,
1553                    0,
1554                    family,
1555                    "a string argument",
1556                    is_string_like(family),
1557                );
1558            }
1559            if let Some(family) = arg_family(1) {
1560                check_function_argument(
1561                    errors,
1562                    strict,
1563                    &name,
1564                    1,
1565                    family,
1566                    "a numeric argument",
1567                    family.is_numeric(),
1568                );
1569            }
1570            if (name == "lpad" || name == "rpad") && function.args.len() > 2 {
1571                if let Some(family) = arg_family(2) {
1572                    check_function_argument(
1573                        errors,
1574                        strict,
1575                        &name,
1576                        2,
1577                        family,
1578                        "a string argument",
1579                        is_string_like(family),
1580                    );
1581                }
1582            }
1583        }
1584        _ => {}
1585    }
1586}
1587
1588fn check_function_catalog(
1589    function: &Function,
1590    dialect: DialectType,
1591    function_catalog: Option<&dyn FunctionCatalog>,
1592    strict: bool,
1593    errors: &mut Vec<ValidationError>,
1594) {
1595    let Some(catalog) = function_catalog else {
1596        return;
1597    };
1598
1599    let raw_name = function_base_name(&function.name);
1600    let normalized_name = function_dispatch_name(&function.name);
1601    let arity = function.args.len();
1602    let Some(signatures) = catalog.lookup(dialect, raw_name, &normalized_name) else {
1603        errors.push(if strict {
1604            ValidationError::error(
1605                format!(
1606                    "Unknown function '{}' for dialect {:?}",
1607                    function.name, dialect
1608                ),
1609                validation_codes::E_UNKNOWN_FUNCTION,
1610            )
1611        } else {
1612            ValidationError::warning(
1613                format!(
1614                    "Unknown function '{}' for dialect {:?}",
1615                    function.name, dialect
1616                ),
1617                validation_codes::E_UNKNOWN_FUNCTION,
1618            )
1619        });
1620        return;
1621    };
1622
1623    if signatures.iter().any(|sig| sig.matches_arity(arity)) {
1624        return;
1625    }
1626
1627    let expected = signatures
1628        .iter()
1629        .map(|sig| sig.describe_arity())
1630        .collect::<Vec<_>>()
1631        .join(", ");
1632    errors.push(if strict {
1633        ValidationError::error(
1634            format!(
1635                "Invalid arity for function '{}': got {}, expected {}",
1636                function.name, arity, expected
1637            ),
1638            validation_codes::E_INVALID_FUNCTION_ARITY,
1639        )
1640    } else {
1641        ValidationError::warning(
1642            format!(
1643                "Invalid arity for function '{}': got {}, expected {}",
1644                function.name, arity, expected
1645            ),
1646            validation_codes::E_INVALID_FUNCTION_ARITY,
1647        )
1648    });
1649}
1650
1651#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1652struct DeclaredRelationship {
1653    source_table: String,
1654    source_column: String,
1655    target_table: String,
1656    target_column: String,
1657}
1658
1659fn build_declared_relationships(
1660    schema: &ValidationSchema,
1661    schema_map: &HashMap<String, TableSchemaEntry>,
1662) -> Vec<DeclaredRelationship> {
1663    let mut relationships = HashSet::new();
1664
1665    for table in &schema.tables {
1666        let Some(source_key) =
1667            resolve_reference_table_key(&table.name, table.schema.as_deref(), None, schema_map)
1668        else {
1669            continue;
1670        };
1671
1672        for column in &table.columns {
1673            let Some(reference) = &column.references else {
1674                continue;
1675            };
1676            let Some(target_key) = resolve_reference_table_key(
1677                &reference.table,
1678                reference.schema.as_deref(),
1679                table.schema.as_deref(),
1680                schema_map,
1681            ) else {
1682                continue;
1683            };
1684
1685            relationships.insert(DeclaredRelationship {
1686                source_table: source_key.clone(),
1687                source_column: lower(&column.name),
1688                target_table: target_key,
1689                target_column: lower(&reference.column),
1690            });
1691        }
1692
1693        for foreign_key in &table.foreign_keys {
1694            if foreign_key.columns.len() != foreign_key.references.columns.len() {
1695                continue;
1696            }
1697            let Some(target_key) = resolve_reference_table_key(
1698                &foreign_key.references.table,
1699                foreign_key.references.schema.as_deref(),
1700                table.schema.as_deref(),
1701                schema_map,
1702            ) else {
1703                continue;
1704            };
1705
1706            for (source_col, target_col) in foreign_key
1707                .columns
1708                .iter()
1709                .zip(foreign_key.references.columns.iter())
1710            {
1711                relationships.insert(DeclaredRelationship {
1712                    source_table: source_key.clone(),
1713                    source_column: lower(source_col),
1714                    target_table: target_key.clone(),
1715                    target_column: lower(target_col),
1716                });
1717            }
1718        }
1719    }
1720
1721    relationships.into_iter().collect()
1722}
1723
1724fn resolve_column_binding(
1725    column: &Column,
1726    schema_map: &HashMap<String, TableSchemaEntry>,
1727    context: &TypeCheckContext,
1728    resolver: &mut Resolver<'_>,
1729) -> Option<(String, String)> {
1730    let column_name = lower(&column.name.name);
1731    if column_name.is_empty() {
1732        return None;
1733    }
1734
1735    if let Some(table) = &column.table {
1736        let mut table_key = lower(&table.name);
1737        if let Some(mapped) = context.table_aliases.get(&table_key) {
1738            table_key = mapped.clone();
1739        }
1740        if schema_map.contains_key(&table_key) {
1741            return Some((table_key, column_name));
1742        }
1743        return None;
1744    }
1745
1746    if let Some(resolved_source) = resolver.get_table(&column_name) {
1747        let mut table_key = lower(&resolved_source);
1748        if let Some(mapped) = context.table_aliases.get(&table_key) {
1749            table_key = mapped.clone();
1750        }
1751        if schema_map.contains_key(&table_key) {
1752            return Some((table_key, column_name));
1753        }
1754    }
1755
1756    let candidates: Vec<String> = context
1757        .referenced_tables
1758        .iter()
1759        .filter_map(|table_name| {
1760            schema_map
1761                .get(table_name)
1762                .filter(|entry| entry.columns.contains_key(&column_name))
1763                .map(|_| table_name.clone())
1764        })
1765        .collect();
1766    if candidates.len() == 1 {
1767        return Some((candidates[0].clone(), column_name));
1768    }
1769    None
1770}
1771
1772fn extract_join_equality_pairs(
1773    expr: &Expression,
1774    schema_map: &HashMap<String, TableSchemaEntry>,
1775    context: &TypeCheckContext,
1776    resolver: &mut Resolver<'_>,
1777    pairs: &mut Vec<((String, String), (String, String))>,
1778) {
1779    match expr {
1780        Expression::And(op) => {
1781            extract_join_equality_pairs(&op.left, schema_map, context, resolver, pairs);
1782            extract_join_equality_pairs(&op.right, schema_map, context, resolver, pairs);
1783        }
1784        Expression::Paren(paren) => {
1785            extract_join_equality_pairs(&paren.this, schema_map, context, resolver, pairs);
1786        }
1787        Expression::Eq(op) => {
1788            let (Expression::Column(left_col), Expression::Column(right_col)) =
1789                (&op.left, &op.right)
1790            else {
1791                return;
1792            };
1793            let Some(left) = resolve_column_binding(left_col, schema_map, context, resolver) else {
1794                return;
1795            };
1796            let Some(right) = resolve_column_binding(right_col, schema_map, context, resolver)
1797            else {
1798                return;
1799            };
1800            pairs.push((left, right));
1801        }
1802        _ => {}
1803    }
1804}
1805
1806fn relationship_matches_pair(
1807    relationship: &DeclaredRelationship,
1808    left_table: &str,
1809    left_column: &str,
1810    right_table: &str,
1811    right_column: &str,
1812) -> bool {
1813    (relationship.source_table == left_table
1814        && relationship.source_column == left_column
1815        && relationship.target_table == right_table
1816        && relationship.target_column == right_column)
1817        || (relationship.source_table == right_table
1818            && relationship.source_column == right_column
1819            && relationship.target_table == left_table
1820            && relationship.target_column == left_column)
1821}
1822
1823fn resolved_table_key_from_expr(
1824    expr: &Expression,
1825    schema_map: &HashMap<String, TableSchemaEntry>,
1826) -> Option<String> {
1827    match expr {
1828        Expression::Table(table) => resolve_table_schema_entry(table, schema_map).map(|(k, _)| k),
1829        Expression::Alias(alias) => resolved_table_key_from_expr(&alias.this, schema_map),
1830        _ => None,
1831    }
1832}
1833
1834fn select_from_table_keys(
1835    select: &crate::expressions::Select,
1836    schema_map: &HashMap<String, TableSchemaEntry>,
1837) -> HashSet<String> {
1838    let mut keys = HashSet::new();
1839    if let Some(from_clause) = &select.from {
1840        for expr in &from_clause.expressions {
1841            if let Some(key) = resolved_table_key_from_expr(expr, schema_map) {
1842                keys.insert(key);
1843            }
1844        }
1845    }
1846    keys
1847}
1848
1849fn is_natural_or_implied_join(kind: JoinKind) -> bool {
1850    matches!(
1851        kind,
1852        JoinKind::Natural
1853            | JoinKind::NaturalLeft
1854            | JoinKind::NaturalRight
1855            | JoinKind::NaturalFull
1856            | JoinKind::CrossApply
1857            | JoinKind::OuterApply
1858            | JoinKind::AsOf
1859            | JoinKind::AsOfLeft
1860            | JoinKind::AsOfRight
1861            | JoinKind::Lateral
1862            | JoinKind::LeftLateral
1863    )
1864}
1865
1866fn check_query_reference_quality(
1867    stmt: &Expression,
1868    schema_map: &HashMap<String, TableSchemaEntry>,
1869    resolver_schema: &MappingSchema,
1870    relationships: &[DeclaredRelationship],
1871) -> Vec<ValidationError> {
1872    let mut errors = Vec::new();
1873
1874    for node in stmt.dfs() {
1875        let Expression::Select(select) = node else {
1876            continue;
1877        };
1878
1879        let select_expr = Expression::Select(select.clone());
1880        let context = collect_type_check_context(&select_expr, schema_map);
1881        let scope = build_scope(&select_expr);
1882        let mut resolver = Resolver::new(&scope, resolver_schema, true);
1883
1884        let mut cumulative_left_tables = select_from_table_keys(select, schema_map);
1885
1886        for join in &select.joins {
1887            let right_table_key = resolved_table_key_from_expr(&join.this, schema_map);
1888            let has_explicit_condition = join.on.is_some() || !join.using.is_empty();
1889            let cartesian_like_kind = matches!(
1890                join.kind,
1891                JoinKind::Cross
1892                    | JoinKind::Implicit
1893                    | JoinKind::Array
1894                    | JoinKind::LeftArray
1895                    | JoinKind::Paste
1896            );
1897
1898            if right_table_key.is_some()
1899                && (cartesian_like_kind
1900                    || (!has_explicit_condition && !is_natural_or_implied_join(join.kind)))
1901            {
1902                errors.push(ValidationError::warning(
1903                    "Potential cartesian join: JOIN without ON/USING condition",
1904                    validation_codes::W_CARTESIAN_JOIN,
1905                ));
1906            }
1907
1908            if let (Some(on_expr), Some(right_key)) = (&join.on, right_table_key.clone()) {
1909                if join.using.is_empty() {
1910                    let mut eq_pairs = Vec::new();
1911                    extract_join_equality_pairs(
1912                        on_expr,
1913                        schema_map,
1914                        &context,
1915                        &mut resolver,
1916                        &mut eq_pairs,
1917                    );
1918
1919                    let relevant_relationships: Vec<&DeclaredRelationship> = relationships
1920                        .iter()
1921                        .filter(|rel| {
1922                            cumulative_left_tables.contains(&rel.source_table)
1923                                && rel.target_table == right_key
1924                                || (cumulative_left_tables.contains(&rel.target_table)
1925                                    && rel.source_table == right_key)
1926                        })
1927                        .collect();
1928
1929                    if !relevant_relationships.is_empty() {
1930                        let uses_declared_fk = eq_pairs.iter().any(|((lt, lc), (rt, rc))| {
1931                            relevant_relationships
1932                                .iter()
1933                                .any(|rel| relationship_matches_pair(rel, lt, lc, rt, rc))
1934                        });
1935                        if !uses_declared_fk {
1936                            errors.push(ValidationError::warning(
1937                                "JOIN predicate does not use declared foreign-key relationship columns",
1938                                validation_codes::W_JOIN_NOT_USING_DECLARED_REFERENCE,
1939                            ));
1940                        }
1941                    }
1942                }
1943            }
1944
1945            if let Some(right_key) = right_table_key {
1946                cumulative_left_tables.insert(right_key);
1947            }
1948        }
1949    }
1950
1951    errors
1952}
1953
1954fn are_setop_compatible(left: TypeFamily, right: TypeFamily) -> bool {
1955    if left == TypeFamily::Unknown || right == TypeFamily::Unknown {
1956        return true;
1957    }
1958    if left == right {
1959        return true;
1960    }
1961    if left.is_numeric() && right.is_numeric() {
1962        return true;
1963    }
1964    if left.is_temporal() && right.is_temporal() {
1965        return true;
1966    }
1967    false
1968}
1969
1970fn merged_setop_family(left: TypeFamily, right: TypeFamily) -> TypeFamily {
1971    if left == TypeFamily::Unknown {
1972        return right;
1973    }
1974    if right == TypeFamily::Unknown {
1975        return left;
1976    }
1977    if left == right {
1978        return left;
1979    }
1980    if left.is_numeric() && right.is_numeric() {
1981        if left == TypeFamily::Numeric || right == TypeFamily::Numeric {
1982            return TypeFamily::Numeric;
1983        }
1984        return TypeFamily::Integer;
1985    }
1986    if left.is_temporal() && right.is_temporal() {
1987        if left == TypeFamily::Timestamp || right == TypeFamily::Timestamp {
1988            return TypeFamily::Timestamp;
1989        }
1990        if left == TypeFamily::Date || right == TypeFamily::Date {
1991            return TypeFamily::Date;
1992        }
1993        return TypeFamily::Time;
1994    }
1995    TypeFamily::Unknown
1996}
1997
1998fn are_assignment_compatible(target: TypeFamily, source: TypeFamily) -> bool {
1999    if target == TypeFamily::Unknown || source == TypeFamily::Unknown {
2000        return true;
2001    }
2002    if target == source {
2003        return true;
2004    }
2005
2006    match target {
2007        TypeFamily::Boolean => source == TypeFamily::Boolean,
2008        TypeFamily::Integer | TypeFamily::Numeric => source.is_numeric(),
2009        TypeFamily::Date | TypeFamily::Time | TypeFamily::Timestamp | TypeFamily::Interval => {
2010            source.is_temporal()
2011        }
2012        TypeFamily::String => true,
2013        TypeFamily::Binary => matches!(source, TypeFamily::Binary | TypeFamily::String),
2014        TypeFamily::Json => matches!(source, TypeFamily::Json | TypeFamily::String),
2015        TypeFamily::Uuid => matches!(source, TypeFamily::Uuid | TypeFamily::String),
2016        TypeFamily::Array => source == TypeFamily::Array,
2017        TypeFamily::Map => source == TypeFamily::Map,
2018        TypeFamily::Struct => source == TypeFamily::Struct,
2019        TypeFamily::Unknown => true,
2020    }
2021}
2022
2023fn projection_families(
2024    query: &Expression,
2025    schema_map: &HashMap<String, TableSchemaEntry>,
2026    dialect: DialectType,
2027) -> Option<Vec<TypeFamily>> {
2028    let pair = match query {
2029        Expression::Union(query) => Some((&query.left, &query.right)),
2030        Expression::Intersect(query) => Some((&query.left, &query.right)),
2031        Expression::Except(query) => Some((&query.left, &query.right)),
2032        _ => None,
2033    };
2034    if let Some((left, right)) = pair {
2035        let left = projection_families(left, schema_map, dialect)?;
2036        let right = projection_families(right, schema_map, dialect)?;
2037        return match crate::set_operation::set_operation_layout(query, Some(dialect)).ok()? {
2038            Some(layout) => Some(
2039                layout
2040                    .outputs
2041                    .iter()
2042                    .map(|output| {
2043                        merged_setop_family(
2044                            output
2045                                .left_ordinal
2046                                .and_then(|i| left.get(i).copied())
2047                                .unwrap_or(TypeFamily::Unknown),
2048                            output
2049                                .right_ordinal
2050                                .and_then(|i| right.get(i).copied())
2051                                .unwrap_or(TypeFamily::Unknown),
2052                        )
2053                    })
2054                    .collect(),
2055            ),
2056            None if left.len() == right.len() => Some(
2057                left.into_iter()
2058                    .zip(right)
2059                    .map(|(l, r)| merged_setop_family(l, r))
2060                    .collect(),
2061            ),
2062            _ => None,
2063        };
2064    }
2065    match query {
2066        Expression::Select(select) => {
2067            if select
2068                .expressions
2069                .iter()
2070                .any(|e| matches!(e, Expression::Star(_) | Expression::BracedWildcard(_)))
2071            {
2072                return None;
2073            }
2074            let context = collect_type_check_context(query, schema_map);
2075            Some(
2076                select
2077                    .expressions
2078                    .iter()
2079                    .map(|e| infer_expression_type_family(e, schema_map, &context))
2080                    .collect(),
2081            )
2082        }
2083        Expression::Subquery(query) => projection_families(&query.this, schema_map, dialect),
2084        Expression::Paren(query) => projection_families(&query.this, schema_map, dialect),
2085        Expression::Values(values) => Some(
2086            values
2087                .expressions
2088                .first()?
2089                .expressions
2090                .iter()
2091                .map(|e| infer_expression_type_family(e, schema_map, &TypeCheckContext::default()))
2092                .collect(),
2093        ),
2094        _ => None,
2095    }
2096}
2097
2098fn check_set_operation_compatibility(
2099    query: &Expression,
2100    dialect: DialectType,
2101    op_name: &str,
2102    left_expr: &Expression,
2103    right_expr: &Expression,
2104    schema_map: &HashMap<String, TableSchemaEntry>,
2105    strict: bool,
2106    errors: &mut Vec<ValidationError>,
2107) {
2108    let Some(mut left_projection) = projection_families(left_expr, schema_map, dialect) else {
2109        return;
2110    };
2111    let Some(mut right_projection) = projection_families(right_expr, schema_map, dialect) else {
2112        return;
2113    };
2114
2115    match crate::set_operation::set_operation_layout(query, Some(dialect)) {
2116        Ok(Some(layout)) => {
2117            let left = left_projection;
2118            let right = right_projection;
2119            left_projection = layout
2120                .outputs
2121                .iter()
2122                .map(|output| {
2123                    output
2124                        .left_ordinal
2125                        .and_then(|i| left.get(i).copied())
2126                        .unwrap_or(TypeFamily::Unknown)
2127                })
2128                .collect();
2129            right_projection = layout
2130                .outputs
2131                .iter()
2132                .map(|output| {
2133                    output
2134                        .right_ordinal
2135                        .and_then(|i| right.get(i).copied())
2136                        .unwrap_or(TypeFamily::Unknown)
2137                })
2138                .collect();
2139        }
2140        Err(error) => {
2141            if !error.is_indeterminate() {
2142                errors.push(type_issue(
2143                    strict,
2144                    validation_codes::E_SETOP_ARITY_MISMATCH,
2145                    validation_codes::W_SETOP_IMPLICIT_COERCION,
2146                    error.to_string(),
2147                ));
2148            }
2149            return;
2150        }
2151        Ok(None) => {}
2152    }
2153    if left_projection.len() != right_projection.len() {
2154        errors.push(type_issue(
2155            strict,
2156            validation_codes::E_SETOP_ARITY_MISMATCH,
2157            validation_codes::W_SETOP_IMPLICIT_COERCION,
2158            format!(
2159                "{} operands return different column counts: left {}, right {}",
2160                op_name,
2161                left_projection.len(),
2162                right_projection.len()
2163            ),
2164        ));
2165        return;
2166    }
2167
2168    for (idx, (left, right)) in left_projection
2169        .into_iter()
2170        .zip(right_projection)
2171        .enumerate()
2172    {
2173        if !are_setop_compatible(left, right) {
2174            errors.push(type_issue(
2175                strict,
2176                validation_codes::E_SETOP_TYPE_MISMATCH,
2177                validation_codes::W_SETOP_IMPLICIT_COERCION,
2178                format!(
2179                    "{} column {} has incompatible types: {} vs {}",
2180                    op_name,
2181                    idx + 1,
2182                    type_family_name(left),
2183                    type_family_name(right)
2184                ),
2185            ));
2186        }
2187    }
2188}
2189
2190fn check_insert_assignments(
2191    dialect: DialectType,
2192    stmt: &Expression,
2193    insert: &Insert,
2194    schema_map: &HashMap<String, TableSchemaEntry>,
2195    strict: bool,
2196    errors: &mut Vec<ValidationError>,
2197) {
2198    let Some((target_table_key, table_schema)) =
2199        resolve_table_schema_entry(&insert.table, schema_map)
2200    else {
2201        return;
2202    };
2203
2204    let mut target_columns = Vec::new();
2205    if insert.columns.is_empty() {
2206        target_columns.extend(table_schema.column_order.iter().cloned());
2207    } else {
2208        for column in &insert.columns {
2209            let col_name = lower(&column.name);
2210            if table_schema.columns.contains_key(&col_name) {
2211                target_columns.push(col_name);
2212            } else {
2213                errors.push(if strict {
2214                    ValidationError::error(
2215                        format!(
2216                            "Unknown column '{}' in table '{}'",
2217                            column.name, target_table_key
2218                        ),
2219                        validation_codes::E_UNKNOWN_COLUMN,
2220                    )
2221                } else {
2222                    ValidationError::warning(
2223                        format!(
2224                            "Unknown column '{}' in table '{}'",
2225                            column.name, target_table_key
2226                        ),
2227                        validation_codes::E_UNKNOWN_COLUMN,
2228                    )
2229                });
2230            }
2231        }
2232    }
2233
2234    if target_columns.is_empty() {
2235        return;
2236    }
2237
2238    let context = collect_type_check_context(stmt, schema_map);
2239
2240    if !insert.default_values {
2241        for (row_idx, row) in insert.values.iter().enumerate() {
2242            if row.len() != target_columns.len() {
2243                errors.push(type_issue(
2244                    strict,
2245                    validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2246                    validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2247                    format!(
2248                        "INSERT row {} has {} values but target has {} columns",
2249                        row_idx + 1,
2250                        row.len(),
2251                        target_columns.len()
2252                    ),
2253                ));
2254                continue;
2255            }
2256
2257            for (value, target_column) in row.iter().zip(target_columns.iter()) {
2258                let Some(target_family) = table_schema.columns.get(target_column).copied() else {
2259                    continue;
2260                };
2261                let source_family = infer_expression_type_family(value, schema_map, &context);
2262                if !are_assignment_compatible(target_family, source_family) {
2263                    errors.push(type_issue(
2264                        strict,
2265                        validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2266                        validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2267                        format!(
2268                            "INSERT assignment type mismatch for '{}.{}': expected {}, found {}",
2269                            target_table_key,
2270                            target_column,
2271                            type_family_name(target_family),
2272                            type_family_name(source_family)
2273                        ),
2274                    ));
2275                }
2276            }
2277        }
2278    }
2279
2280    if let Some(query) = &insert.query {
2281        // DuckDB BY NAME maps source columns by name, not position.
2282        if insert.by_name {
2283            return;
2284        }
2285
2286        let Some(source_projection) = projection_families(query, schema_map, dialect) else {
2287            return;
2288        };
2289
2290        if source_projection.len() != target_columns.len() {
2291            errors.push(type_issue(
2292                strict,
2293                validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2294                validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2295                format!(
2296                    "INSERT source query has {} columns but target has {} columns",
2297                    source_projection.len(),
2298                    target_columns.len()
2299                ),
2300            ));
2301            return;
2302        }
2303
2304        for (source_family, target_column) in
2305            source_projection.into_iter().zip(target_columns.iter())
2306        {
2307            let Some(target_family) = table_schema.columns.get(target_column).copied() else {
2308                continue;
2309            };
2310            if !are_assignment_compatible(target_family, source_family) {
2311                errors.push(type_issue(
2312                    strict,
2313                    validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2314                    validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2315                    format!(
2316                        "INSERT assignment type mismatch for '{}.{}': expected {}, found {}",
2317                        target_table_key,
2318                        target_column,
2319                        type_family_name(target_family),
2320                        type_family_name(source_family)
2321                    ),
2322                ));
2323            }
2324        }
2325    }
2326}
2327
2328fn check_update_assignments(
2329    stmt: &Expression,
2330    update: &Update,
2331    schema_map: &HashMap<String, TableSchemaEntry>,
2332    strict: bool,
2333    errors: &mut Vec<ValidationError>,
2334) {
2335    let Some((target_table_key, table_schema)) =
2336        resolve_table_schema_entry(&update.table, schema_map)
2337    else {
2338        return;
2339    };
2340
2341    let context = collect_type_check_context(stmt, schema_map);
2342
2343    for (column, value) in &update.set {
2344        let col_name = lower(&column.name);
2345        let Some(target_family) = table_schema.columns.get(&col_name).copied() else {
2346            errors.push(if strict {
2347                ValidationError::error(
2348                    format!(
2349                        "Unknown column '{}' in table '{}'",
2350                        column.name, target_table_key
2351                    ),
2352                    validation_codes::E_UNKNOWN_COLUMN,
2353                )
2354            } else {
2355                ValidationError::warning(
2356                    format!(
2357                        "Unknown column '{}' in table '{}'",
2358                        column.name, target_table_key
2359                    ),
2360                    validation_codes::E_UNKNOWN_COLUMN,
2361                )
2362            });
2363            continue;
2364        };
2365
2366        let source_family = infer_expression_type_family(value, schema_map, &context);
2367        if !are_assignment_compatible(target_family, source_family) {
2368            errors.push(type_issue(
2369                strict,
2370                validation_codes::E_INVALID_ASSIGNMENT_TYPE,
2371                validation_codes::W_IMPLICIT_CAST_ASSIGNMENT,
2372                format!(
2373                    "UPDATE assignment type mismatch for '{}.{}': expected {}, found {}",
2374                    target_table_key,
2375                    col_name,
2376                    type_family_name(target_family),
2377                    type_family_name(source_family)
2378                ),
2379            ));
2380        }
2381    }
2382}
2383
2384fn check_types(
2385    stmt: &Expression,
2386    dialect: DialectType,
2387    schema_map: &HashMap<String, TableSchemaEntry>,
2388    function_catalog: Option<&dyn FunctionCatalog>,
2389    strict: bool,
2390) -> Vec<ValidationError> {
2391    let mut errors = Vec::new();
2392    let context = collect_type_check_context(stmt, schema_map);
2393
2394    for node in stmt.dfs() {
2395        for (clause, predicate) in crate::binding::predicates(node) {
2396            let family = infer_expression_type_family(predicate, schema_map, &context);
2397            if !predicate_compatible(family, dialect) {
2398                errors.push(type_issue(
2399                    strict,
2400                    validation_codes::E_INVALID_PREDICATE_TYPE,
2401                    validation_codes::W_PREDICATE_NULLABILITY,
2402                    format!(
2403                        "{clause} expects a boolean predicate, found {}",
2404                        type_family_name(family)
2405                    ),
2406                ));
2407            }
2408        }
2409        match node {
2410            Expression::Insert(insert) => {
2411                check_insert_assignments(dialect, stmt, insert, schema_map, strict, &mut errors);
2412            }
2413            Expression::Update(update) => {
2414                check_update_assignments(stmt, update, schema_map, strict, &mut errors);
2415            }
2416            Expression::Union(union) => {
2417                check_set_operation_compatibility(
2418                    node,
2419                    dialect,
2420                    "UNION",
2421                    &union.left,
2422                    &union.right,
2423                    schema_map,
2424                    strict,
2425                    &mut errors,
2426                );
2427            }
2428            Expression::Intersect(intersect) => {
2429                check_set_operation_compatibility(
2430                    node,
2431                    dialect,
2432                    "INTERSECT",
2433                    &intersect.left,
2434                    &intersect.right,
2435                    schema_map,
2436                    strict,
2437                    &mut errors,
2438                );
2439            }
2440            Expression::Except(except) => {
2441                check_set_operation_compatibility(
2442                    node,
2443                    dialect,
2444                    "EXCEPT",
2445                    &except.left,
2446                    &except.right,
2447                    schema_map,
2448                    strict,
2449                    &mut errors,
2450                );
2451            }
2452            Expression::And(op) | Expression::Or(op) => {
2453                for (side, expr) in [("left", &op.left), ("right", &op.right)] {
2454                    let family = infer_expression_type_family(expr, schema_map, &context);
2455                    if !predicate_compatible(family, dialect) {
2456                        errors.push(type_issue(
2457                            strict,
2458                            validation_codes::E_INVALID_PREDICATE_TYPE,
2459                            validation_codes::W_PREDICATE_NULLABILITY,
2460                            format!(
2461                                "Logical {} operand expects boolean, found {}",
2462                                side,
2463                                type_family_name(family)
2464                            ),
2465                        ));
2466                    }
2467                }
2468            }
2469            Expression::Not(unary) => {
2470                let family = infer_expression_type_family(&unary.this, schema_map, &context);
2471                if !predicate_compatible(family, dialect) {
2472                    errors.push(type_issue(
2473                        strict,
2474                        validation_codes::E_INVALID_PREDICATE_TYPE,
2475                        validation_codes::W_PREDICATE_NULLABILITY,
2476                        format!("NOT expects boolean, found {}", type_family_name(family)),
2477                    ));
2478                }
2479            }
2480            Expression::Eq(op)
2481            | Expression::Neq(op)
2482            | Expression::Lt(op)
2483            | Expression::Lte(op)
2484            | Expression::Gt(op)
2485            | Expression::Gte(op) => {
2486                let left = infer_expression_type_family(&op.left, schema_map, &context);
2487                let right = infer_expression_type_family(&op.right, schema_map, &context);
2488                if !are_comparable(left, right) {
2489                    errors.push(type_issue(
2490                        strict,
2491                        validation_codes::E_INCOMPATIBLE_COMPARISON_TYPES,
2492                        validation_codes::W_IMPLICIT_CAST_COMPARISON,
2493                        format!(
2494                            "Incompatible comparison between {} and {}",
2495                            type_family_name(left),
2496                            type_family_name(right)
2497                        ),
2498                    ));
2499                }
2500            }
2501            Expression::Like(op) | Expression::ILike(op) => {
2502                let left = infer_expression_type_family(&op.left, schema_map, &context);
2503                let right = infer_expression_type_family(&op.right, schema_map, &context);
2504                if left != TypeFamily::Unknown
2505                    && right != TypeFamily::Unknown
2506                    && (!is_string_like(left) || !is_string_like(right))
2507                {
2508                    errors.push(type_issue(
2509                        strict,
2510                        validation_codes::E_INCOMPATIBLE_COMPARISON_TYPES,
2511                        validation_codes::W_IMPLICIT_CAST_COMPARISON,
2512                        format!(
2513                            "LIKE/ILIKE expects string operands, found {} and {}",
2514                            type_family_name(left),
2515                            type_family_name(right)
2516                        ),
2517                    ));
2518                }
2519            }
2520            Expression::Between(between) => {
2521                let this_family = infer_expression_type_family(&between.this, schema_map, &context);
2522                let low_family = infer_expression_type_family(&between.low, schema_map, &context);
2523                let high_family = infer_expression_type_family(&between.high, schema_map, &context);
2524
2525                if !are_comparable(this_family, low_family)
2526                    || !are_comparable(this_family, high_family)
2527                {
2528                    errors.push(type_issue(
2529                        strict,
2530                        validation_codes::E_INCOMPATIBLE_COMPARISON_TYPES,
2531                        validation_codes::W_IMPLICIT_CAST_COMPARISON,
2532                        format!(
2533                            "BETWEEN bounds are incompatible with {} (found {} and {})",
2534                            type_family_name(this_family),
2535                            type_family_name(low_family),
2536                            type_family_name(high_family)
2537                        ),
2538                    ));
2539                }
2540            }
2541            Expression::In(in_expr) => {
2542                let this_family = infer_expression_type_family(&in_expr.this, schema_map, &context);
2543                for value in &in_expr.expressions {
2544                    let item_family = infer_expression_type_family(value, schema_map, &context);
2545                    if !are_comparable(this_family, item_family) {
2546                        errors.push(type_issue(
2547                            strict,
2548                            validation_codes::E_INCOMPATIBLE_COMPARISON_TYPES,
2549                            validation_codes::W_IMPLICIT_CAST_COMPARISON,
2550                            format!(
2551                                "IN item type {} is incompatible with {}",
2552                                type_family_name(item_family),
2553                                type_family_name(this_family)
2554                            ),
2555                        ));
2556                        break;
2557                    }
2558                }
2559            }
2560            Expression::Add(op)
2561            | Expression::Sub(op)
2562            | Expression::Mul(op)
2563            | Expression::Div(op)
2564            | Expression::Mod(op) => {
2565                let left = infer_expression_type_family(&op.left, schema_map, &context);
2566                let right = infer_expression_type_family(&op.right, schema_map, &context);
2567
2568                if left == TypeFamily::Unknown || right == TypeFamily::Unknown {
2569                    continue;
2570                }
2571
2572                let temporal_ok = matches!(node, Expression::Add(_) | Expression::Sub(_))
2573                    && ((left.is_temporal() && right.is_numeric())
2574                        || (right.is_temporal() && left.is_numeric())
2575                        || (matches!(node, Expression::Sub(_))
2576                            && left.is_temporal()
2577                            && right.is_temporal()));
2578
2579                if !(left.is_numeric() && right.is_numeric()) && !temporal_ok {
2580                    errors.push(type_issue(
2581                        strict,
2582                        validation_codes::E_INVALID_ARITHMETIC_TYPE,
2583                        validation_codes::W_IMPLICIT_CAST_ARITHMETIC,
2584                        format!(
2585                            "Arithmetic operation expects numeric-compatible operands, found {} and {}",
2586                            type_family_name(left),
2587                            type_family_name(right)
2588                        ),
2589                    ));
2590                }
2591            }
2592            Expression::Function(function) => {
2593                check_function_catalog(function, dialect, function_catalog, strict, &mut errors);
2594                check_generic_function(function, schema_map, &context, strict, &mut errors);
2595            }
2596            Expression::Upper(func)
2597            | Expression::Lower(func)
2598            | Expression::LTrim(func)
2599            | Expression::RTrim(func)
2600            | Expression::Reverse(func) => {
2601                let family = infer_expression_type_family(&func.this, schema_map, &context);
2602                check_function_argument(
2603                    &mut errors,
2604                    strict,
2605                    "string_function",
2606                    0,
2607                    family,
2608                    "a string argument",
2609                    is_string_like(family),
2610                );
2611            }
2612            Expression::Length(func) => {
2613                let family = infer_expression_type_family(&func.this, schema_map, &context);
2614                check_function_argument(
2615                    &mut errors,
2616                    strict,
2617                    "length",
2618                    0,
2619                    family,
2620                    "a string or binary argument",
2621                    is_string_or_binary(family),
2622                );
2623            }
2624            Expression::Trim(func) => {
2625                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2626                check_function_argument(
2627                    &mut errors,
2628                    strict,
2629                    "trim",
2630                    0,
2631                    this_family,
2632                    "a string argument",
2633                    is_string_like(this_family),
2634                );
2635                if let Some(chars) = &func.characters {
2636                    let chars_family = infer_expression_type_family(chars, schema_map, &context);
2637                    check_function_argument(
2638                        &mut errors,
2639                        strict,
2640                        "trim",
2641                        1,
2642                        chars_family,
2643                        "a string argument",
2644                        is_string_like(chars_family),
2645                    );
2646                }
2647            }
2648            Expression::Substring(func) => {
2649                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2650                check_function_argument(
2651                    &mut errors,
2652                    strict,
2653                    "substring",
2654                    0,
2655                    this_family,
2656                    "a string argument",
2657                    is_string_like(this_family),
2658                );
2659
2660                let start_family = infer_expression_type_family(&func.start, schema_map, &context);
2661                check_function_argument(
2662                    &mut errors,
2663                    strict,
2664                    "substring",
2665                    1,
2666                    start_family,
2667                    "a numeric argument",
2668                    start_family.is_numeric(),
2669                );
2670                if let Some(length) = &func.length {
2671                    let length_family = infer_expression_type_family(length, schema_map, &context);
2672                    check_function_argument(
2673                        &mut errors,
2674                        strict,
2675                        "substring",
2676                        2,
2677                        length_family,
2678                        "a numeric argument",
2679                        length_family.is_numeric(),
2680                    );
2681                }
2682            }
2683            Expression::Replace(func) => {
2684                for (arg, idx) in [
2685                    (&func.this, 0_usize),
2686                    (&func.old, 1_usize),
2687                    (&func.new, 2_usize),
2688                ] {
2689                    let family = infer_expression_type_family(arg, schema_map, &context);
2690                    check_function_argument(
2691                        &mut errors,
2692                        strict,
2693                        "replace",
2694                        idx,
2695                        family,
2696                        "a string argument",
2697                        is_string_like(family),
2698                    );
2699                }
2700            }
2701            Expression::Left(func) | Expression::Right(func) => {
2702                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2703                check_function_argument(
2704                    &mut errors,
2705                    strict,
2706                    "left_right",
2707                    0,
2708                    this_family,
2709                    "a string argument",
2710                    is_string_like(this_family),
2711                );
2712                let length_family =
2713                    infer_expression_type_family(&func.length, schema_map, &context);
2714                check_function_argument(
2715                    &mut errors,
2716                    strict,
2717                    "left_right",
2718                    1,
2719                    length_family,
2720                    "a numeric argument",
2721                    length_family.is_numeric(),
2722                );
2723            }
2724            Expression::Repeat(func) => {
2725                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2726                check_function_argument(
2727                    &mut errors,
2728                    strict,
2729                    "repeat",
2730                    0,
2731                    this_family,
2732                    "a string argument",
2733                    is_string_like(this_family),
2734                );
2735                let times_family = infer_expression_type_family(&func.times, schema_map, &context);
2736                check_function_argument(
2737                    &mut errors,
2738                    strict,
2739                    "repeat",
2740                    1,
2741                    times_family,
2742                    "a numeric argument",
2743                    times_family.is_numeric(),
2744                );
2745            }
2746            Expression::Lpad(func) | Expression::Rpad(func) => {
2747                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2748                check_function_argument(
2749                    &mut errors,
2750                    strict,
2751                    "pad",
2752                    0,
2753                    this_family,
2754                    "a string argument",
2755                    is_string_like(this_family),
2756                );
2757                let length_family =
2758                    infer_expression_type_family(&func.length, schema_map, &context);
2759                check_function_argument(
2760                    &mut errors,
2761                    strict,
2762                    "pad",
2763                    1,
2764                    length_family,
2765                    "a numeric argument",
2766                    length_family.is_numeric(),
2767                );
2768                if let Some(fill) = &func.fill {
2769                    let fill_family = infer_expression_type_family(fill, schema_map, &context);
2770                    check_function_argument(
2771                        &mut errors,
2772                        strict,
2773                        "pad",
2774                        2,
2775                        fill_family,
2776                        "a string argument",
2777                        is_string_like(fill_family),
2778                    );
2779                }
2780            }
2781            Expression::Abs(func)
2782            | Expression::Sqrt(func)
2783            | Expression::Cbrt(func)
2784            | Expression::Ln(func)
2785            | Expression::Exp(func) => {
2786                let family = infer_expression_type_family(&func.this, schema_map, &context);
2787                check_function_argument(
2788                    &mut errors,
2789                    strict,
2790                    "numeric_function",
2791                    0,
2792                    family,
2793                    "a numeric argument",
2794                    family.is_numeric(),
2795                );
2796            }
2797            Expression::Round(func) => {
2798                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2799                check_function_argument(
2800                    &mut errors,
2801                    strict,
2802                    "round",
2803                    0,
2804                    this_family,
2805                    "a numeric argument",
2806                    this_family.is_numeric(),
2807                );
2808                if let Some(decimals) = &func.decimals {
2809                    let decimals_family =
2810                        infer_expression_type_family(decimals, schema_map, &context);
2811                    check_function_argument(
2812                        &mut errors,
2813                        strict,
2814                        "round",
2815                        1,
2816                        decimals_family,
2817                        "a numeric argument",
2818                        decimals_family.is_numeric(),
2819                    );
2820                }
2821            }
2822            Expression::Floor(func) => {
2823                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2824                check_function_argument(
2825                    &mut errors,
2826                    strict,
2827                    "floor",
2828                    0,
2829                    this_family,
2830                    "a numeric argument",
2831                    this_family.is_numeric(),
2832                );
2833                if let Some(scale) = &func.scale {
2834                    let scale_family = infer_expression_type_family(scale, schema_map, &context);
2835                    check_function_argument(
2836                        &mut errors,
2837                        strict,
2838                        "floor",
2839                        1,
2840                        scale_family,
2841                        "a numeric argument",
2842                        scale_family.is_numeric(),
2843                    );
2844                }
2845            }
2846            Expression::Ceil(func) => {
2847                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2848                check_function_argument(
2849                    &mut errors,
2850                    strict,
2851                    "ceil",
2852                    0,
2853                    this_family,
2854                    "a numeric argument",
2855                    this_family.is_numeric(),
2856                );
2857                if let Some(decimals) = &func.decimals {
2858                    let decimals_family =
2859                        infer_expression_type_family(decimals, schema_map, &context);
2860                    check_function_argument(
2861                        &mut errors,
2862                        strict,
2863                        "ceil",
2864                        1,
2865                        decimals_family,
2866                        "a numeric argument",
2867                        decimals_family.is_numeric(),
2868                    );
2869                }
2870            }
2871            Expression::Power(func) => {
2872                let left_family = infer_expression_type_family(&func.this, schema_map, &context);
2873                check_function_argument(
2874                    &mut errors,
2875                    strict,
2876                    "power",
2877                    0,
2878                    left_family,
2879                    "a numeric argument",
2880                    left_family.is_numeric(),
2881                );
2882                let right_family =
2883                    infer_expression_type_family(&func.expression, schema_map, &context);
2884                check_function_argument(
2885                    &mut errors,
2886                    strict,
2887                    "power",
2888                    1,
2889                    right_family,
2890                    "a numeric argument",
2891                    right_family.is_numeric(),
2892                );
2893            }
2894            Expression::Log(func) => {
2895                let this_family = infer_expression_type_family(&func.this, schema_map, &context);
2896                check_function_argument(
2897                    &mut errors,
2898                    strict,
2899                    "log",
2900                    0,
2901                    this_family,
2902                    "a numeric argument",
2903                    this_family.is_numeric(),
2904                );
2905                if let Some(base) = &func.base {
2906                    let base_family = infer_expression_type_family(base, schema_map, &context);
2907                    check_function_argument(
2908                        &mut errors,
2909                        strict,
2910                        "log",
2911                        1,
2912                        base_family,
2913                        "a numeric argument",
2914                        base_family.is_numeric(),
2915                    );
2916                }
2917            }
2918            _ => {}
2919        }
2920    }
2921
2922    errors
2923}
2924
2925fn predicate_compatible(family: TypeFamily, dialect: DialectType) -> bool {
2926    matches!(family, TypeFamily::Unknown | TypeFamily::Boolean)
2927        || (matches!(
2928            dialect,
2929            DialectType::MySQL | DialectType::SQLite | DialectType::DuckDB
2930        ) && family.is_numeric())
2931}
2932
2933mod semantics;
2934pub(crate) use semantics::check_semantics;
2935
2936fn resolve_scope_source_name(scope: &crate::scope::Scope, name: &str) -> Option<String> {
2937    scope
2938        .sources
2939        .get_key_value(name)
2940        .map(|(k, _)| k.clone())
2941        .or_else(|| {
2942            scope
2943                .sources
2944                .keys()
2945                .find(|source| source.eq_ignore_ascii_case(name))
2946                .cloned()
2947        })
2948}
2949
2950fn source_has_column(
2951    scope: &crate::scope::Scope,
2952    source: &str,
2953    columns: &[String],
2954    column: &crate::expressions::Identifier,
2955    dialect: DialectType,
2956) -> bool {
2957    let strategy = get_normalization_strategy(Some(dialect));
2958    let expected = normalize_identifier(column.clone(), strategy).name;
2959    columns.iter().any(|name| {
2960        if name == "*" {
2961            return true;
2962        }
2963        let identifier = scope
2964            .sources
2965            .get(source)
2966            .and_then(|source| source_output_identifier(&source.expression, name))
2967            .cloned()
2968            .unwrap_or_else(|| crate::binding::schema_identifier(name));
2969        normalize_identifier(identifier, strategy).name == expected
2970    })
2971}
2972
2973fn source_display_name(scope: &crate::scope::Scope, source_name: &str) -> String {
2974    scope
2975        .sources
2976        .get(source_name)
2977        .map(|source| match &source.expression {
2978            Expression::Table(table) => lower(&table_ref_display_name(table)),
2979            _ => lower(source_name),
2980        })
2981        .unwrap_or_else(|| lower(source_name))
2982}
2983
2984/// Attach original token locations without manufacturing offsets for synthetic ASTs.
2985fn reference_diagnostic(
2986    message: String,
2987    code: &str,
2988    strict: bool,
2989    span: Option<crate::tokens::Span>,
2990) -> ValidationError {
2991    let mut error = if strict {
2992        ValidationError::error(message, code)
2993    } else {
2994        ValidationError::warning(message, code)
2995    };
2996    if let Some(span) = span {
2997        error = error
2998            .with_location(span.line, span.column)
2999            .with_span(Some(span.start), Some(span.end));
3000    }
3001    error
3002}
3003
3004use crate::scope::{scope_query, selected_reference_scope as selected_validation_scope};
3005
3006/// Validation starts from SQL, so source ranges identify individual references
3007/// across the scope tree's AST copies. Never bind synthetic/unpositioned columns.
3008/// Types, not defining expressions, are stored to keep alias chains linear in size.
3009type ProjectionAliasBindings = HashMap<(usize, usize), DataType>;
3010
3011fn projection_reference_key(column: &Column) -> Option<(usize, usize)> {
3012    if column.table.is_some() {
3013        return None;
3014    }
3015    column
3016        .name
3017        .span
3018        .or(column.span)
3019        .map(|span| (span.start, span.end))
3020}
3021
3022use crate::binding::bound_identifier as validation_bound_identifier;
3023
3024/// Apply already resolved bindings to the private validation AST. The canonical
3025/// visitor keeps this stack-safe and covers references inside typed functions.
3026fn apply_projection_alias_bindings(
3027    expression: Expression,
3028    bindings: &ProjectionAliasBindings,
3029) -> Expression {
3030    if bindings.is_empty() {
3031        return expression;
3032    }
3033    enum Task {
3034        Visit(Expression),
3035        Finish(Expression, usize),
3036    }
3037    let mut pending = vec![Task::Visit(expression)];
3038    let mut results = Vec::new();
3039    while let Some(task) = pending.pop() {
3040        match task {
3041            Task::Visit(mut expression) => {
3042                if let Expression::Column(column) = &expression {
3043                    if let Some(data_type) =
3044                        projection_reference_key(column).and_then(|key| bindings.get(&key))
3045                    {
3046                        results.push(validation_bound_identifier(column.name.clone(), data_type));
3047                        continue;
3048                    }
3049                }
3050                let mut children = Vec::new();
3051                crate::ast_children::for_each_child_mut(&mut expression, |child| {
3052                    children.push(std::mem::replace(
3053                        child,
3054                        Expression::Null(crate::expressions::Null),
3055                    ));
3056                });
3057                pending.push(Task::Finish(expression, children.len()));
3058                pending.extend(children.into_iter().rev().map(Task::Visit));
3059            }
3060            Task::Finish(mut expression, count) => {
3061                let mut children = results.split_off(results.len() - count).into_iter();
3062                crate::ast_children::for_each_child_mut(&mut expression, |child| {
3063                    *child = children.next().expect("alias binding child");
3064                });
3065                results.push(expression);
3066            }
3067        }
3068    }
3069    results.pop().expect("alias binding result")
3070}
3071
3072fn bind_scope_projection_aliases(
3073    scope: &mut crate::scope::Scope,
3074    ancestors: &[&crate::scope::Scope],
3075    resolver_schema: &MappingSchema,
3076    dialect: DialectType,
3077    check_types: bool,
3078    bindings: &mut ProjectionAliasBindings,
3079) {
3080    let Expression::Select(select) = &scope.expression else {
3081        return;
3082    };
3083    if !select
3084        .expressions
3085        .iter()
3086        .any(|expression| matches!(expression, Expression::Alias(_)))
3087    {
3088        return;
3089    }
3090
3091    // A real input wins over a lateral alias. Ambiguous inputs must also remain
3092    // columns so the ordinary reference checker can diagnose them. Open sources
3093    // do not provide enough information to safely choose an alias instead.
3094    let strategy = get_normalization_strategy(Some(dialect));
3095    let mut inputs = HashSet::new();
3096    let mut open = false;
3097    for source_scope in std::iter::once(&*scope).chain(ancestors.iter().copied()) {
3098        let mut resolver = Resolver::new(source_scope, resolver_schema, true);
3099        for source in source_scope.sources.keys() {
3100            let columns = resolver.get_source_columns(source).unwrap_or_default();
3101            open |= columns.is_empty() || columns.iter().any(|name| name == "*");
3102            let expression = &source_scope.sources[source].expression;
3103            for name in columns {
3104                let identifier = source_output_identifier(expression, &name)
3105                    .cloned()
3106                    .unwrap_or_else(|| crate::expressions::Identifier::new(name));
3107                inputs.insert(normalize_identifier(identifier, strategy).name);
3108            }
3109        }
3110    }
3111    if open {
3112        return;
3113    }
3114
3115    // A child SELECT can use CTEs declared in a containing WITH. Reconstruct
3116    // that lexical catalogue for type inference, in original declaration order.
3117    let mut ctes: Vec<_> = if check_types {
3118        scope
3119            .cte_sources
3120            .values()
3121            .filter_map(|source| match &source.expression {
3122                Expression::Cte(cte) => Some(cte.as_ref().clone()),
3123                _ => None,
3124            })
3125            .collect()
3126    } else {
3127        Vec::new()
3128    };
3129    ctes.sort_by_key(|cte| cte.alias.span.map(|span| span.start));
3130    let mut aliases = HashMap::<String, Option<DataType>>::new();
3131    let Expression::Select(select) = &mut scope.expression else {
3132        unreachable!()
3133    };
3134    // Keep the original sources/CTEs for scoped type inference, without copying
3135    // the entire projection list for each alias. Bound uses are constant-size.
3136    let mut projections = std::mem::take(&mut select.expressions);
3137    let mut type_select = select.clone();
3138    if !ctes.is_empty() {
3139        type_select.with = Some(crate::expressions::With {
3140            ctes,
3141            recursive: false,
3142            leading_comments: Vec::new(),
3143            search: None,
3144        });
3145    }
3146    for projection in projections.iter_mut() {
3147        let mut references = ProjectionAliasBindings::new();
3148        for node in walk_in_scope(projection, false) {
3149            if !crate::binding::lateral_aliases(dialect) {
3150                break;
3151            }
3152            let Expression::Column(column) = node else {
3153                continue;
3154            };
3155            let Some(key) = projection_reference_key(column) else {
3156                continue;
3157            };
3158            let name = normalize_identifier(column.name.clone(), strategy).name;
3159            if inputs.contains(&name) {
3160                continue;
3161            }
3162            if let Some(Some(data_type)) = aliases.get(&name) {
3163                references.insert(key, data_type.clone());
3164            }
3165        }
3166        let original = std::mem::replace(projection, Expression::Null(crate::expressions::Null));
3167        *projection = apply_projection_alias_bindings(original, &references);
3168        bindings.extend(references);
3169        if let Expression::Alias(alias) = projection {
3170            let name = normalize_identifier(alias.alias.clone(), strategy).name;
3171            // Duplicate names are not evidence of an unambiguous binding.
3172            if aliases.contains_key(&name) {
3173                aliases.insert(name, None);
3174                continue;
3175            }
3176            if !check_types {
3177                aliases.insert(name, Some(DataType::Unknown));
3178                continue;
3179            }
3180            let mut typed = Expression::Select(type_select.clone());
3181            if let Expression::Select(query) = &mut typed {
3182                query.expressions.push(alias.this.clone());
3183            }
3184            // Child queries were bound first. Their aliases must contribute
3185            // their actual types, not remain unresolved inputs in this copy.
3186            typed = apply_projection_alias_bindings(typed, bindings);
3187            annotate_types(&mut typed, Some(resolver_schema), Some(dialect));
3188            let data_type = match typed {
3189                Expression::Select(query) => query.expressions[0]
3190                    .inferred_type()
3191                    .cloned()
3192                    .unwrap_or(DataType::Unknown),
3193                _ => unreachable!(),
3194            };
3195            aliases.insert(name, Some(data_type));
3196        }
3197    }
3198    select.expressions = projections;
3199    // Only clause-specific output visibility is enabled. JOIN ON remains an
3200    // input scope, and aliases never become visible in a nested query.
3201    use crate::binding::{clause_aliases, AliasClause};
3202    let mut clauses = Vec::new();
3203    if clause_aliases(dialect, AliasClause::Where) {
3204        if let Some(clause) = &mut select.where_clause {
3205            clauses.push(&mut clause.this);
3206        }
3207    }
3208    if clause_aliases(dialect, AliasClause::Group) {
3209        if let Some(clause) = &mut select.group_by {
3210            clauses.extend(clause.expressions.iter_mut());
3211        }
3212    }
3213    if clause_aliases(dialect, AliasClause::Having) {
3214        if let Some(clause) = &mut select.having {
3215            clauses.push(&mut clause.this);
3216        }
3217    }
3218    if clause_aliases(dialect, AliasClause::Qualify) {
3219        if let Some(clause) = &mut select.qualify {
3220            clauses.push(&mut clause.this);
3221        }
3222    }
3223    for clause in clauses {
3224        let mut references = ProjectionAliasBindings::new();
3225        for node in walk_in_scope(clause, false) {
3226            let Expression::Column(column) = node else {
3227                continue;
3228            };
3229            let Some(key) = projection_reference_key(column) else {
3230                continue;
3231            };
3232            let name = normalize_identifier(column.name.clone(), strategy).name;
3233            if !inputs.contains(&name) {
3234                if let Some(Some(data_type)) = aliases.get(&name) {
3235                    references.insert(key, data_type.clone());
3236                }
3237            }
3238        }
3239        let original = std::mem::replace(clause, Expression::Null(crate::expressions::Null));
3240        *clause = apply_projection_alias_bindings(original, &references);
3241        bindings.extend(references);
3242    }
3243}
3244
3245/// Resolver output names are strings. Retain quoting when those names were
3246/// declared by a CTE/derived table, rather than folding a quoted output name.
3247fn source_output_identifier<'a>(
3248    expression: &'a Expression,
3249    name: &str,
3250) -> Option<&'a crate::expressions::Identifier> {
3251    let columns: &[crate::expressions::Identifier] = match expression {
3252        Expression::Cte(cte) => &cte.columns,
3253        Expression::Subquery(query) => &query.column_aliases,
3254        Expression::Alias(alias) => &alias.column_aliases,
3255        Expression::Table(table) => &table.column_aliases,
3256        Expression::Values(values) => &values.column_aliases,
3257        Expression::Paren(paren) => return source_output_identifier(&paren.this, name),
3258        _ => &[],
3259    };
3260    if !columns.is_empty() {
3261        return columns.iter().find(|column| column.name == name);
3262    }
3263    match scope_query(expression) {
3264        Expression::Values(values) => values
3265            .column_aliases
3266            .iter()
3267            .find(|column| column.name == name),
3268        Expression::Select(select) => select.expressions.iter().find_map(|projection| {
3269            let identifier = match projection {
3270                Expression::Alias(alias) => &alias.alias,
3271                Expression::Column(column) => &column.name,
3272                Expression::Identifier(identifier) => identifier,
3273                _ => return None,
3274            };
3275            (identifier.name == name).then_some(identifier)
3276        }),
3277        Expression::Union(query) => source_output_identifier(&query.left, name),
3278        Expression::Intersect(query) => source_output_identifier(&query.left, name),
3279        Expression::Except(query) => source_output_identifier(&query.left, name),
3280        _ => None,
3281    }
3282}
3283
3284/// Prepare the private validation AST, not the public parser/serialized AST.
3285/// Bound parameters become identifiers (optionally typed), leaving only free
3286/// column references for schema resolution. Doing this once, before dotted
3287/// normalization, also prevents type checks from borrowing a shadowed table
3288/// column's type. Nested query scopes do not inherit lambda parameters.
3289use crate::binding::bind_lambdas as bind_validation_lambdas;
3290
3291#[derive(Clone, Copy)]
3292struct ReferenceValidationOptions {
3293    dialect: DialectType,
3294    strict: bool,
3295    check_references: bool,
3296    check_types: bool,
3297}
3298
3299#[derive(Clone, Copy, PartialEq, Eq)]
3300enum OutputNameResolution {
3301    InputOnly,
3302    OutputFirst,
3303    InputFirst,
3304}
3305
3306/// Collect ordering references together with whether they can resolve to the
3307/// current SELECT's outputs. Other clauses use the ordinary input-source walk.
3308fn order_by_validation_columns(
3309    order_by: &crate::expressions::OrderBy,
3310    dialect: DialectType,
3311) -> Vec<(&Column, OutputNameResolution)> {
3312    // Only enable aliases inside scalar expressions for dialects whose rules
3313    // support them. In particular, PostgreSQL and T-SQL require standalone
3314    // output names; `ORDER BY alias + 1` must resolve against input columns.
3315    let scalar_resolution = match dialect {
3316        // DuckDB's scalar binder prefers inputs inside ordering expressions,
3317        // unlike its standalone ORDER BY output-name lookup.
3318        DialectType::DuckDB => OutputNameResolution::InputFirst,
3319        DialectType::Snowflake | DialectType::BigQuery => OutputNameResolution::OutputFirst,
3320        _ => OutputNameResolution::InputOnly,
3321    };
3322    let mut columns = Vec::new();
3323    let mut pending: Vec<_> = order_by
3324        .expressions
3325        .iter()
3326        .rev()
3327        .map(|ordered| (&ordered.this, OutputNameResolution::OutputFirst))
3328        .collect();
3329    while let Some((node, resolution)) = pending.pop() {
3330        match node {
3331            // Child query scopes are validated separately and must not inherit
3332            // their parent's SELECT-list aliases.
3333            Expression::Select(_)
3334            | Expression::Union(_)
3335            | Expression::Intersect(_)
3336            | Expression::Except(_)
3337            | Expression::Subquery(_)
3338            | Expression::Exists(_)
3339            | Expression::Cte(_) => continue,
3340            Expression::Column(column) => columns.push((column.as_ref(), resolution)),
3341            Expression::Paren(paren) => pending.push((&paren.this, resolution)),
3342            _ => {
3343                let input_only = resolution == OutputNameResolution::InputOnly
3344                    || matches!(
3345                        node,
3346                        Expression::WindowFunction(_)
3347                            | Expression::WithinGroup(_)
3348                            | Expression::Filter(_)
3349                    )
3350                    || crate::traversal::is_aggregate(node);
3351                if input_only {
3352                    columns.extend(walk_in_scope(node, false).filter_map(|node| match node {
3353                        Expression::Column(column) => {
3354                            Some((column.as_ref(), OutputNameResolution::InputOnly))
3355                        }
3356                        _ => None,
3357                    }));
3358                } else {
3359                    pending.extend(
3360                        node.children()
3361                            .into_iter()
3362                            .rev()
3363                            .map(|child| (child, scalar_resolution)),
3364                    );
3365                }
3366            }
3367        }
3368    }
3369    columns
3370}
3371
3372fn validate_scope_columns(
3373    scope: &crate::scope::Scope,
3374    ancestors: &[&crate::scope::Scope],
3375    schema_map: &HashMap<String, TableSchemaEntry>,
3376    resolver_schema: &MappingSchema,
3377    options: ReferenceValidationOptions,
3378) -> Vec<ValidationError> {
3379    let ReferenceValidationOptions {
3380        dialect,
3381        strict,
3382        check_references,
3383        ..
3384    } = options;
3385    let mut errors = Vec::new();
3386    let Expression::Select(select) = &scope.expression else {
3387        return errors;
3388    };
3389
3390    // Normalize struct access with the actual lexical context. A real outer
3391    // qualifier must not be reinterpreted as a column of an open local table.
3392    let mut visible = crate::scope::Scope::new(scope.expression.clone());
3393    for source_scope in std::iter::once(scope).chain(ancestors.iter().copied()) {
3394        for (name, source) in &source_scope.sources {
3395            if resolve_scope_source_name(&visible, name).is_none() {
3396                visible.sources.insert(name.clone(), source.clone());
3397            }
3398        }
3399    }
3400    let mut normalized = select.clone();
3401    let mut normalizer = Resolver::new(&visible, resolver_schema, true);
3402    let _ = normalize_dotted_columns_in_scope(&mut normalized, &visible, &mut normalizer);
3403    let strategy = get_normalization_strategy(Some(dialect));
3404    let mut output_names = HashMap::<String, usize>::new();
3405    for projection in &normalized.expressions {
3406        let identifier = match projection {
3407            // Until wildcards are expanded, output-name uniqueness is not
3408            // known. Preserve source validation instead of letting an explicit
3409            // alias hide a conflicting column contributed by a wildcard.
3410            Expression::Star(_) => {
3411                output_names.clear();
3412                break;
3413            }
3414            Expression::Alias(alias) => &alias.alias,
3415            Expression::Column(column) if column.name.name != "*" => &column.name,
3416            Expression::Column(_) => {
3417                output_names.clear();
3418                break;
3419            }
3420            _ => continue,
3421        };
3422        let name = normalize_identifier(identifier.clone(), strategy).name;
3423        *output_names.entry(name).or_default() += 1;
3424    }
3425    // Separate query-level ordering from the ordinary scope walk. This avoids
3426    // accidentally exempting a same-named reference in another clause, window,
3427    // or nested query, and leaves the caller's AST untouched.
3428    let order_by = normalized.order_by.take();
3429    let expression = Expression::Select(normalized);
3430    let mut resolvers: Vec<_> = std::iter::once(scope)
3431        .chain(ancestors.iter().copied())
3432        .map(|scope| (scope, Resolver::new(scope, resolver_schema, true)))
3433        .collect();
3434    let using_columns: HashSet<String> = select
3435        .joins
3436        .iter()
3437        .flat_map(|join| join.using.iter().map(|id| lower(&id.name)))
3438        .collect();
3439
3440    let input_columns = walk_in_scope(&expression, false).filter_map(|node| match node {
3441        Expression::Column(column) => Some((column.as_ref(), OutputNameResolution::InputOnly)),
3442        _ => None,
3443    });
3444    let order_columns = order_by
3445        .as_ref()
3446        .into_iter()
3447        .flat_map(|order| order_by_validation_columns(order, dialect));
3448    for (column, resolution) in input_columns.chain(order_columns) {
3449        let output_matches =
3450            if resolution != OutputNameResolution::InputOnly && column.table.is_none() {
3451                let name = normalize_identifier(column.name.clone(), strategy).name;
3452                // Only an unambiguous output name takes precedence over sources.
3453                // Duplicate output names retain the existing reference checks.
3454                output_names.get(&name) == Some(&1)
3455            } else {
3456                false
3457            };
3458        if output_matches && resolution == OutputNameResolution::OutputFirst {
3459            continue;
3460        }
3461        let name = lower(&column.name.name);
3462        if name.is_empty() || name == "*" {
3463            continue;
3464        }
3465        let span = column.name.span.or(column.span);
3466        if let Some(qualifier) = &column.table {
3467            let resolved = resolvers.iter_mut().find_map(|(scope, resolver)| {
3468                resolve_scope_source_name(scope, &qualifier.name).map(|source| {
3469                    (
3470                        *scope,
3471                        resolver.get_source_columns(&source).unwrap_or_default(),
3472                        source,
3473                    )
3474                })
3475            });
3476            if let Some((source_scope, columns, source)) = resolved {
3477                if !columns.is_empty()
3478                    && !source_has_column(source_scope, &source, &columns, &column.name, dialect)
3479                {
3480                    errors.push(reference_diagnostic(
3481                        format!(
3482                            "Unknown column '{}' in table '{}'",
3483                            name,
3484                            source_display_name(source_scope, &source)
3485                        ),
3486                        validation_codes::E_UNKNOWN_COLUMN,
3487                        strict,
3488                        span,
3489                    ));
3490                }
3491            } else {
3492                errors.push(reference_diagnostic(
3493                    format!(
3494                        "Unknown table or alias '{}' referenced by column '{}'",
3495                        qualifier.name, name
3496                    ),
3497                    validation_codes::E_UNRESOLVED_REFERENCE,
3498                    strict,
3499                    qualifier.span.or(column.span),
3500                ));
3501            }
3502            continue;
3503        }
3504
3505        let mut found = false;
3506        for (source_scope, resolver) in &mut resolvers {
3507            let mut matches = 0;
3508            let mut open = false;
3509            for source in source_scope.sources.keys() {
3510                let columns = resolver.get_source_columns(source).unwrap_or_default();
3511                open |= columns.is_empty() || columns.iter().any(|name| name == "*");
3512                // Wildcards are not evidence of a definite ambiguity.
3513                matches += usize::from(
3514                    !columns.iter().any(|name| name == "*")
3515                        && source_has_column(source_scope, source, &columns, &column.name, dialect),
3516                );
3517            }
3518            if matches > 0 || open {
3519                if matches > 1 && check_references && !using_columns.contains(&name) {
3520                    errors.push(reference_diagnostic(
3521                        format!(
3522                            "Ambiguous unqualified column '{}' found in {} referenced tables",
3523                            name, matches
3524                        ),
3525                        if strict {
3526                            validation_codes::E_AMBIGUOUS_COLUMN_REFERENCE
3527                        } else {
3528                            validation_codes::W_WEAK_REFERENCE_INTEGRITY
3529                        },
3530                        strict,
3531                        span,
3532                    ));
3533                }
3534                found = true;
3535                break;
3536            }
3537        }
3538        if !found {
3539            if output_matches && resolution == OutputNameResolution::InputFirst {
3540                continue;
3541            }
3542            // Preserve the existing schema-only lookup for standalone column
3543            // expressions (SELECT id). Never consult unrelated tables when
3544            // the query has actual sources or an enclosing query context.
3545            if scope.sources.is_empty()
3546                && ancestors.is_empty()
3547                && (schema_map.is_empty()
3548                    || schema_map
3549                        .values()
3550                        .any(|table| table.columns.contains_key(&name)))
3551            {
3552                continue;
3553            }
3554            let message = if scope.sources.len() == 1 {
3555                let source = scope.sources.keys().next().unwrap();
3556                format!(
3557                    "Unknown column '{}' in table '{}'",
3558                    name,
3559                    source_display_name(scope, source)
3560                )
3561            } else {
3562                format!(
3563                    "Unknown column '{}' (not found in any referenced table)",
3564                    name
3565                )
3566            };
3567            errors.push(reference_diagnostic(
3568                message,
3569                validation_codes::E_UNKNOWN_COLUMN,
3570                strict,
3571                span,
3572            ));
3573        }
3574    }
3575    errors
3576}
3577
3578fn validate_scope_tree(
3579    scope: &crate::scope::Scope,
3580    ancestors: &[&crate::scope::Scope],
3581    schema_map: &HashMap<String, TableSchemaEntry>,
3582    resolver_schema: &MappingSchema,
3583    options: ReferenceValidationOptions,
3584    errors: &mut Vec<ValidationError>,
3585    bindings: &mut ProjectionAliasBindings,
3586) {
3587    let mut selected = selected_validation_scope(scope);
3588    // Bind children before consumers, but retain the existing parent-first
3589    // diagnostic order. Only physical sources are inherited, never output aliases.
3590    let mut child_errors = Vec::new();
3591    // CTEs and ordinary derived tables cannot see their containing SELECT's
3592    // sources, but may retain ancestors of an enclosing correlated subquery.
3593    for child in scope
3594        .cte_scopes
3595        .iter()
3596        .chain(&scope.derived_table_scopes)
3597        .chain(&scope.union_scopes)
3598    {
3599        // A lateral child sees the sources registered before it, not its own
3600        // output, later siblings, or unused CTE declarations. Keep even an
3601        // empty context so standalone schema lookup cannot invent an input.
3602        let lateral = child.is_lateral.then(|| {
3603            let mut lateral = crate::scope::Scope::new(Expression::null());
3604            lateral.sources = child.lateral_sources.clone();
3605            lateral
3606        });
3607        let lateral_ancestors = lateral.as_ref().map(|lateral| {
3608            std::iter::once(lateral)
3609                .chain(ancestors.iter().copied())
3610                .collect::<Vec<_>>()
3611        });
3612        validate_scope_tree(
3613            child,
3614            lateral_ancestors.as_deref().unwrap_or(ancestors),
3615            schema_map,
3616            resolver_schema,
3617            options,
3618            &mut child_errors,
3619            bindings,
3620        );
3621    }
3622    let outer: Vec<_> = std::iter::once(&selected)
3623        .chain(ancestors.iter().copied())
3624        .collect();
3625    for child in scope.subquery_scopes.iter().chain(&scope.udtf_scopes) {
3626        validate_scope_tree(
3627            child,
3628            &outer,
3629            schema_map,
3630            resolver_schema,
3631            options,
3632            &mut child_errors,
3633            bindings,
3634        );
3635    }
3636    bind_scope_projection_aliases(
3637        &mut selected,
3638        ancestors,
3639        resolver_schema,
3640        options.dialect,
3641        options.check_types,
3642        bindings,
3643    );
3644    for node in walk_in_scope(&selected.expression, false) {
3645        let Expression::Table(table) = node else {
3646            continue;
3647        };
3648        let is_cte = table.schema.is_none()
3649            && table.catalog.is_none()
3650            && selected
3651                .cte_sources
3652                .keys()
3653                .any(|name| name.eq_ignore_ascii_case(&table.name.name));
3654        if !is_cte {
3655            validate_schema_table(table, schema_map, options.strict, errors);
3656        }
3657    }
3658    errors.extend(validate_scope_columns(
3659        &selected,
3660        ancestors,
3661        schema_map,
3662        resolver_schema,
3663        options,
3664    ));
3665    errors.extend(child_errors);
3666}
3667
3668fn validate_schema_table(
3669    table: &TableRef,
3670    schema_map: &HashMap<String, TableSchemaEntry>,
3671    strict: bool,
3672    errors: &mut Vec<ValidationError>,
3673) {
3674    if !table_ref_candidates(table)
3675        .iter()
3676        .any(|key| schema_map.contains_key(key))
3677    {
3678        errors.push(reference_diagnostic(
3679            format!("Unknown table '{}'", table_ref_display_name(table)),
3680            validation_codes::E_UNKNOWN_TABLE,
3681            strict,
3682            table.name.span,
3683        ));
3684    }
3685}
3686
3687fn validate_statement_with_schema(
3688    stmt: &Expression,
3689    schema_map: &HashMap<String, TableSchemaEntry>,
3690    resolver_schema: &MappingSchema,
3691    options: ReferenceValidationOptions,
3692    bindings: &mut ProjectionAliasBindings,
3693) -> Vec<ValidationError> {
3694    let mut errors = Vec::new();
3695    // Visit each query tree once, including queries embedded in DML/DDL.
3696    let mut pending = vec![stmt];
3697    while let Some(expression) = pending.pop() {
3698        if let Some(select) = crate::binding::dml_scope(expression) {
3699            let query = Expression::Select(Box::new(select));
3700            let scope = build_scope(&query);
3701            validate_scope_tree(
3702                &scope,
3703                &[],
3704                schema_map,
3705                resolver_schema,
3706                options,
3707                &mut errors,
3708                bindings,
3709            );
3710            let target_columns = match expression {
3711                Expression::Insert(insert) => {
3712                    Some((&insert.table, insert.columns.iter().collect::<Vec<_>>()))
3713                }
3714                Expression::Update(update) => Some((
3715                    &update.table,
3716                    update.set.iter().map(|(column, _)| column).collect(),
3717                )),
3718                _ => None,
3719            };
3720            if let Some((table, columns)) = target_columns {
3721                if let Some((_, entry)) = resolve_table_schema_entry(table, schema_map) {
3722                    for column in columns {
3723                        if !entry.columns.is_empty()
3724                            && !entry.columns.contains_key("*")
3725                            && !entry.columns.contains_key(&lower(&column.name))
3726                        {
3727                            errors.push(reference_diagnostic(
3728                                format!(
3729                                    "Unknown column '{}' in table '{}'",
3730                                    column.name, table.name.name
3731                                ),
3732                                validation_codes::E_UNKNOWN_COLUMN,
3733                                options.strict,
3734                                column.span,
3735                            ));
3736                        }
3737                    }
3738                }
3739            }
3740            if let Expression::Insert(insert) = expression {
3741                // VALUES has no implicit target-table input scope.
3742                for value in insert.values.iter().flatten() {
3743                    for node in walk_in_scope(value, false) {
3744                        if let Expression::Column(column) = node {
3745                            errors.push(reference_diagnostic(
3746                                format!("Unknown column '{}' in INSERT VALUES", column.name.name),
3747                                validation_codes::E_UNKNOWN_COLUMN,
3748                                options.strict,
3749                                column.name.span.or(column.span),
3750                            ));
3751                        }
3752                    }
3753                    pending.extend(value.children().into_iter().filter(|child| {
3754                        matches!(child, Expression::Subquery(_) | Expression::Select(_))
3755                    }));
3756                }
3757                if let Some(query) = &insert.query {
3758                    // Validate the source as a query, not as a bare SELECT in
3759                    // a synthetic projection (which is not a scalar subquery).
3760                    // The leading CTEs were already checked in the DML scope.
3761                    let source = crate::scope::build_scope_with_ctes(query, &scope.cte_sources);
3762                    // INSERT does not expose its target columns to the source.
3763                    // An empty enclosing scope also disables standalone-schema
3764                    // fallback for source queries without a FROM clause.
3765                    let outer = crate::scope::Scope::new(Expression::null());
3766                    validate_scope_tree(
3767                        &source,
3768                        &[&outer],
3769                        schema_map,
3770                        resolver_schema,
3771                        options,
3772                        &mut errors,
3773                        bindings,
3774                    );
3775                }
3776            }
3777            if let Expression::Merge(merge) = expression {
3778                let target = match merge.this.as_ref() {
3779                    Expression::Table(table) => Some(table.as_ref()),
3780                    Expression::Alias(alias) => match &alias.this {
3781                        Expression::Table(table) => Some(table.as_ref()),
3782                        _ => None,
3783                    },
3784                    _ => None,
3785                };
3786                if let Some((table, (_, entry))) = target.and_then(|table| {
3787                    resolve_table_schema_entry(table, schema_map).map(|entry| (table, entry))
3788                }) {
3789                    if let Some(whens) = &merge.whens {
3790                        for node in whens.dfs() {
3791                            let Expression::When(when) = node else {
3792                                continue;
3793                            };
3794                            let Expression::Tuple(action) = when.then.as_ref() else {
3795                                continue;
3796                            };
3797                            let Some(Expression::Var(kind)) = action.expressions.first() else {
3798                                continue;
3799                            };
3800                            let Some(Expression::Tuple(assignments)) = action.expressions.get(1)
3801                            else {
3802                                continue;
3803                            };
3804                            for assignment in &assignments.expressions {
3805                                let target = if kind.this.eq_ignore_ascii_case("UPDATE") {
3806                                    if let Expression::Eq(eq) = assignment {
3807                                        &eq.left
3808                                    } else {
3809                                        continue;
3810                                    }
3811                                } else if kind.this.eq_ignore_ascii_case("INSERT") {
3812                                    assignment
3813                                } else {
3814                                    continue;
3815                                };
3816                                let identifier = match target {
3817                                    Expression::Identifier(id) => id,
3818                                    Expression::Column(column) => &column.name,
3819                                    _ => continue,
3820                                };
3821                                if !entry.columns.is_empty()
3822                                    && !entry.columns.contains_key("*")
3823                                    && !entry.columns.contains_key(&lower(&identifier.name))
3824                                {
3825                                    errors.push(reference_diagnostic(
3826                                        format!(
3827                                            "Unknown column '{}' in table '{}'",
3828                                            identifier.name, table.name.name
3829                                        ),
3830                                        validation_codes::E_UNKNOWN_COLUMN,
3831                                        options.strict,
3832                                        identifier.span,
3833                                    ));
3834                                }
3835                            }
3836                        }
3837                    }
3838                }
3839            }
3840            continue;
3841        }
3842        if matches!(
3843            scope_query(expression),
3844            Expression::Select(_)
3845                | Expression::Union(_)
3846                | Expression::Intersect(_)
3847                | Expression::Except(_)
3848        ) {
3849            let scope = build_scope(expression);
3850            validate_scope_tree(
3851                &scope,
3852                &[],
3853                schema_map,
3854                resolver_schema,
3855                options,
3856                &mut errors,
3857                bindings,
3858            );
3859        } else {
3860            // DDL may declare a new table; only its query inputs are references.
3861            if let Expression::CreateTable(create) = expression {
3862                if let Some(query) = &create.as_select {
3863                    pending.push(query);
3864                }
3865                if let Some(source) = &create.clone_source {
3866                    validate_schema_table(source, schema_map, options.strict, &mut errors);
3867                }
3868                continue;
3869            }
3870            if let Expression::Table(table) = expression {
3871                validate_schema_table(table, schema_map, options.strict, &mut errors);
3872            }
3873            pending.extend(expression.children().into_iter().rev());
3874        }
3875    }
3876    errors
3877}
3878
3879/// Validate SQL using syntax + schema-aware checks, with optional semantic checks.
3880pub fn validate_with_schema(
3881    sql: &str,
3882    dialect: DialectType,
3883    schema: &ValidationSchema,
3884    options: &SchemaValidationOptions,
3885) -> ValidationResult {
3886    let strict = options.strict.unwrap_or(schema.strict.unwrap_or(true));
3887
3888    // Parse once, preserving syntax-error precedence and the caller's guards.
3889    let d = Dialect::get(dialect);
3890    let statements = match crate::parse_for_validation(
3891        sql,
3892        &d,
3893        &crate::ValidationOptions {
3894            complexity_guard: options.complexity_guard,
3895            strict_syntax: options.strict_syntax,
3896            semantic: options.semantic,
3897        },
3898    ) {
3899        Ok(exprs) => exprs,
3900        Err(result) => return result,
3901    };
3902
3903    let schema_map = build_schema_map(schema);
3904    let resolver_schema = mapping_schema_from_validation_schema_with_dialect(schema, dialect);
3905    let mut all_errors = Vec::new();
3906    let embedded_function_catalog = if options.check_types && options.function_catalog.is_none() {
3907        default_embedded_function_catalog()
3908    } else {
3909        None
3910    };
3911    let effective_function_catalog = options
3912        .function_catalog
3913        .as_deref()
3914        .or_else(|| embedded_function_catalog.as_deref());
3915    let declared_relationships = if options.check_references {
3916        build_declared_relationships(schema, &schema_map)
3917    } else {
3918        Vec::new()
3919    };
3920
3921    if options.check_references {
3922        all_errors.extend(check_reference_integrity(schema, &schema_map, strict));
3923    }
3924
3925    for mut statement in statements {
3926        if options.semantic {
3927            all_errors.extend(check_semantics(&statement, dialect, Some(schema)));
3928        }
3929        crate::binding::bind_dml_pseudoreferences(&mut statement);
3930        let statement = bind_validation_lambdas(statement, dialect);
3931        let mut bindings = ProjectionAliasBindings::new();
3932        all_errors.extend(validate_statement_with_schema(
3933            &statement,
3934            &schema_map,
3935            &resolver_schema,
3936            ReferenceValidationOptions {
3937                dialect,
3938                strict,
3939                check_references: options.check_references,
3940                check_types: options.check_types,
3941            },
3942            &mut bindings,
3943        ));
3944        let mut statement = apply_projection_alias_bindings(statement, &bindings);
3945        if options.check_types {
3946            // Expand known stars using the same qualification path as lineage.
3947            // Open/partial schemas remain conservative if qualification fails.
3948            if statement.dfs().any(|node| {
3949                matches!(
3950                    node,
3951                    Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_)
3952                )
3953            }) && statement
3954                .dfs()
3955                .any(|node| matches!(node, Expression::Star(_)))
3956            {
3957                if let Ok(qualified) = crate::optimizer::qualify_schema_aware_expression(
3958                    statement.clone(),
3959                    &resolver_schema,
3960                    Some(dialect),
3961                ) {
3962                    statement = qualified;
3963                }
3964            }
3965            annotate_types(&mut statement, Some(&resolver_schema), Some(dialect));
3966        }
3967        let stmt = &statement;
3968        if options.check_types {
3969            all_errors.extend(check_types(
3970                stmt,
3971                dialect,
3972                &schema_map,
3973                effective_function_catalog,
3974                strict,
3975            ));
3976        }
3977        if options.check_references {
3978            all_errors.extend(check_query_reference_quality(
3979                stmt,
3980                &schema_map,
3981                &resolver_schema,
3982                &declared_relationships,
3983            ));
3984        }
3985    }
3986
3987    ValidationResult::with_errors(all_errors)
3988}
3989
3990#[cfg(test)]
3991mod tests;