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