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