Skip to main content

polyglot_sql/
query_analysis.rs

1//! Compact query analysis facts.
2//!
3//! This module intentionally builds on the existing parser, scope builder, type
4//! annotator, and lineage implementation. It is a convenience API: callers that
5//! need the full AST or full lineage graph should continue using those lower
6//! level APIs directly.
7
8use crate::ast_transforms::get_output_column_names_for_dialect;
9use crate::dialects::{Dialect, DialectType};
10use crate::expressions::{DataType, Expression, JoinKind, TableRef, With};
11use crate::lineage::{lineage_by_index_from_expression, LineageNode};
12use crate::optimizer::annotate_types::annotate_types;
13use crate::optimizer::qualify_schema_aware_expression;
14use crate::schema::{MappingSchema, Schema};
15use crate::scope::{build_scope, Scope, SourceInfo, SourceKind};
16use crate::traversal::{contains_aggregate, ExpressionWalk};
17use crate::validation::{mapping_schema_from_validation_schema_with_dialect, ValidationSchema};
18use crate::{parse_one, Error, Result};
19use serde::{Deserialize, Serialize};
20use std::collections::{HashMap, HashSet};
21
22mod column_uses;
23
24/// Options for [`analyze_query`].
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26#[serde(rename_all = "camelCase", default)]
27pub struct AnalyzeQueryOptions {
28    /// SQL dialect used for parsing and dialect-aware rendering.
29    pub dialect: DialectType,
30    /// Optional validation schema used for qualification and type annotation.
31    pub schema: Option<ValidationSchema>,
32}
33
34/// Compact facts about a query's output shape and data dependencies.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub struct QueryAnalysis {
38    pub shape: QueryShape,
39    pub ctes: Vec<String>,
40    pub cte_facts: Vec<CteFact>,
41    pub projections: Vec<ProjectionFact>,
42    pub relations: Vec<RelationFact>,
43    pub base_tables: Vec<RelationFact>,
44    pub star_projections: Vec<StarProjectionFact>,
45    pub set_operations: Vec<SetOperationFact>,
46    /// Clause-specific uses, separate from output projection lineage.
47    #[serde(default)]
48    pub column_uses: Vec<ColumnUseFact>,
49}
50
51/// A half-open range in the original SQL, measured in Unicode characters.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53pub struct QuerySourceSpan {
54    pub start: usize,
55    pub end: usize,
56}
57
58/// The syntactic role of a column-containing expression.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum ColumnUseContext {
62    Join,
63    Filter,
64    Group,
65    Having,
66    Qualify,
67    WindowPartition,
68    WindowOrder,
69    WindowFrame,
70    Order,
71    AggregateOrder,
72    SetOperationFilter,
73}
74
75/// One resolved dependency of an original column occurrence. Several terminal
76/// dependencies can share a span (for example, a reference to a computed CTE
77/// column). Spans identify the use, not the upstream column's definition.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct ColumnUseReferenceFact {
81    #[serde(flatten)]
82    pub reference: ColumnReferenceFact,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub span: Option<QuerySourceSpan>,
85}
86
87/// One containing expression, with references in occurrence order. Paths are
88/// deterministic within an analysis, not persistent IDs across SQL edits.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct ColumnUseFact {
92    pub context: ColumnUseContext,
93    pub scope_path: String,
94    pub expression_path: String,
95    /// Dialect-rendered SQL; not necessarily the original source substring.
96    pub expression_sql: String,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub span: Option<QuerySourceSpan>,
99    pub references: Vec<ColumnUseReferenceFact>,
100}
101
102/// Top-level query shape.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum QueryShape {
106    Select,
107    SetOperation,
108}
109
110/// Compact fact about one output projection.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct ProjectionFact {
114    pub index: usize,
115    pub name: Option<String>,
116    pub is_star: bool,
117    pub star_table: Option<String>,
118    pub transform_kind: TransformKind,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub transform_function: Option<TransformFunctionFact>,
121    pub cast_type: Option<String>,
122    pub type_hint: Option<String>,
123    pub nullability: ProjectionNullability,
124    pub upstream: Vec<ColumnReferenceFact>,
125}
126
127/// Compact fact about a function-like projection transform.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct TransformFunctionFact {
131    pub name: String,
132    pub literal_args: Vec<String>,
133    pub column_args: Vec<ColumnReferenceFact>,
134}
135
136/// Compact fact about one top-level CTE definition.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct CteFact {
140    pub name: String,
141    pub columns: Vec<String>,
142    pub body_sql: String,
143    pub output_columns: Vec<String>,
144}
145
146/// Compact fact about one original star projection.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct StarProjectionFact {
150    pub index: usize,
151    pub table: Option<String>,
152    pub expanded_columns: Vec<String>,
153}
154
155/// Compact fact about an upstream column reference.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157#[serde(rename_all = "camelCase")]
158pub struct ColumnReferenceFact {
159    pub source_name: Option<String>,
160    pub source_alias: Option<String>,
161    pub source_kind: SourceKind,
162    pub table: Option<String>,
163    pub column: String,
164    pub unqualified: bool,
165    pub confidence: ReferenceConfidence,
166}
167
168/// Compact fact about a relation visible in the root scope.
169#[derive(Debug, Clone, Serialize, Deserialize)]
170#[serde(rename_all = "camelCase")]
171pub struct RelationFact {
172    pub name: String,
173    pub alias: Option<String>,
174    pub kind: SourceKind,
175    pub columns: Vec<String>,
176    pub catalog: Option<String>,
177    pub schema: Option<String>,
178    pub table: Option<String>,
179}
180
181/// Compact fact about a set operation.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct SetOperationFact {
185    pub kind: String,
186    pub all: bool,
187    pub distinct: bool,
188    pub output_columns: Vec<String>,
189    pub branches: Vec<SetOperationBranchFact>,
190}
191
192/// Compact facts for one immediate set-operation branch.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct SetOperationBranchFact {
196    pub index: usize,
197    pub role: SetOperationBranchRole,
198    pub projections: Vec<ProjectionFact>,
199}
200
201/// Semantic contribution of one set-operation branch.
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204pub enum SetOperationBranchRole {
205    Value,
206    Filter,
207}
208
209/// High-level kind of transformation performed by a projection.
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "snake_case")]
212pub enum TransformKind {
213    Direct,
214    Cast,
215    Aggregation,
216    Constant,
217    Expression,
218    Star,
219}
220
221/// Confidence level for a compact upstream column reference.
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum ReferenceConfidence {
225    Resolved,
226    Ambiguous,
227    Unknown,
228}
229
230/// Conservative nullability classification for one output projection.
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(rename_all = "snake_case")]
233pub enum ProjectionNullability {
234    NonNull,
235    Nullable,
236    Unknown,
237}
238
239/// Analyze a single SELECT or set-operation query.
240pub fn analyze_query(sql: &str, options: AnalyzeQueryOptions) -> Result<QueryAnalysis> {
241    let mut expression = parse_one(sql, options.dialect)?;
242    expression = effective_query(expression);
243    ensure_query(&expression)?;
244    let original_expression = expression.clone();
245
246    let mapping_schema = options
247        .schema
248        .as_ref()
249        .map(|schema| analysis_mapping_schema(schema, options.dialect));
250    let schema_info = options.schema.as_ref().map(AnalysisSchemaInfo::from_schema);
251    let cte_facts = top_level_cte_facts(&original_expression, options.dialect)?;
252    let star_projections = star_projection_facts(
253        &original_expression,
254        mapping_schema.as_ref(),
255        options.dialect,
256    );
257
258    if let Some(schema) = mapping_schema.as_ref() {
259        use crate::optimizer::qualify_columns::QualifyColumnsError;
260        expression = match qualify_schema_aware_expression(
261            expression.clone(),
262            schema,
263            Some(options.dialect),
264        ) {
265            Ok(qualified) => qualified,
266            // Analysis is not validation. Incomplete schemas and unresolved
267            // lexical references must still yield conservative usage facts.
268            Err(
269                QualifyColumnsError::UnknownTable(_)
270                | QualifyColumnsError::UnknownColumn(_)
271                | QualifyColumnsError::AmbiguousColumn(_)
272                | QualifyColumnsError::ColumnNotResolved { .. },
273            ) => expression,
274            Err(error) => {
275                return Err(Error::internal(format!(
276                    "query analysis qualification failed: {error}"
277                )))
278            }
279        };
280    }
281
282    crate::lineage::expand_cte_stars(
283        &mut expression,
284        mapping_schema.as_ref().map(|schema| schema as &dyn Schema),
285    );
286    annotate_types(
287        &mut expression,
288        mapping_schema.as_ref().map(|schema| schema as &dyn Schema),
289        Some(options.dialect),
290    );
291
292    let scope = build_scope(&expression);
293    let nullability_context = NullabilityContext {
294        schema: schema_info.as_ref(),
295        nullable_sources: nullable_source_names(&expression),
296    };
297    let shape = if is_set_operation(&expression) {
298        QueryShape::SetOperation
299    } else {
300        QueryShape::Select
301    };
302
303    Ok(QueryAnalysis {
304        shape,
305        ctes: collect_cte_names(&expression),
306        cte_facts,
307        projections: projection_facts_for_query(
308            &expression,
309            &scope,
310            options.dialect,
311            &nullability_context,
312        ),
313        relations: relation_facts(&scope, mapping_schema.as_ref(), options.dialect),
314        base_tables: base_table_facts(&scope, mapping_schema.as_ref(), options.dialect),
315        star_projections,
316        set_operations: set_operation_facts(&expression, &scope, options.dialect),
317        column_uses: column_uses::collect(
318            &original_expression,
319            &scope,
320            mapping_schema.as_ref(),
321            options.dialect,
322        ),
323    })
324}
325
326fn analysis_mapping_schema(schema: &ValidationSchema, dialect: DialectType) -> MappingSchema {
327    mapping_schema_from_validation_schema_with_dialect(schema, dialect)
328}
329
330fn validation_table_names(table: &crate::validation::SchemaTable) -> Vec<String> {
331    let mut names = Vec::new();
332
333    names.push(table.name.to_ascii_lowercase());
334    if let Some(schema_name) = &table.schema {
335        names.push(format!(
336            "{}.{}",
337            schema_name.to_ascii_lowercase(),
338            table.name.to_ascii_lowercase()
339        ));
340    }
341    for alias in &table.aliases {
342        names.push(alias.to_ascii_lowercase());
343    }
344
345    names.sort();
346    names.dedup();
347    names
348}
349
350#[derive(Debug, Clone)]
351struct AnalysisColumnInfo {
352    nullable: Option<bool>,
353    primary_key: bool,
354}
355
356#[derive(Debug, Clone)]
357struct AnalysisSchemaInfo {
358    columns: HashMap<(String, String), AnalysisColumnInfo>,
359}
360
361impl AnalysisSchemaInfo {
362    fn from_schema(schema: &ValidationSchema) -> Self {
363        let mut columns = HashMap::new();
364
365        for table in &schema.tables {
366            let table_names = validation_table_names(table);
367            let primary_keys: HashSet<String> = table
368                .primary_key
369                .iter()
370                .map(|column| column.to_ascii_lowercase())
371                .collect();
372
373            for column in &table.columns {
374                let info = AnalysisColumnInfo {
375                    nullable: column.nullable,
376                    primary_key: column.primary_key
377                        || primary_keys.contains(&column.name.to_ascii_lowercase()),
378                };
379
380                for table_name in &table_names {
381                    columns.insert(
382                        (
383                            normalize_lookup_name(table_name),
384                            normalize_lookup_name(&column.name),
385                        ),
386                        info.clone(),
387                    );
388                }
389            }
390        }
391
392        Self { columns }
393    }
394
395    fn column(&self, table: &str, column: &str) -> Option<&AnalysisColumnInfo> {
396        self.columns
397            .get(&(normalize_lookup_name(table), normalize_lookup_name(column)))
398    }
399}
400
401struct NullabilityContext<'a> {
402    schema: Option<&'a AnalysisSchemaInfo>,
403    nullable_sources: HashSet<String>,
404}
405
406fn top_level_cte_facts(expression: &Expression, dialect: DialectType) -> Result<Vec<CteFact>> {
407    let Some(with_clause) = with_clause(expression) else {
408        return Ok(Vec::new());
409    };
410
411    with_clause
412        .ctes
413        .iter()
414        .map(|cte| {
415            Ok(CteFact {
416                name: cte.alias.name.clone(),
417                columns: cte
418                    .columns
419                    .iter()
420                    .map(|column| column.name.clone())
421                    .collect(),
422                body_sql: Dialect::get(dialect).generate(&cte.this)?,
423                output_columns: get_output_column_names_for_dialect(&cte.this, Some(dialect)),
424            })
425        })
426        .collect()
427}
428
429fn star_projection_facts(
430    expression: &Expression,
431    mapping_schema: Option<&MappingSchema>,
432    dialect: DialectType,
433) -> Vec<StarProjectionFact> {
434    let scope = build_scope(expression);
435    let ordered_sources = ordered_source_names_for_query(expression);
436
437    select_expressions_for_query(expression)
438        .iter()
439        .enumerate()
440        .filter_map(|(index, projection)| {
441            let inner = unwrap_projection_alias(projection);
442            if !projection_is_star(inner) {
443                return None;
444            }
445
446            let table = projection_star_table(inner);
447            let expanded_columns = expanded_star_columns(
448                table.as_deref(),
449                &scope,
450                &ordered_sources,
451                mapping_schema,
452                dialect,
453            );
454
455            Some(StarProjectionFact {
456                index,
457                table,
458                expanded_columns,
459            })
460        })
461        .collect()
462}
463
464fn expanded_star_columns(
465    star_table: Option<&str>,
466    scope: &Scope,
467    ordered_sources: &[String],
468    mapping_schema: Option<&MappingSchema>,
469    dialect: DialectType,
470) -> Vec<String> {
471    let mut columns = Vec::new();
472    let mut source_names: Vec<String> = if ordered_sources.is_empty() {
473        let mut names: Vec<_> = scope.sources.keys().cloned().collect();
474        names.sort();
475        names
476    } else {
477        ordered_sources.to_vec()
478    };
479
480    source_names.dedup();
481
482    for source_name in source_names {
483        let Some(source) = scope.sources.get(&source_name) else {
484            continue;
485        };
486
487        if let Some(star_table) = star_table {
488            let matches = source_name.eq_ignore_ascii_case(star_table)
489                || source
490                    .alias
491                    .as_deref()
492                    .is_some_and(|alias| alias.eq_ignore_ascii_case(star_table))
493                || source_table_name(source)
494                    .is_some_and(|table| table.eq_ignore_ascii_case(star_table));
495
496            if !matches {
497                continue;
498            }
499        }
500
501        columns.extend(source_columns(source, mapping_schema, dialect));
502    }
503
504    columns
505}
506
507fn ordered_source_names_for_query(expression: &Expression) -> Vec<String> {
508    match expression {
509        Expression::Select(select) => ordered_source_names_for_select(select),
510        Expression::Union(union) => ordered_source_names_for_query(&union.left),
511        Expression::Intersect(intersect) => ordered_source_names_for_query(&intersect.left),
512        Expression::Except(except) => ordered_source_names_for_query(&except.left),
513        Expression::Subquery(subquery) => ordered_source_names_for_query(&subquery.this),
514        _ => Vec::new(),
515    }
516}
517
518fn ordered_source_names_for_select(select: &crate::expressions::Select) -> Vec<String> {
519    let mut sources = Vec::new();
520
521    if let Some(from) = &select.from {
522        for expression in &from.expressions {
523            if let Some(source_name) = expression_source_name(expression) {
524                sources.push(source_name);
525            }
526        }
527    }
528
529    for join in &select.joins {
530        if let Some(source_name) = expression_source_name(&join.this) {
531            sources.push(source_name);
532        }
533    }
534
535    sources
536}
537
538fn nullable_source_names(expression: &Expression) -> HashSet<String> {
539    match expression {
540        Expression::Select(select) => nullable_source_names_for_select(select),
541        Expression::Union(union) => nullable_source_names(&union.left),
542        Expression::Intersect(intersect) => nullable_source_names(&intersect.left),
543        Expression::Except(except) => nullable_source_names(&except.left),
544        Expression::Subquery(subquery) => nullable_source_names(&subquery.this),
545        _ => HashSet::new(),
546    }
547}
548
549fn nullable_source_names_for_select(select: &crate::expressions::Select) -> HashSet<String> {
550    let mut nullable = HashSet::new();
551    let mut left_sources = Vec::new();
552
553    if let Some(from) = &select.from {
554        for expression in &from.expressions {
555            if let Some(source_name) = expression_source_name(expression) {
556                left_sources.push(source_name);
557            }
558        }
559    }
560
561    for join in &select.joins {
562        let right_source = expression_source_name(&join.this);
563
564        if join_nullable_left(join.kind) {
565            for source_name in &left_sources {
566                nullable.insert(normalize_lookup_name(source_name));
567            }
568        }
569
570        if join_nullable_right(join.kind) {
571            if let Some(source_name) = &right_source {
572                nullable.insert(normalize_lookup_name(source_name));
573            }
574        }
575
576        if let Some(source_name) = right_source {
577            left_sources.push(source_name);
578        }
579    }
580
581    nullable
582}
583
584fn join_nullable_left(kind: JoinKind) -> bool {
585    matches!(
586        kind,
587        JoinKind::Right
588            | JoinKind::NaturalRight
589            | JoinKind::AsOfRight
590            | JoinKind::Full
591            | JoinKind::NaturalFull
592            | JoinKind::Outer
593    )
594}
595
596fn join_nullable_right(kind: JoinKind) -> bool {
597    matches!(
598        kind,
599        JoinKind::Left
600            | JoinKind::NaturalLeft
601            | JoinKind::AsOfLeft
602            | JoinKind::LeftLateral
603            | JoinKind::OuterApply
604            | JoinKind::LeftArray
605            | JoinKind::Full
606            | JoinKind::NaturalFull
607            | JoinKind::Outer
608    )
609}
610
611fn expression_source_name(expression: &Expression) -> Option<String> {
612    match expression {
613        Expression::Table(table) => table
614            .alias
615            .as_ref()
616            .map(|alias| alias.name.clone())
617            .or_else(|| Some(table.name.name.clone())),
618        Expression::Subquery(subquery) => subquery.alias.as_ref().map(|alias| alias.name.clone()),
619        Expression::Alias(alias) => Some(alias.alias.name.clone()),
620        Expression::Cte(cte) => Some(cte.alias.name.clone()),
621        _ => None,
622    }
623}
624
625fn normalize_lookup_name(name: &str) -> String {
626    name.to_ascii_lowercase()
627}
628
629fn effective_query(expression: Expression) -> Expression {
630    match expression {
631        Expression::Prepare(prepare) => prepare.statement,
632        Expression::Subquery(subquery) if subquery.alias.is_none() => subquery.this,
633        other => other,
634    }
635}
636
637fn ensure_query(expression: &Expression) -> Result<()> {
638    if matches!(
639        expression,
640        Expression::Select(_)
641            | Expression::Union(_)
642            | Expression::Intersect(_)
643            | Expression::Except(_)
644    ) {
645        Ok(())
646    } else {
647        Err(Error::internal(
648            "analyze_query requires a SELECT or set operation query",
649        ))
650    }
651}
652
653fn is_set_operation(expression: &Expression) -> bool {
654    matches!(
655        expression,
656        Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_)
657    )
658}
659
660fn collect_cte_names(expression: &Expression) -> Vec<String> {
661    let mut names = Vec::new();
662    let mut seen = HashSet::new();
663    collect_cte_names_inner(expression, &mut names, &mut seen);
664    names
665}
666
667fn collect_cte_names_inner(
668    expression: &Expression,
669    names: &mut Vec<String>,
670    seen: &mut HashSet<String>,
671) {
672    if let Some(with_clause) = with_clause(expression) {
673        collect_with_names(with_clause, names, seen);
674    }
675
676    match expression {
677        Expression::Union(union) => {
678            collect_cte_names_inner(&union.left, names, seen);
679            collect_cte_names_inner(&union.right, names, seen);
680        }
681        Expression::Intersect(intersect) => {
682            collect_cte_names_inner(&intersect.left, names, seen);
683            collect_cte_names_inner(&intersect.right, names, seen);
684        }
685        Expression::Except(except) => {
686            collect_cte_names_inner(&except.left, names, seen);
687            collect_cte_names_inner(&except.right, names, seen);
688        }
689        Expression::Subquery(subquery) => collect_cte_names_inner(&subquery.this, names, seen),
690        _ => {}
691    }
692}
693
694fn collect_with_names(with_clause: &With, names: &mut Vec<String>, seen: &mut HashSet<String>) {
695    for cte in &with_clause.ctes {
696        if seen.insert(cte.alias.name.clone()) {
697            names.push(cte.alias.name.clone());
698        }
699        collect_cte_names_inner(&cte.this, names, seen);
700    }
701}
702
703fn with_clause(expression: &Expression) -> Option<&With> {
704    match expression {
705        Expression::Select(select) => select.with.as_ref(),
706        Expression::Union(union) => union.with.as_ref(),
707        Expression::Intersect(intersect) => intersect.with.as_ref(),
708        Expression::Except(except) => except.with.as_ref(),
709        _ => None,
710    }
711}
712
713fn projection_facts_for_query(
714    expression: &Expression,
715    scope: &Scope,
716    dialect: DialectType,
717    nullability_context: &NullabilityContext<'_>,
718) -> Vec<ProjectionFact> {
719    let expressions = projection_sources_for_query(expression, dialect);
720    let names = get_output_column_names_for_dialect(expression, Some(dialect));
721
722    expressions
723        .iter()
724        .enumerate()
725        .map(|(index, (projection, null_padded))| {
726            let mut fact = projection_fact(
727                index,
728                names
729                    .get(index)
730                    .cloned()
731                    .or_else(|| projection_name(projection)),
732                projection,
733                expression,
734                scope,
735                dialect,
736                nullability_context,
737            );
738            if *null_padded {
739                fact.nullability = ProjectionNullability::Nullable;
740            }
741            fact
742        })
743        .collect()
744}
745
746/// Return one representative projection for each result ordinal together with
747/// whether any immediate name-aligned branch contributes a synthetic NULL.
748fn projection_sources_for_query(
749    expression: &Expression,
750    dialect: DialectType,
751) -> Vec<(&Expression, bool)> {
752    match crate::set_operation::set_operation_layout(expression, Some(dialect)) {
753        Ok(Some(layout)) => layout
754            .outputs
755            .iter()
756            .filter_map(|output| {
757                let null_padded = output.left_ordinal.is_none() || output.right_ordinal.is_none();
758                output
759                    .left_ordinal
760                    .and_then(|ordinal| {
761                        projection_source_for_ordinal(
762                            set_operation_left(expression)?,
763                            ordinal,
764                            dialect,
765                        )
766                    })
767                    .or_else(|| {
768                        output.right_ordinal.and_then(|ordinal| {
769                            projection_source_for_ordinal(
770                                set_operation_right(expression)?,
771                                ordinal,
772                                dialect,
773                            )
774                        })
775                    })
776                    .map(|(projection, nested_null_padded)| {
777                        (projection, null_padded || nested_null_padded)
778                    })
779            })
780            .collect(),
781        _ => select_expressions_for_query(expression)
782            .into_iter()
783            .map(|projection| (projection, false))
784            .collect(),
785    }
786}
787
788fn projection_source_for_ordinal(
789    expression: &Expression,
790    ordinal: usize,
791    dialect: DialectType,
792) -> Option<(&Expression, bool)> {
793    match crate::set_operation::set_operation_layout(expression, Some(dialect)) {
794        Ok(Some(layout)) => {
795            let output = layout.outputs.get(ordinal)?;
796            let null_padded = output.left_ordinal.is_none() || output.right_ordinal.is_none();
797            output
798                .left_ordinal
799                .and_then(|child_ordinal| {
800                    projection_source_for_ordinal(
801                        set_operation_left(expression)?,
802                        child_ordinal,
803                        dialect,
804                    )
805                })
806                .or_else(|| {
807                    output.right_ordinal.and_then(|child_ordinal| {
808                        projection_source_for_ordinal(
809                            set_operation_right(expression)?,
810                            child_ordinal,
811                            dialect,
812                        )
813                    })
814                })
815                .map(|(projection, nested_null_padded)| {
816                    (projection, null_padded || nested_null_padded)
817                })
818        }
819        _ => match expression {
820            Expression::Select(select) => select
821                .expressions
822                .get(ordinal)
823                .map(|projection| (projection, false)),
824            Expression::Union(union) => {
825                projection_source_for_ordinal(&union.left, ordinal, dialect)
826            }
827            Expression::Intersect(intersect) => {
828                projection_source_for_ordinal(&intersect.left, ordinal, dialect)
829            }
830            Expression::Except(except) => {
831                projection_source_for_ordinal(&except.left, ordinal, dialect)
832            }
833            Expression::Subquery(subquery) => {
834                projection_source_for_ordinal(&subquery.this, ordinal, dialect)
835            }
836            Expression::Paren(paren) => {
837                projection_source_for_ordinal(&paren.this, ordinal, dialect)
838            }
839            _ => None,
840        },
841    }
842}
843
844fn set_operation_left(expression: &Expression) -> Option<&Expression> {
845    match expression {
846        Expression::Union(set_op) => Some(&set_op.left),
847        Expression::Intersect(set_op) => Some(&set_op.left),
848        Expression::Except(set_op) => Some(&set_op.left),
849        _ => None,
850    }
851}
852
853fn set_operation_right(expression: &Expression) -> Option<&Expression> {
854    match expression {
855        Expression::Union(set_op) => Some(&set_op.right),
856        Expression::Intersect(set_op) => Some(&set_op.right),
857        Expression::Except(set_op) => Some(&set_op.right),
858        _ => None,
859    }
860}
861
862fn select_expressions_for_query(expression: &Expression) -> Vec<&Expression> {
863    match expression {
864        Expression::Select(select) => select.expressions.iter().collect(),
865        Expression::Union(union) => select_expressions_for_query(&union.left),
866        Expression::Intersect(intersect) => select_expressions_for_query(&intersect.left),
867        Expression::Except(except) => select_expressions_for_query(&except.left),
868        Expression::Subquery(subquery) => select_expressions_for_query(&subquery.this),
869        _ => Vec::new(),
870    }
871}
872
873fn projection_fact(
874    index: usize,
875    name: Option<String>,
876    projection: &Expression,
877    query: &Expression,
878    scope: &Scope,
879    dialect: DialectType,
880    nullability_context: &NullabilityContext<'_>,
881) -> ProjectionFact {
882    let inner = unwrap_projection_alias(projection);
883    let is_star = projection_is_star(inner);
884    let upstream = lineage_by_index_from_expression(index, query, Some(dialect), false)
885        .map(|node| terminal_references_from_lineage(&node))
886        .ok()
887        .filter(|refs| !refs.is_empty())
888        .unwrap_or_else(|| fallback_column_references(inner, scope));
889
890    ProjectionFact {
891        index,
892        name,
893        is_star,
894        star_table: projection_star_table(inner),
895        transform_kind: transform_kind(inner),
896        transform_function: transform_function_fact(inner, scope, dialect),
897        cast_type: cast_type(inner, dialect),
898        type_hint: projection
899            .inferred_type()
900            .or_else(|| inner.inferred_type())
901            .and_then(|data_type| render_data_type(data_type, dialect)),
902        nullability: projection_nullability(inner, scope, nullability_context),
903        upstream,
904    }
905}
906
907fn transform_function_fact(
908    expression: &Expression,
909    scope: &Scope,
910    dialect: DialectType,
911) -> Option<TransformFunctionFact> {
912    let mut matches = expression
913        .find_all(|candidate| transform_function_fact_for_node(candidate, scope, dialect).is_some())
914        .into_iter();
915
916    let first = matches.next()?;
917    if matches.next().is_some() {
918        return None;
919    }
920
921    transform_function_fact_for_node(first, scope, dialect)
922}
923
924fn transform_function_fact_for_node(
925    expression: &Expression,
926    scope: &Scope,
927    dialect: DialectType,
928) -> Option<TransformFunctionFact> {
929    match expression {
930        Expression::Function(function) => Some(transform_function_from_args(
931            &function.name,
932            &function.args,
933            scope,
934            dialect,
935        )),
936        Expression::AggregateFunction(function) => Some(transform_function_from_args(
937            &function.name,
938            &function.args,
939            scope,
940            dialect,
941        )),
942        Expression::DateTrunc(function) => Some(transform_function_from_parts(
943            "DATE_TRUNC",
944            vec![datetime_field_name(&function.unit)],
945            vec![&function.this],
946            scope,
947            dialect,
948        )),
949        Expression::TimestampTrunc(function) => Some(transform_function_from_parts(
950            "TIMESTAMP_TRUNC",
951            vec![datetime_field_name(&function.unit)],
952            vec![&function.this],
953            scope,
954            dialect,
955        )),
956        Expression::TimeTrunc(function) => {
957            let mut args = vec![function.this.as_ref()];
958            if let Some(zone) = function.zone.as_deref() {
959                args.push(zone);
960            }
961            Some(transform_function_from_parts(
962                "TIME_TRUNC",
963                vec![function.unit.clone()],
964                args,
965                scope,
966                dialect,
967            ))
968        }
969        Expression::Extract(function) => Some(transform_function_from_parts(
970            "EXTRACT",
971            vec![datetime_field_name(&function.field)],
972            vec![&function.this],
973            scope,
974            dialect,
975        )),
976        Expression::DateAdd(function) => Some(transform_function_from_parts(
977            "DATE_ADD",
978            Vec::new(),
979            vec![&function.this, &function.interval],
980            scope,
981            dialect,
982        )),
983        Expression::DateSub(function) => Some(transform_function_from_parts(
984            "DATE_SUB",
985            Vec::new(),
986            vec![&function.this, &function.interval],
987            scope,
988            dialect,
989        )),
990        Expression::DateDiff(function) => Some(transform_function_from_parts(
991            "DATE_DIFF",
992            Vec::new(),
993            vec![&function.this, &function.expression],
994            scope,
995            dialect,
996        )),
997        _ => None,
998    }
999}
1000
1001fn transform_function_from_args(
1002    name: &str,
1003    args: &[Expression],
1004    scope: &Scope,
1005    dialect: DialectType,
1006) -> TransformFunctionFact {
1007    let literal_args = args
1008        .iter()
1009        .filter_map(|arg| literal_argument(arg, dialect))
1010        .collect();
1011    transform_function_from_parts(name, literal_args, args.iter().collect(), scope, dialect)
1012}
1013
1014fn transform_function_from_parts(
1015    name: &str,
1016    literal_args: Vec<String>,
1017    args: Vec<&Expression>,
1018    scope: &Scope,
1019    _dialect: DialectType,
1020) -> TransformFunctionFact {
1021    let column_args = dedupe_column_refs(
1022        args.into_iter()
1023            .flat_map(|arg| fallback_column_references(arg, scope))
1024            .collect(),
1025    );
1026
1027    TransformFunctionFact {
1028        name: name.to_string(),
1029        literal_args,
1030        column_args,
1031    }
1032}
1033
1034fn literal_argument(expression: &Expression, dialect: DialectType) -> Option<String> {
1035    match expression {
1036        Expression::Literal(literal) => Some(literal.value_str().to_string()),
1037        Expression::Boolean(boolean) => Some(boolean.value.to_string()),
1038        Expression::Null(_) => Some("NULL".to_string()),
1039        Expression::Identifier(identifier) => Some(identifier.name.clone()),
1040        Expression::Var(var) => Some(var.this.clone()),
1041        Expression::DataType(data_type) => render_data_type(data_type, dialect),
1042        _ => None,
1043    }
1044}
1045
1046fn datetime_field_name(field: &crate::expressions::DateTimeField) -> String {
1047    match field {
1048        crate::expressions::DateTimeField::Year => "year".to_string(),
1049        crate::expressions::DateTimeField::Month => "month".to_string(),
1050        crate::expressions::DateTimeField::Day => "day".to_string(),
1051        crate::expressions::DateTimeField::Hour => "hour".to_string(),
1052        crate::expressions::DateTimeField::Minute => "minute".to_string(),
1053        crate::expressions::DateTimeField::Second => "second".to_string(),
1054        crate::expressions::DateTimeField::Millisecond => "millisecond".to_string(),
1055        crate::expressions::DateTimeField::Microsecond => "microsecond".to_string(),
1056        crate::expressions::DateTimeField::DayOfWeek => "day_of_week".to_string(),
1057        crate::expressions::DateTimeField::DayOfYear => "day_of_year".to_string(),
1058        crate::expressions::DateTimeField::Week => "week".to_string(),
1059        crate::expressions::DateTimeField::WeekWithModifier(modifier) => {
1060            format!("week({modifier})")
1061        }
1062        crate::expressions::DateTimeField::Quarter => "quarter".to_string(),
1063        crate::expressions::DateTimeField::Epoch => "epoch".to_string(),
1064        crate::expressions::DateTimeField::Timezone => "timezone".to_string(),
1065        crate::expressions::DateTimeField::TimezoneHour => "timezone_hour".to_string(),
1066        crate::expressions::DateTimeField::TimezoneMinute => "timezone_minute".to_string(),
1067        crate::expressions::DateTimeField::Date => "date".to_string(),
1068        crate::expressions::DateTimeField::Time => "time".to_string(),
1069        crate::expressions::DateTimeField::Custom(name) => name.clone(),
1070    }
1071}
1072
1073fn unwrap_projection_alias(expression: &Expression) -> &Expression {
1074    match expression {
1075        Expression::Alias(alias) => unwrap_projection_alias(&alias.this),
1076        Expression::Annotated(annotated) => unwrap_projection_alias(&annotated.this),
1077        Expression::Paren(paren) => unwrap_projection_alias(&paren.this),
1078        _ => expression,
1079    }
1080}
1081
1082fn projection_name(expression: &Expression) -> Option<String> {
1083    match expression {
1084        Expression::Alias(alias) => Some(alias.alias.name.clone()),
1085        Expression::Column(column) => Some(column.name.name.clone()),
1086        Expression::Identifier(identifier) => Some(identifier.name.clone()),
1087        Expression::Star(_) => Some("*".to_string()),
1088        Expression::Annotated(annotated) => projection_name(&annotated.this),
1089        _ => None,
1090    }
1091}
1092
1093fn projection_is_star(expression: &Expression) -> bool {
1094    matches!(expression, Expression::Star(_))
1095        || matches!(expression, Expression::Column(column) if column.name.name == "*")
1096}
1097
1098fn projection_star_table(expression: &Expression) -> Option<String> {
1099    match expression {
1100        Expression::Star(star) => star
1101            .table
1102            .as_ref()
1103            .map(|identifier| identifier.name.clone()),
1104        Expression::Column(column) if column.name.name == "*" => column
1105            .table
1106            .as_ref()
1107            .map(|identifier| identifier.name.clone()),
1108        _ => None,
1109    }
1110}
1111
1112fn transform_kind(expression: &Expression) -> TransformKind {
1113    if projection_is_star(expression) {
1114        TransformKind::Star
1115    } else if is_cast_expression(expression) {
1116        TransformKind::Cast
1117    } else if contains_aggregate(expression) {
1118        TransformKind::Aggregation
1119    } else if matches!(
1120        expression,
1121        Expression::Column(_) | Expression::Identifier(_)
1122    ) {
1123        TransformKind::Direct
1124    } else if is_simple_constant(expression) {
1125        TransformKind::Constant
1126    } else {
1127        TransformKind::Expression
1128    }
1129}
1130
1131fn is_cast_expression(expression: &Expression) -> bool {
1132    matches!(
1133        expression,
1134        Expression::Cast(_) | Expression::TryCast(_) | Expression::SafeCast(_)
1135    )
1136}
1137
1138fn cast_type(expression: &Expression, dialect: DialectType) -> Option<String> {
1139    match expression {
1140        Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
1141            render_data_type(&cast.to, dialect)
1142        }
1143        _ => None,
1144    }
1145}
1146
1147fn render_data_type(data_type: &DataType, dialect: DialectType) -> Option<String> {
1148    Dialect::get(dialect)
1149        .generate(&Expression::DataType(data_type.clone()))
1150        .ok()
1151}
1152
1153fn is_simple_constant(expression: &Expression) -> bool {
1154    match expression {
1155        Expression::Literal(_) | Expression::Boolean(_) | Expression::Null(_) => true,
1156        Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
1157            is_simple_constant(&cast.this)
1158        }
1159        Expression::Neg(unary) | Expression::BitwiseNot(unary) => is_simple_constant(&unary.this),
1160        _ => false,
1161    }
1162}
1163
1164fn projection_nullability(
1165    expression: &Expression,
1166    scope: &Scope,
1167    context: &NullabilityContext<'_>,
1168) -> ProjectionNullability {
1169    match expression {
1170        Expression::Alias(alias) => projection_nullability(&alias.this, scope, context),
1171        Expression::Annotated(annotated) => projection_nullability(&annotated.this, scope, context),
1172        Expression::Paren(paren) => projection_nullability(&paren.this, scope, context),
1173        Expression::Literal(_) | Expression::Boolean(_) => ProjectionNullability::NonNull,
1174        Expression::Null(_) => ProjectionNullability::Nullable,
1175        Expression::Count(_) | Expression::CountIf(_) => ProjectionNullability::NonNull,
1176        Expression::Cast(cast) => projection_nullability(&cast.this, scope, context),
1177        Expression::TryCast(_) | Expression::SafeCast(_) => ProjectionNullability::Unknown,
1178        Expression::Column(column) => column_nullability(
1179            &column.name.name,
1180            column.table.as_ref().map(|table| table.name.as_str()),
1181            scope,
1182            context,
1183        ),
1184        Expression::Identifier(identifier) => {
1185            column_nullability(&identifier.name, None, scope, context)
1186        }
1187        Expression::Coalesce(func) => coalesce_nullability(&func.expressions, scope, context),
1188        _ => ProjectionNullability::Unknown,
1189    }
1190}
1191
1192fn column_nullability(
1193    column_name: &str,
1194    source_name: Option<&str>,
1195    scope: &Scope,
1196    context: &NullabilityContext<'_>,
1197) -> ProjectionNullability {
1198    let resolved_source_name = source_name
1199        .map(str::to_string)
1200        .or_else(|| single_scope_source_name(scope));
1201
1202    if let Some(source_name) = &resolved_source_name {
1203        if context
1204            .nullable_sources
1205            .contains(&normalize_lookup_name(source_name))
1206        {
1207            return ProjectionNullability::Nullable;
1208        }
1209    }
1210
1211    let Some(schema) = context.schema else {
1212        return ProjectionNullability::Unknown;
1213    };
1214
1215    let table_name = resolved_source_name
1216        .as_ref()
1217        .and_then(|name| scope.sources.get(name).and_then(source_table_name))
1218        .or(resolved_source_name);
1219
1220    let Some(table_name) = table_name else {
1221        return ProjectionNullability::Unknown;
1222    };
1223
1224    match schema.column(&table_name, column_name) {
1225        Some(info) if info.primary_key || info.nullable == Some(false) => {
1226            ProjectionNullability::NonNull
1227        }
1228        Some(info) if info.nullable == Some(true) => ProjectionNullability::Nullable,
1229        Some(_) | None => ProjectionNullability::Unknown,
1230    }
1231}
1232
1233fn single_scope_source_name(scope: &Scope) -> Option<String> {
1234    if scope.sources.len() == 1 {
1235        scope.sources.keys().next().cloned()
1236    } else {
1237        None
1238    }
1239}
1240
1241fn coalesce_nullability(
1242    expressions: &[Expression],
1243    scope: &Scope,
1244    context: &NullabilityContext<'_>,
1245) -> ProjectionNullability {
1246    if expressions.is_empty() {
1247        return ProjectionNullability::Unknown;
1248    }
1249
1250    let mut all_nullable = true;
1251
1252    for expression in expressions {
1253        match projection_nullability(unwrap_projection_alias(expression), scope, context) {
1254            ProjectionNullability::NonNull => return ProjectionNullability::NonNull,
1255            ProjectionNullability::Nullable => {}
1256            ProjectionNullability::Unknown => all_nullable = false,
1257        }
1258    }
1259
1260    if all_nullable {
1261        ProjectionNullability::Nullable
1262    } else {
1263        ProjectionNullability::Unknown
1264    }
1265}
1266
1267fn terminal_references_from_lineage(node: &LineageNode) -> Vec<ColumnReferenceFact> {
1268    let mut refs = Vec::new();
1269    collect_terminal_references(node, &mut refs);
1270    dedupe_column_refs(refs)
1271}
1272
1273fn collect_terminal_references(node: &LineageNode, refs: &mut Vec<ColumnReferenceFact>) {
1274    if node.downstream.is_empty() {
1275        if let Some(reference) = column_reference_from_lineage_node(node) {
1276            refs.push(reference);
1277        }
1278        return;
1279    }
1280
1281    for child in &node.downstream {
1282        collect_terminal_references(child, refs);
1283    }
1284}
1285
1286fn column_reference_from_lineage_node(node: &LineageNode) -> Option<ColumnReferenceFact> {
1287    match &node.expression {
1288        Expression::Column(column) => {
1289            let source_name = non_empty_string(node.source_name.clone());
1290            let table =
1291                lineage_node_table(node).or_else(|| column.table.as_ref().map(|t| t.name.clone()));
1292            let confidence = if node.source_kind == SourceKind::Unknown && source_name.is_none() {
1293                ReferenceConfidence::Unknown
1294            } else {
1295                ReferenceConfidence::Resolved
1296            };
1297            Some(ColumnReferenceFact {
1298                source_name,
1299                source_alias: node.source_alias.clone(),
1300                source_kind: node.source_kind,
1301                table,
1302                column: column.name.name.clone(),
1303                unqualified: column.table.is_none(),
1304                confidence,
1305            })
1306        }
1307        Expression::Star(_) => Some(ColumnReferenceFact {
1308            source_name: non_empty_string(node.source_name.clone()),
1309            source_alias: node.source_alias.clone(),
1310            source_kind: node.source_kind,
1311            table: lineage_node_table(node),
1312            column: "*".to_string(),
1313            unqualified: true,
1314            confidence: if node.source_kind == SourceKind::Unknown {
1315                ReferenceConfidence::Unknown
1316            } else {
1317                ReferenceConfidence::Resolved
1318            },
1319        }),
1320        _ => None,
1321    }
1322}
1323
1324fn lineage_node_table(node: &LineageNode) -> Option<String> {
1325    match &node.source {
1326        Expression::Table(table) => Some(table_name(table)),
1327        _ => None,
1328    }
1329}
1330
1331fn fallback_column_references(expression: &Expression, scope: &Scope) -> Vec<ColumnReferenceFact> {
1332    let mut refs = Vec::new();
1333    let source_count = scope.sources.len();
1334    let single_source = if source_count == 1 {
1335        scope.sources.iter().next()
1336    } else {
1337        None
1338    };
1339
1340    for column_expr in expression.find_all(|candidate| matches!(candidate, Expression::Column(_))) {
1341        if let Expression::Column(column) = column_expr {
1342            if column.name.name == "*" {
1343                continue;
1344            }
1345            let source = column
1346                .table
1347                .as_ref()
1348                .and_then(|table| scope.sources.get(&table.name));
1349            let (source_name, source_alias, source_kind, table, confidence) =
1350                if let Some(table_identifier) = &column.table {
1351                    if let Some(source) = source {
1352                        (
1353                            Some(table_identifier.name.clone()),
1354                            source.alias.clone(),
1355                            source.kind,
1356                            source_table_name(source)
1357                                .or_else(|| Some(table_identifier.name.clone())),
1358                            ReferenceConfidence::Resolved,
1359                        )
1360                    } else {
1361                        (
1362                            Some(table_identifier.name.clone()),
1363                            None,
1364                            SourceKind::Unknown,
1365                            Some(table_identifier.name.clone()),
1366                            ReferenceConfidence::Unknown,
1367                        )
1368                    }
1369                } else if let Some((name, source)) = single_source {
1370                    (
1371                        Some(name.clone()),
1372                        source.alias.clone(),
1373                        source.kind,
1374                        source_table_name(source).or_else(|| Some(name.clone())),
1375                        ReferenceConfidence::Resolved,
1376                    )
1377                } else if source_count > 1 {
1378                    (
1379                        None,
1380                        None,
1381                        SourceKind::Unknown,
1382                        None,
1383                        ReferenceConfidence::Ambiguous,
1384                    )
1385                } else {
1386                    (
1387                        None,
1388                        None,
1389                        SourceKind::Unknown,
1390                        None,
1391                        ReferenceConfidence::Unknown,
1392                    )
1393                };
1394
1395            refs.push(ColumnReferenceFact {
1396                source_name,
1397                source_alias,
1398                source_kind,
1399                table,
1400                column: column.name.name.clone(),
1401                unqualified: column.table.is_none(),
1402                confidence,
1403            });
1404        }
1405    }
1406
1407    dedupe_column_refs(refs)
1408}
1409
1410fn dedupe_column_refs(refs: Vec<ColumnReferenceFact>) -> Vec<ColumnReferenceFact> {
1411    let mut seen = HashSet::new();
1412    let mut deduped = Vec::new();
1413
1414    for reference in refs {
1415        let key = (
1416            reference.source_name.clone(),
1417            reference.source_alias.clone(),
1418            reference.table.clone(),
1419            reference.column.clone(),
1420            format!("{:?}", reference.source_kind),
1421            reference.unqualified,
1422            format!("{:?}", reference.confidence),
1423        );
1424        if seen.insert(key) {
1425            deduped.push(reference);
1426        }
1427    }
1428
1429    deduped
1430}
1431
1432fn relation_facts(
1433    scope: &Scope,
1434    mapping_schema: Option<&crate::schema::MappingSchema>,
1435    dialect: DialectType,
1436) -> Vec<RelationFact> {
1437    let mut relations = Vec::new();
1438    let mut seen = HashSet::new();
1439    collect_relation_facts(scope, mapping_schema, dialect, &mut seen, &mut relations);
1440
1441    relations.sort_by(|left, right| {
1442        left.name
1443            .cmp(&right.name)
1444            .then_with(|| left.alias.cmp(&right.alias))
1445    });
1446    relations
1447}
1448
1449fn collect_relation_facts(
1450    scope: &Scope,
1451    mapping_schema: Option<&crate::schema::MappingSchema>,
1452    dialect: DialectType,
1453    seen: &mut HashSet<String>,
1454    relations: &mut Vec<RelationFact>,
1455) {
1456    for relation in scope.sources.iter().map(|(source_name, source)| {
1457        let identity = source_table_identity(source);
1458        RelationFact {
1459            name: source
1460                .lineage_name
1461                .clone()
1462                .or_else(|| identity.as_ref().map(|identity| identity.name.clone()))
1463                .unwrap_or_else(|| source_name.clone()),
1464            alias: source.alias.clone().or_else(|| source_alias(source)),
1465            kind: source.kind,
1466            columns: source_columns(source, mapping_schema, dialect),
1467            catalog: identity
1468                .as_ref()
1469                .and_then(|identity| identity.catalog.clone()),
1470            schema: identity
1471                .as_ref()
1472                .and_then(|identity| identity.schema.clone()),
1473            table: identity
1474                .as_ref()
1475                .and_then(|identity| identity.table.clone()),
1476        }
1477    }) {
1478        let key = format!("{:?}|{}|{:?}", relation.kind, relation.name, relation.alias);
1479        if seen.insert(key) {
1480            relations.push(relation);
1481        }
1482    }
1483
1484    for branch_scope in &scope.union_scopes {
1485        collect_relation_facts(branch_scope, mapping_schema, dialect, seen, relations);
1486    }
1487}
1488
1489fn base_table_facts(
1490    scope: &Scope,
1491    mapping_schema: Option<&crate::schema::MappingSchema>,
1492    dialect: DialectType,
1493) -> Vec<RelationFact> {
1494    let mut relations = Vec::new();
1495    let mut seen = HashSet::new();
1496
1497    collect_base_table_facts(scope, mapping_schema, dialect, &mut seen, &mut relations);
1498
1499    relations.sort_by(|left, right| left.name.cmp(&right.name));
1500    relations
1501}
1502
1503fn collect_base_table_facts(
1504    scope: &Scope,
1505    mapping_schema: Option<&crate::schema::MappingSchema>,
1506    dialect: DialectType,
1507    seen: &mut HashSet<String>,
1508    relations: &mut Vec<RelationFact>,
1509) {
1510    for source in scope.sources.values() {
1511        if source.kind != SourceKind::Table {
1512            continue;
1513        }
1514
1515        let Some(identity) = source_table_identity(source) else {
1516            continue;
1517        };
1518
1519        if seen.insert(identity.name.clone()) {
1520            relations.push(RelationFact {
1521                name: identity.name,
1522                alias: source.alias.clone().or_else(|| source_alias(source)),
1523                kind: SourceKind::Table,
1524                columns: source_columns(source, mapping_schema, dialect),
1525                catalog: identity.catalog,
1526                schema: identity.schema,
1527                table: identity.table,
1528            });
1529        }
1530    }
1531
1532    for child_scope in scope
1533        .cte_scopes
1534        .iter()
1535        .chain(scope.union_scopes.iter())
1536        .chain(scope.table_scopes.iter())
1537        .chain(scope.derived_table_scopes.iter())
1538        .chain(scope.subquery_scopes.iter())
1539    {
1540        collect_base_table_facts(child_scope, mapping_schema, dialect, seen, relations);
1541    }
1542}
1543
1544fn source_columns(
1545    source: &SourceInfo,
1546    mapping_schema: Option<&crate::schema::MappingSchema>,
1547    dialect: DialectType,
1548) -> Vec<String> {
1549    match &source.expression {
1550        Expression::Table(table) => mapping_schema
1551            .and_then(|schema| schema.column_names(&table_name(table)).ok())
1552            .unwrap_or_default(),
1553        Expression::Select(_)
1554        | Expression::Union(_)
1555        | Expression::Intersect(_)
1556        | Expression::Except(_) => {
1557            get_output_column_names_for_dialect(&source.expression, Some(dialect))
1558        }
1559        Expression::Subquery(subquery) => {
1560            get_output_column_names_for_dialect(&subquery.this, Some(dialect))
1561        }
1562        Expression::Cte(cte) if !cte.columns.is_empty() => cte
1563            .columns
1564            .iter()
1565            .map(|column| column.name.clone())
1566            .collect(),
1567        Expression::Cte(cte) => get_output_column_names_for_dialect(&cte.this, Some(dialect)),
1568        _ => Vec::new(),
1569    }
1570}
1571
1572fn source_table_name(source: &SourceInfo) -> Option<String> {
1573    source_table_identity(source).map(|identity| identity.name)
1574}
1575
1576fn source_alias(source: &SourceInfo) -> Option<String> {
1577    match &source.expression {
1578        Expression::Table(table) => table.alias.as_ref().map(|alias| alias.name.clone()),
1579        Expression::Subquery(subquery) => subquery.alias.as_ref().map(|alias| alias.name.clone()),
1580        _ => None,
1581    }
1582}
1583
1584fn table_name(table: &TableRef) -> String {
1585    let mut parts = Vec::new();
1586    if let Some(catalog) = &table.catalog {
1587        parts.push(catalog.name.clone());
1588    }
1589    if let Some(schema) = &table.schema {
1590        parts.push(schema.name.clone());
1591    }
1592    parts.push(table.name.name.clone());
1593    parts.join(".")
1594}
1595
1596#[derive(Debug, Clone)]
1597struct RelationIdentity {
1598    name: String,
1599    catalog: Option<String>,
1600    schema: Option<String>,
1601    table: Option<String>,
1602}
1603
1604fn source_table_identity(source: &SourceInfo) -> Option<RelationIdentity> {
1605    match &source.expression {
1606        Expression::Table(table) => Some(table_identity(table)),
1607        _ => None,
1608    }
1609}
1610
1611fn table_identity(table: &TableRef) -> RelationIdentity {
1612    RelationIdentity {
1613        name: table_name(table),
1614        catalog: table.catalog.as_ref().map(|catalog| catalog.name.clone()),
1615        schema: table.schema.as_ref().map(|schema| schema.name.clone()),
1616        table: Some(table.name.name.clone()),
1617    }
1618}
1619
1620fn set_operation_facts(
1621    expression: &Expression,
1622    scope: &Scope,
1623    dialect: DialectType,
1624) -> Vec<SetOperationFact> {
1625    let mut facts = Vec::new();
1626    collect_set_operation_facts(expression, scope, dialect, &mut facts);
1627    facts
1628}
1629
1630fn collect_set_operation_facts(
1631    expression: &Expression,
1632    scope: &Scope,
1633    dialect: DialectType,
1634    facts: &mut Vec<SetOperationFact>,
1635) {
1636    match expression {
1637        Expression::Union(union) => {
1638            facts.push(SetOperationFact {
1639                kind: "union".to_string(),
1640                all: union.all,
1641                distinct: union.distinct,
1642                output_columns: get_output_column_names_for_dialect(expression, Some(dialect)),
1643                branches: set_operation_branches(
1644                    &union.left,
1645                    &union.right,
1646                    scope,
1647                    dialect,
1648                    SetOperationBranchRole::Value,
1649                ),
1650            });
1651            collect_set_operation_facts(&union.left, scope, dialect, facts);
1652            collect_set_operation_facts(&union.right, scope, dialect, facts);
1653        }
1654        Expression::Intersect(intersect) => {
1655            facts.push(SetOperationFact {
1656                kind: "intersect".to_string(),
1657                all: intersect.all,
1658                distinct: intersect.distinct,
1659                output_columns: get_output_column_names_for_dialect(expression, Some(dialect)),
1660                branches: set_operation_branches(
1661                    &intersect.left,
1662                    &intersect.right,
1663                    scope,
1664                    dialect,
1665                    SetOperationBranchRole::Filter,
1666                ),
1667            });
1668            collect_set_operation_facts(&intersect.left, scope, dialect, facts);
1669            collect_set_operation_facts(&intersect.right, scope, dialect, facts);
1670        }
1671        Expression::Except(except) => {
1672            facts.push(SetOperationFact {
1673                kind: "except".to_string(),
1674                all: except.all,
1675                distinct: except.distinct,
1676                output_columns: get_output_column_names_for_dialect(expression, Some(dialect)),
1677                branches: set_operation_branches(
1678                    &except.left,
1679                    &except.right,
1680                    scope,
1681                    dialect,
1682                    SetOperationBranchRole::Filter,
1683                ),
1684            });
1685            collect_set_operation_facts(&except.left, scope, dialect, facts);
1686            collect_set_operation_facts(&except.right, scope, dialect, facts);
1687        }
1688        Expression::Subquery(subquery) => {
1689            collect_set_operation_facts(&subquery.this, scope, dialect, facts);
1690        }
1691        _ => {}
1692    }
1693}
1694
1695fn set_operation_branches(
1696    left: &Expression,
1697    right: &Expression,
1698    scope: &Scope,
1699    dialect: DialectType,
1700    right_role: SetOperationBranchRole,
1701) -> Vec<SetOperationBranchFact> {
1702    vec![
1703        SetOperationBranchFact {
1704            index: 0,
1705            role: SetOperationBranchRole::Value,
1706            projections: projection_facts_for_branch(left, scope, dialect),
1707        },
1708        SetOperationBranchFact {
1709            index: 1,
1710            role: right_role,
1711            projections: projection_facts_for_branch(right, scope, dialect),
1712        },
1713    ]
1714}
1715
1716fn projection_facts_for_branch(
1717    expression: &Expression,
1718    root_scope: &Scope,
1719    dialect: DialectType,
1720) -> Vec<ProjectionFact> {
1721    let branch_scope = build_scope(expression);
1722    let scope = if branch_scope.sources.is_empty() {
1723        root_scope
1724    } else {
1725        &branch_scope
1726    };
1727    let nullability_context = NullabilityContext {
1728        schema: None,
1729        nullable_sources: nullable_source_names(expression),
1730    };
1731    projection_facts_for_query(expression, scope, dialect, &nullability_context)
1732}
1733
1734fn non_empty_string(value: String) -> Option<String> {
1735    if value.is_empty() {
1736        None
1737    } else {
1738        Some(value)
1739    }
1740}