Skip to main content

polyglot_sql/
validation.rs

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