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