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, Identifier, JoinKind, TableRef, With};
11use crate::lineage::{LineageNode, ScopedLineage};
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_with_options, ComplexityGuardOptions, Error, ParseOptions, Result};
19use serde::{Deserialize, Serialize};
20use std::cell::RefCell;
21use std::collections::{HashMap, HashSet};
22
23mod column_uses;
24
25/// Options for [`analyze_query`].
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
28pub struct AnalyzeQueryOptions {
29    /// Per-call limits used for parsing and rendering analysis facts.
30    pub complexity_guard: Option<ComplexityGuardOptions>,
31    /// SQL dialect used for parsing and dialect-aware rendering.
32    pub dialect: DialectType,
33    /// Optional validation schema used for qualification and type annotation.
34    pub schema: Option<ValidationSchema>,
35}
36
37/// Compact facts about a query's output shape and data dependencies.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct QueryAnalysis {
41    pub shape: QueryShape,
42    pub ctes: Vec<String>,
43    pub cte_facts: Vec<CteFact>,
44    pub projections: Vec<ProjectionFact>,
45    pub relations: Vec<RelationFact>,
46    pub base_tables: Vec<RelationFact>,
47    pub star_projections: Vec<StarProjectionFact>,
48    pub set_operations: Vec<SetOperationFact>,
49    /// Clause-specific uses, separate from output projection lineage.
50    #[serde(default)]
51    pub column_uses: Vec<ColumnUseFact>,
52}
53
54/// A half-open range in the original SQL, measured in Unicode characters.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56pub struct QuerySourceSpan {
57    pub start: usize,
58    pub end: usize,
59}
60
61/// The syntactic role of a column-containing expression.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum ColumnUseContext {
65    Join,
66    Filter,
67    Group,
68    Having,
69    Qualify,
70    WindowPartition,
71    WindowOrder,
72    WindowFrame,
73    Order,
74    AggregateOrder,
75    SetOperationFilter,
76}
77
78/// One resolved dependency of an original column occurrence. Several terminal
79/// dependencies can share a span (for example, a reference to a computed CTE
80/// column). Spans identify the use, not the upstream column's definition.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ColumnUseReferenceFact {
84    #[serde(flatten)]
85    pub reference: ColumnReferenceFact,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub span: Option<QuerySourceSpan>,
88}
89
90/// One containing expression, with references in occurrence order. Paths are
91/// deterministic within an analysis, not persistent IDs across SQL edits.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase")]
94pub struct ColumnUseFact {
95    pub context: ColumnUseContext,
96    pub scope_path: String,
97    pub expression_path: String,
98    /// Dialect-rendered SQL; not necessarily the original source substring.
99    pub expression_sql: String,
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub span: Option<QuerySourceSpan>,
102    pub references: Vec<ColumnUseReferenceFact>,
103}
104
105/// Top-level query shape.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "snake_case")]
108pub enum QueryShape {
109    Select,
110    SetOperation,
111}
112
113/// Compact fact about one output projection.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct ProjectionFact {
117    pub index: usize,
118    pub name: Option<String>,
119    pub is_star: bool,
120    pub star_table: Option<String>,
121    pub transform_kind: TransformKind,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub transform_function: Option<TransformFunctionFact>,
124    pub cast_type: Option<String>,
125    pub type_hint: Option<String>,
126    pub nullability: ProjectionNullability,
127    pub upstream: Vec<ColumnReferenceFact>,
128}
129
130/// Compact fact about a function-like projection transform.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct TransformFunctionFact {
134    pub name: String,
135    pub literal_args: Vec<String>,
136    pub column_args: Vec<ColumnReferenceFact>,
137}
138
139/// Compact fact about one top-level CTE definition.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142pub struct CteFact {
143    pub name: String,
144    pub columns: Vec<String>,
145    pub body_sql: String,
146    pub output_columns: Vec<String>,
147}
148
149/// Compact fact about one original star projection.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct StarProjectionFact {
153    pub index: usize,
154    pub table: Option<String>,
155    pub expanded_columns: Vec<String>,
156}
157
158/// Compact fact about an upstream column reference.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct ColumnReferenceFact {
162    pub source_name: Option<String>,
163    pub source_alias: Option<String>,
164    pub source_kind: SourceKind,
165    pub table: Option<String>,
166    pub column: String,
167    pub unqualified: bool,
168    pub confidence: ReferenceConfidence,
169}
170
171/// Compact fact about a relation visible in the root scope.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct RelationFact {
175    pub name: String,
176    pub alias: Option<String>,
177    pub kind: SourceKind,
178    pub columns: Vec<String>,
179    pub catalog: Option<String>,
180    pub schema: Option<String>,
181    pub table: Option<String>,
182}
183
184/// Compact fact about a set operation.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct SetOperationFact {
188    pub kind: String,
189    pub all: bool,
190    pub distinct: bool,
191    pub output_columns: Vec<String>,
192    pub branches: Vec<SetOperationBranchFact>,
193}
194
195/// Compact facts for one immediate set-operation branch.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct SetOperationBranchFact {
199    pub index: usize,
200    pub role: SetOperationBranchRole,
201    pub projections: Vec<ProjectionFact>,
202}
203
204/// Semantic contribution of one set-operation branch.
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub enum SetOperationBranchRole {
208    Value,
209    Filter,
210}
211
212/// High-level kind of transformation performed by a projection.
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "snake_case")]
215pub enum TransformKind {
216    Direct,
217    Cast,
218    Aggregation,
219    Constant,
220    Expression,
221    Star,
222}
223
224/// Confidence level for a compact upstream column reference.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub enum ReferenceConfidence {
228    Resolved,
229    Ambiguous,
230    Unknown,
231}
232
233/// Conservative nullability classification for one output projection.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "snake_case")]
236pub enum ProjectionNullability {
237    NonNull,
238    Nullable,
239    Unknown,
240}
241
242/// Analyze a single SELECT or set-operation query.
243pub fn analyze_query(sql: &str, options: AnalyzeQueryOptions) -> Result<QueryAnalysis> {
244    let mut expression = parse_one_with_options(
245        sql,
246        options.dialect,
247        &ParseOptions {
248            complexity_guard: options.complexity_guard,
249        },
250    )?;
251    expression = effective_query(expression);
252    ensure_query(&expression)?;
253    expression = crate::binding::bind_lambdas(expression, options.dialect);
254    let original_expression = expression.clone();
255
256    let mapping_schema = options
257        .schema
258        .as_ref()
259        .map(|schema| analysis_mapping_schema(schema, options.dialect));
260    let schema_info = options.schema.as_ref().map(AnalysisSchemaInfo::from_schema);
261    let cte_facts = top_level_cte_facts(
262        &original_expression,
263        options.dialect,
264        options.complexity_guard,
265    )?;
266    let star_projections = star_projection_facts(
267        &original_expression,
268        mapping_schema.as_ref(),
269        options.dialect,
270    );
271
272    if let Some(schema) = mapping_schema.as_ref() {
273        use crate::optimizer::qualify_columns::QualifyColumnsError;
274        expression = match qualify_schema_aware_expression(
275            expression.clone(),
276            schema,
277            Some(options.dialect),
278        ) {
279            Ok(qualified) => qualified,
280            // Analysis is not validation. Incomplete schemas and unresolved
281            // lexical references must still yield conservative usage facts.
282            Err(
283                QualifyColumnsError::UnknownTable(_)
284                | QualifyColumnsError::UnknownColumn(_)
285                | QualifyColumnsError::AmbiguousColumn(_)
286                | QualifyColumnsError::ColumnNotResolved { .. },
287            ) => expression,
288            Err(error) => {
289                return Err(Error::internal(format!(
290                    "query analysis qualification failed: {error}"
291                )))
292            }
293        };
294    }
295
296    crate::lineage::expand_cte_stars(
297        &mut expression,
298        mapping_schema.as_ref().map(|schema| schema as &dyn Schema),
299    );
300    annotate_types(
301        &mut expression,
302        mapping_schema.as_ref().map(|schema| schema as &dyn Schema),
303        Some(options.dialect),
304    );
305
306    let scope = build_scope(&expression);
307    let original_scope = build_scope(&original_expression);
308    let empty_schema = MappingSchema::with_dialect(options.dialect);
309    let mut uncertain_columns = HashMap::new();
310    column_uses::collect_uncertain_occurrences(
311        &original_scope,
312        mapping_schema.as_ref().unwrap_or(&empty_schema),
313        &mut uncertain_columns,
314    );
315    let nullability_context = NullabilityContext::new(
316        &scope,
317        schema_info.as_ref(),
318        mapping_schema.as_ref(),
319        options.dialect,
320        &uncertain_columns,
321    );
322    let shape = if is_set_operation(&expression) {
323        QueryShape::SetOperation
324    } else {
325        QueryShape::Select
326    };
327
328    Ok(QueryAnalysis {
329        shape,
330        ctes: collect_cte_names(&expression),
331        cte_facts,
332        projections: projection_facts_for_query(
333            &expression,
334            &scope,
335            options.dialect,
336            &nullability_context,
337        ),
338        relations: relation_facts(&scope, mapping_schema.as_ref(), options.dialect),
339        base_tables: base_table_facts(&scope, mapping_schema.as_ref(), options.dialect),
340        star_projections,
341        set_operations: set_operation_facts(
342            &expression,
343            &scope,
344            options.dialect,
345            &nullability_context,
346        ),
347        column_uses: column_uses::collect(
348            &original_scope,
349            &scope,
350            mapping_schema.as_ref(),
351            options.dialect,
352            &uncertain_columns,
353            options.complexity_guard,
354        ),
355    })
356}
357
358fn analysis_mapping_schema(schema: &ValidationSchema, dialect: DialectType) -> MappingSchema {
359    mapping_schema_from_validation_schema_with_dialect(schema, dialect)
360}
361
362fn validation_table_names(table: &crate::validation::SchemaTable) -> Vec<String> {
363    let mut names = Vec::new();
364
365    names.push(table.name.to_ascii_lowercase());
366    if let Some(schema_name) = &table.schema {
367        names.push(format!(
368            "{}.{}",
369            schema_name.to_ascii_lowercase(),
370            table.name.to_ascii_lowercase()
371        ));
372    }
373    for alias in &table.aliases {
374        names.push(alias.to_ascii_lowercase());
375    }
376
377    names.sort();
378    names.dedup();
379    names
380}
381
382#[derive(Debug, Clone)]
383struct AnalysisColumnInfo {
384    nullable: Option<bool>,
385    primary_key: bool,
386}
387
388#[derive(Debug, Clone)]
389struct AnalysisSchemaInfo {
390    columns: HashMap<(String, String), AnalysisColumnInfo>,
391}
392
393impl AnalysisSchemaInfo {
394    fn from_schema(schema: &ValidationSchema) -> Self {
395        let mut columns = HashMap::new();
396
397        for table in &schema.tables {
398            let table_names = validation_table_names(table);
399            let primary_keys: HashSet<String> = table
400                .primary_key
401                .iter()
402                .map(|column| column.to_ascii_lowercase())
403                .collect();
404
405            for column in &table.columns {
406                let info = AnalysisColumnInfo {
407                    nullable: column.nullable,
408                    primary_key: column.primary_key
409                        || primary_keys.contains(&column.name.to_ascii_lowercase()),
410                };
411
412                for table_name in &table_names {
413                    columns.insert(
414                        (
415                            normalize_lookup_name(table_name),
416                            normalize_lookup_name(&column.name),
417                        ),
418                        info.clone(),
419                    );
420                }
421            }
422        }
423
424        Self { columns }
425    }
426
427    fn column(&self, table: &str, column: &str) -> Option<&AnalysisColumnInfo> {
428        self.columns
429            .get(&(normalize_lookup_name(table), normalize_lookup_name(column)))
430    }
431}
432
433struct NullabilityContext<'a> {
434    lineage: RefCell<HashMap<*const Scope, ScopedLineage>>,
435    schema: Option<&'a AnalysisSchemaInfo>,
436    mapping_schema: Option<&'a MappingSchema>,
437    empty_schema: MappingSchema,
438    dialect: DialectType,
439    scopes: Vec<NullabilityScope<'a>>,
440    scope_ids: HashMap<*const Scope, usize>,
441    outputs: RefCell<HashMap<(usize, usize), ProjectionNullability>>,
442    resolving: RefCell<HashSet<(usize, usize)>>,
443    uncertain_columns: &'a HashMap<(usize, usize), ReferenceConfidence>,
444}
445
446struct NullabilityScope<'a> {
447    scope: &'a Scope,
448    selected: Scope,
449    bindings: HashMap<String, &'a Expression>,
450    ctes: HashMap<String, usize>,
451    derived: Vec<usize>,
452    branches: Vec<usize>,
453    nullable_sources: HashSet<String>,
454}
455
456impl<'a> NullabilityContext<'a> {
457    fn new(
458        scope: &'a Scope,
459        schema: Option<&'a AnalysisSchemaInfo>,
460        mapping_schema: Option<&'a MappingSchema>,
461        dialect: DialectType,
462        uncertain_columns: &'a HashMap<(usize, usize), ReferenceConfidence>,
463    ) -> Self {
464        let mut context = Self {
465            lineage: RefCell::new(HashMap::new()),
466            schema,
467            mapping_schema,
468            empty_schema: MappingSchema::with_dialect(dialect),
469            dialect,
470            scopes: Vec::new(),
471            scope_ids: HashMap::new(),
472            outputs: RefCell::new(HashMap::new()),
473            resolving: RefCell::new(HashSet::new()),
474            uncertain_columns,
475        };
476        context.index_scope(scope, HashMap::new(), None);
477        context
478    }
479
480    // Keep each CTE's definition environment, rather than resolving it in the
481    // environment of a later consumer that may shadow the same names.
482    fn index_scope(
483        &mut self,
484        scope: &'a Scope,
485        mut ctes: HashMap<String, usize>,
486        recursive_name: Option<&Identifier>,
487    ) -> usize {
488        let id = self.scopes.len();
489        self.scope_ids.insert(scope, id);
490        if let Some(name) = recursive_name {
491            ctes.insert(
492                crate::set_operation::identifier_key(name, Some(self.dialect)),
493                id,
494            );
495        }
496        let query = crate::scope::scope_query(&scope.expression);
497        let bindings: HashMap<_, _> = if let Expression::Select(select) = query {
498            select
499                .from
500                .iter()
501                .flat_map(|from| &from.expressions)
502                .chain(select.joins.iter().map(|join| &join.this))
503                .filter_map(|expression| {
504                    expression_source_name(expression).map(|name| (name, expression))
505                })
506                .collect()
507        } else {
508            HashMap::new()
509        };
510        self.scopes.push(NullabilityScope {
511            scope,
512            selected: crate::scope::selected_reference_scope(scope),
513            bindings,
514            ctes: HashMap::new(),
515            derived: Vec::new(),
516            branches: Vec::new(),
517            nullable_sources: nullable_source_names(query, self.dialect),
518        });
519        let recursive = with_clause(crate::scope::scope_query(&scope.expression))
520            .is_some_and(|with| with.recursive);
521        for child in &scope.cte_scopes {
522            if let Expression::Cte(cte) = &child.expression {
523                let name = &cte.alias;
524                let child_id = self.index_scope(child, ctes.clone(), recursive.then_some(name));
525                ctes.insert(
526                    crate::set_operation::identifier_key(name, Some(self.dialect)),
527                    child_id,
528                );
529            }
530        }
531        self.scopes[id].ctes = ctes.clone();
532        for child in &scope.derived_table_scopes {
533            let child_id = self.index_scope(child, ctes.clone(), None);
534            self.scopes[id].derived.push(child_id);
535        }
536        for child in &scope.union_scopes {
537            let child_id = self.index_scope(child, ctes.clone(), None);
538            self.scopes[id].branches.push(child_id);
539        }
540        id
541    }
542}
543
544fn top_level_cte_facts(
545    expression: &Expression,
546    dialect: DialectType,
547    guard: Option<ComplexityGuardOptions>,
548) -> Result<Vec<CteFact>> {
549    let Some(with_clause) = with_clause(expression) else {
550        return Ok(Vec::new());
551    };
552
553    with_clause
554        .ctes
555        .iter()
556        .map(|cte| {
557            Ok(CteFact {
558                name: cte.alias.name.clone(),
559                columns: cte
560                    .columns
561                    .iter()
562                    .map(|column| column.name.clone())
563                    .collect(),
564                body_sql: Dialect::get(dialect).generate_with_guard(&cte.this, guard)?,
565                output_columns: get_output_column_names_for_dialect(&cte.this, Some(dialect)),
566            })
567        })
568        .collect()
569}
570
571fn star_projection_facts(
572    expression: &Expression,
573    mapping_schema: Option<&MappingSchema>,
574    dialect: DialectType,
575) -> Vec<StarProjectionFact> {
576    let scope = build_scope(expression);
577    let ordered_sources = ordered_source_names_for_query(expression);
578
579    select_expressions_for_query(expression)
580        .iter()
581        .enumerate()
582        .filter_map(|(index, projection)| {
583            let inner = unwrap_projection_alias(projection);
584            if !projection_is_star(inner) {
585                return None;
586            }
587
588            let table = projection_star_table(inner);
589            let expanded_columns = expanded_star_columns(
590                table.as_deref(),
591                &scope,
592                &ordered_sources,
593                mapping_schema,
594                dialect,
595            );
596
597            Some(StarProjectionFact {
598                index,
599                table,
600                expanded_columns,
601            })
602        })
603        .collect()
604}
605
606fn expanded_star_columns(
607    star_table: Option<&str>,
608    scope: &Scope,
609    ordered_sources: &[String],
610    mapping_schema: Option<&MappingSchema>,
611    dialect: DialectType,
612) -> Vec<String> {
613    let mut columns = Vec::new();
614    let mut source_names: Vec<String> = if ordered_sources.is_empty() {
615        let mut names: Vec<_> = scope.sources.keys().cloned().collect();
616        names.sort();
617        names
618    } else {
619        ordered_sources.to_vec()
620    };
621
622    source_names.dedup();
623
624    for source_name in source_names {
625        let Some(source) = scope.sources.get(&source_name) else {
626            continue;
627        };
628
629        if let Some(star_table) = star_table {
630            let matches = source_name.eq_ignore_ascii_case(star_table)
631                || source
632                    .alias
633                    .as_deref()
634                    .is_some_and(|alias| alias.eq_ignore_ascii_case(star_table))
635                || source_table_name(source)
636                    .is_some_and(|table| table.eq_ignore_ascii_case(star_table));
637
638            if !matches {
639                continue;
640            }
641        }
642
643        columns.extend(source_columns(source, mapping_schema, dialect));
644    }
645
646    columns
647}
648
649fn ordered_source_names_for_query(expression: &Expression) -> Vec<String> {
650    match expression {
651        Expression::Select(select) => ordered_source_names_for_select(select),
652        Expression::Union(union) => ordered_source_names_for_query(&union.left),
653        Expression::Intersect(intersect) => ordered_source_names_for_query(&intersect.left),
654        Expression::Except(except) => ordered_source_names_for_query(&except.left),
655        Expression::Subquery(subquery) => ordered_source_names_for_query(&subquery.this),
656        _ => Vec::new(),
657    }
658}
659
660fn ordered_source_names_for_select(select: &crate::expressions::Select) -> Vec<String> {
661    let mut sources = Vec::new();
662
663    if let Some(from) = &select.from {
664        for expression in &from.expressions {
665            if let Some(source_name) = expression_source_name(expression) {
666                sources.push(source_name);
667            }
668        }
669    }
670
671    for join in &select.joins {
672        if let Some(source_name) = expression_source_name(&join.this) {
673            sources.push(source_name);
674        }
675    }
676
677    sources
678}
679
680fn nullable_source_names(expression: &Expression, dialect: DialectType) -> HashSet<String> {
681    match expression {
682        Expression::Select(select) => nullable_source_names_for_select(select, dialect),
683        Expression::Union(union) => nullable_source_names(&union.left, dialect),
684        Expression::Intersect(intersect) => nullable_source_names(&intersect.left, dialect),
685        Expression::Except(except) => nullable_source_names(&except.left, dialect),
686        Expression::Subquery(subquery) => nullable_source_names(&subquery.this, dialect),
687        _ => HashSet::new(),
688    }
689}
690
691fn nullable_source_names_for_select(
692    select: &crate::expressions::Select,
693    dialect: DialectType,
694) -> HashSet<String> {
695    let mut nullable = HashSet::new();
696    let mut left_sources = Vec::new();
697
698    if let Some(from) = &select.from {
699        for expression in &from.expressions {
700            if let Some(identifier) = expression_source_identifier(expression) {
701                left_sources.push(crate::set_operation::identifier_key(
702                    identifier,
703                    Some(dialect),
704                ));
705            }
706        }
707    }
708
709    for join in &select.joins {
710        let right_source = expression_source_identifier(&join.this)
711            .map(|identifier| crate::set_operation::identifier_key(identifier, Some(dialect)));
712
713        if join_nullable_left(join.kind) {
714            for source_name in &left_sources {
715                nullable.insert(source_name.clone());
716            }
717        }
718
719        if join_nullable_right(join.kind) {
720            if let Some(source_name) = &right_source {
721                nullable.insert(source_name.clone());
722            }
723        }
724
725        if let Some(source_name) = right_source {
726            left_sources.push(source_name);
727        }
728    }
729
730    nullable
731}
732
733fn join_nullable_left(kind: JoinKind) -> bool {
734    matches!(
735        kind,
736        JoinKind::Right
737            | JoinKind::NaturalRight
738            | JoinKind::AsOfRight
739            | JoinKind::Full
740            | JoinKind::NaturalFull
741            | JoinKind::Outer
742    )
743}
744
745fn join_nullable_right(kind: JoinKind) -> bool {
746    matches!(
747        kind,
748        JoinKind::Left
749            | JoinKind::NaturalLeft
750            | JoinKind::AsOfLeft
751            | JoinKind::LeftLateral
752            | JoinKind::OuterApply
753            | JoinKind::LeftArray
754            | JoinKind::Full
755            | JoinKind::NaturalFull
756            | JoinKind::Outer
757    )
758}
759
760fn expression_source_name(expression: &Expression) -> Option<String> {
761    expression_source_identifier(expression).map(|identifier| identifier.name.clone())
762}
763
764fn expression_source_identifier(expression: &Expression) -> Option<&Identifier> {
765    match expression {
766        Expression::Table(table) => Some(table.alias.as_ref().unwrap_or(&table.name)),
767        Expression::Subquery(subquery) => subquery.alias.as_ref(),
768        Expression::Alias(alias) => Some(&alias.alias),
769        Expression::Cte(cte) => Some(&cte.alias),
770        _ => None,
771    }
772}
773
774fn normalize_lookup_name(name: &str) -> String {
775    name.to_ascii_lowercase()
776}
777
778fn effective_query(expression: Expression) -> Expression {
779    match expression {
780        Expression::Prepare(prepare) => prepare.statement,
781        Expression::Subquery(subquery) if subquery.alias.is_none() => subquery.this,
782        other => other,
783    }
784}
785
786fn ensure_query(expression: &Expression) -> Result<()> {
787    if matches!(
788        expression,
789        Expression::Select(_)
790            | Expression::Union(_)
791            | Expression::Intersect(_)
792            | Expression::Except(_)
793    ) {
794        Ok(())
795    } else {
796        Err(Error::internal(
797            "analyze_query requires a SELECT or set operation query",
798        ))
799    }
800}
801
802fn is_set_operation(expression: &Expression) -> bool {
803    matches!(
804        expression,
805        Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_)
806    )
807}
808
809fn collect_cte_names(expression: &Expression) -> Vec<String> {
810    let mut names = Vec::new();
811    let mut seen = HashSet::new();
812    collect_cte_names_inner(expression, &mut names, &mut seen);
813    names
814}
815
816fn collect_cte_names_inner(
817    expression: &Expression,
818    names: &mut Vec<String>,
819    seen: &mut HashSet<String>,
820) {
821    if let Some(with_clause) = with_clause(expression) {
822        collect_with_names(with_clause, names, seen);
823    }
824
825    match expression {
826        Expression::Union(union) => {
827            collect_cte_names_inner(&union.left, names, seen);
828            collect_cte_names_inner(&union.right, names, seen);
829        }
830        Expression::Intersect(intersect) => {
831            collect_cte_names_inner(&intersect.left, names, seen);
832            collect_cte_names_inner(&intersect.right, names, seen);
833        }
834        Expression::Except(except) => {
835            collect_cte_names_inner(&except.left, names, seen);
836            collect_cte_names_inner(&except.right, names, seen);
837        }
838        Expression::Subquery(subquery) => collect_cte_names_inner(&subquery.this, names, seen),
839        _ => {}
840    }
841}
842
843fn collect_with_names(with_clause: &With, names: &mut Vec<String>, seen: &mut HashSet<String>) {
844    for cte in &with_clause.ctes {
845        if seen.insert(cte.alias.name.clone()) {
846            names.push(cte.alias.name.clone());
847        }
848        collect_cte_names_inner(&cte.this, names, seen);
849    }
850}
851
852fn with_clause(expression: &Expression) -> Option<&With> {
853    match expression {
854        Expression::Select(select) => select.with.as_ref(),
855        Expression::Union(union) => union.with.as_ref(),
856        Expression::Intersect(intersect) => intersect.with.as_ref(),
857        Expression::Except(except) => except.with.as_ref(),
858        _ => None,
859    }
860}
861
862fn projection_facts_for_query(
863    expression: &Expression,
864    scope: &Scope,
865    dialect: DialectType,
866    nullability_context: &NullabilityContext<'_>,
867) -> Vec<ProjectionFact> {
868    let expressions = projection_sources_for_query(expression, dialect);
869    let names = get_output_column_names_for_dialect(expression, Some(dialect));
870
871    expressions
872        .iter()
873        .enumerate()
874        .map(|(index, (projection, null_padded))| {
875            let mut fact = projection_fact(
876                index,
877                names
878                    .get(index)
879                    .cloned()
880                    .or_else(|| projection_name(projection)),
881                projection,
882                expression,
883                scope,
884                dialect,
885                nullability_context,
886            );
887            if *null_padded {
888                fact.nullability = ProjectionNullability::Nullable;
889            }
890            fact
891        })
892        .collect()
893}
894
895/// Return one representative projection for each result ordinal together with
896/// whether any immediate name-aligned branch contributes a synthetic NULL.
897fn projection_sources_for_query(
898    expression: &Expression,
899    dialect: DialectType,
900) -> Vec<(&Expression, bool)> {
901    match crate::set_operation::set_operation_layout(expression, Some(dialect)) {
902        Ok(Some(layout)) => layout
903            .outputs
904            .iter()
905            .filter_map(|output| {
906                let null_padded = output.left_ordinal.is_none() || output.right_ordinal.is_none();
907                output
908                    .left_ordinal
909                    .and_then(|ordinal| {
910                        projection_source_for_ordinal(
911                            set_operation_left(expression)?,
912                            ordinal,
913                            dialect,
914                        )
915                    })
916                    .or_else(|| {
917                        output.right_ordinal.and_then(|ordinal| {
918                            projection_source_for_ordinal(
919                                set_operation_right(expression)?,
920                                ordinal,
921                                dialect,
922                            )
923                        })
924                    })
925                    .map(|(projection, nested_null_padded)| {
926                        (projection, null_padded || nested_null_padded)
927                    })
928            })
929            .collect(),
930        _ => select_expressions_for_query(expression)
931            .into_iter()
932            .map(|projection| (projection, false))
933            .collect(),
934    }
935}
936
937fn projection_source_for_ordinal(
938    expression: &Expression,
939    ordinal: usize,
940    dialect: DialectType,
941) -> Option<(&Expression, bool)> {
942    match crate::set_operation::set_operation_layout(expression, Some(dialect)) {
943        Ok(Some(layout)) => {
944            let output = layout.outputs.get(ordinal)?;
945            let null_padded = output.left_ordinal.is_none() || output.right_ordinal.is_none();
946            output
947                .left_ordinal
948                .and_then(|child_ordinal| {
949                    projection_source_for_ordinal(
950                        set_operation_left(expression)?,
951                        child_ordinal,
952                        dialect,
953                    )
954                })
955                .or_else(|| {
956                    output.right_ordinal.and_then(|child_ordinal| {
957                        projection_source_for_ordinal(
958                            set_operation_right(expression)?,
959                            child_ordinal,
960                            dialect,
961                        )
962                    })
963                })
964                .map(|(projection, nested_null_padded)| {
965                    (projection, null_padded || nested_null_padded)
966                })
967        }
968        _ => match expression {
969            Expression::Select(select) => select
970                .expressions
971                .get(ordinal)
972                .map(|projection| (projection, false)),
973            Expression::Union(union) => {
974                projection_source_for_ordinal(&union.left, ordinal, dialect)
975            }
976            Expression::Intersect(intersect) => {
977                projection_source_for_ordinal(&intersect.left, ordinal, dialect)
978            }
979            Expression::Except(except) => {
980                projection_source_for_ordinal(&except.left, ordinal, dialect)
981            }
982            Expression::Subquery(subquery) => {
983                projection_source_for_ordinal(&subquery.this, ordinal, dialect)
984            }
985            Expression::Paren(paren) => {
986                projection_source_for_ordinal(&paren.this, ordinal, dialect)
987            }
988            _ => None,
989        },
990    }
991}
992
993fn set_operation_left(expression: &Expression) -> Option<&Expression> {
994    match expression {
995        Expression::Union(set_op) => Some(&set_op.left),
996        Expression::Intersect(set_op) => Some(&set_op.left),
997        Expression::Except(set_op) => Some(&set_op.left),
998        _ => None,
999    }
1000}
1001
1002fn set_operation_right(expression: &Expression) -> Option<&Expression> {
1003    match expression {
1004        Expression::Union(set_op) => Some(&set_op.right),
1005        Expression::Intersect(set_op) => Some(&set_op.right),
1006        Expression::Except(set_op) => Some(&set_op.right),
1007        _ => None,
1008    }
1009}
1010
1011fn select_expressions_for_query(expression: &Expression) -> Vec<&Expression> {
1012    match expression {
1013        Expression::Select(select) => select.expressions.iter().collect(),
1014        Expression::Union(union) => select_expressions_for_query(&union.left),
1015        Expression::Intersect(intersect) => select_expressions_for_query(&intersect.left),
1016        Expression::Except(except) => select_expressions_for_query(&except.left),
1017        Expression::Subquery(subquery) => select_expressions_for_query(&subquery.this),
1018        _ => Vec::new(),
1019    }
1020}
1021
1022fn projection_fact(
1023    index: usize,
1024    name: Option<String>,
1025    projection: &Expression,
1026    _query: &Expression,
1027    scope: &Scope,
1028    dialect: DialectType,
1029    nullability_context: &NullabilityContext<'_>,
1030) -> ProjectionFact {
1031    let inner = unwrap_projection_alias(projection);
1032    let is_star = projection_is_star(inner);
1033    let mut lineage = nullability_context.lineage.borrow_mut();
1034    let prepared = lineage
1035        .entry(scope as *const Scope)
1036        .or_insert_with(|| ScopedLineage::new(scope.clone(), &[], dialect));
1037    let mut upstream = prepared
1038        .output(index)
1039        .map(|node| {
1040            let uncertain = lineage_uncertainty(&node, nullability_context.uncertain_columns);
1041            let mut references = terminal_references_from_lineage(&node);
1042            if let Some(confidence) = uncertain {
1043                for reference in &mut references {
1044                    reference.confidence = confidence;
1045                }
1046            }
1047            references
1048        })
1049        .ok()
1050        .filter(|refs| !refs.is_empty())
1051        .unwrap_or_else(|| fallback_column_references(inner, scope));
1052    if let Some(confidence) = expression_uncertainty(inner, nullability_context.uncertain_columns) {
1053        for reference in &mut upstream {
1054            reference.confidence = confidence;
1055        }
1056    }
1057    if let Some(schema) = nullability_context.mapping_schema {
1058        for reference in &mut upstream {
1059            if let Some(table) = &reference.table {
1060                if let Ok(columns) = schema.column_names(table) {
1061                    if !columns.is_empty()
1062                        && !columns.iter().any(|column| {
1063                            column == "*"
1064                                || crate::schema::normalize_name(column, Some(dialect), false, true)
1065                                    == crate::schema::normalize_name(
1066                                        &reference.column,
1067                                        Some(dialect),
1068                                        false,
1069                                        true,
1070                                    )
1071                        })
1072                    {
1073                        reference.confidence = ReferenceConfidence::Unknown;
1074                    }
1075                }
1076            }
1077        }
1078    }
1079
1080    let mut transform_function = transform_function_fact(inner, scope, dialect);
1081    if let Some(function) = &mut transform_function {
1082        for reference in &mut function.column_args {
1083            if let Some(upstream) = upstream.iter().find(|upstream| {
1084                upstream.column == reference.column && upstream.table == reference.table
1085            }) {
1086                reference.confidence = upstream.confidence;
1087            } else if upstream
1088                .iter()
1089                .any(|upstream| upstream.confidence != ReferenceConfidence::Resolved)
1090            {
1091                reference.confidence = ReferenceConfidence::Unknown;
1092            }
1093        }
1094    }
1095    ProjectionFact {
1096        index,
1097        name,
1098        is_star,
1099        star_table: projection_star_table(inner),
1100        transform_kind: transform_kind(inner),
1101        transform_function,
1102        cast_type: cast_type(inner, dialect),
1103        type_hint: projection
1104            .inferred_type()
1105            .or_else(|| inner.inferred_type())
1106            .and_then(|data_type| render_data_type(data_type, dialect)),
1107        nullability: nullability_context
1108            .scope_ids
1109            .get(&(scope as *const Scope))
1110            .map(|id| nullability_context.output(*id, index, 0))
1111            .unwrap_or(ProjectionNullability::Unknown),
1112        upstream,
1113    }
1114}
1115
1116fn expression_uncertainty(
1117    expression: &Expression,
1118    uncertain: &HashMap<(usize, usize), ReferenceConfidence>,
1119) -> Option<ReferenceConfidence> {
1120    combine_uncertainty(
1121        crate::scope::walk_in_scope(expression, false).filter_map(|node| match node {
1122            Expression::Column(column) => column
1123                .span
1124                .or(column.name.span)
1125                .and_then(|span| uncertain.get(&(span.start, span.end)))
1126                .copied(),
1127            _ => None,
1128        }),
1129    )
1130}
1131
1132fn combine_uncertainty(
1133    confidences: impl Iterator<Item = ReferenceConfidence>,
1134) -> Option<ReferenceConfidence> {
1135    confidences.fold(None, |current, confidence| {
1136        Some(
1137            if current == Some(ReferenceConfidence::Ambiguous)
1138                || confidence == ReferenceConfidence::Ambiguous
1139            {
1140                ReferenceConfidence::Ambiguous
1141            } else {
1142                confidence
1143            },
1144        )
1145    })
1146}
1147
1148fn lineage_uncertainty(
1149    node: &LineageNode,
1150    uncertain: &HashMap<(usize, usize), ReferenceConfidence>,
1151) -> Option<ReferenceConfidence> {
1152    combine_uncertainty(
1153        node.walk()
1154            .filter_map(|node| expression_uncertainty(&node.expression, uncertain)),
1155    )
1156}
1157
1158fn transform_function_fact(
1159    expression: &Expression,
1160    scope: &Scope,
1161    dialect: DialectType,
1162) -> Option<TransformFunctionFact> {
1163    let mut matches = expression
1164        .find_all(|candidate| transform_function_fact_for_node(candidate, scope, dialect).is_some())
1165        .into_iter();
1166
1167    let first = matches.next()?;
1168    if matches.next().is_some() {
1169        return None;
1170    }
1171
1172    transform_function_fact_for_node(first, scope, dialect)
1173}
1174
1175fn transform_function_fact_for_node(
1176    expression: &Expression,
1177    scope: &Scope,
1178    dialect: DialectType,
1179) -> Option<TransformFunctionFact> {
1180    match expression {
1181        Expression::Function(function) => Some(transform_function_from_args(
1182            &function.name,
1183            &function.args,
1184            scope,
1185            dialect,
1186        )),
1187        Expression::AggregateFunction(function) => Some(transform_function_from_args(
1188            &function.name,
1189            &function.args,
1190            scope,
1191            dialect,
1192        )),
1193        Expression::DateTrunc(function) => Some(transform_function_from_parts(
1194            "DATE_TRUNC",
1195            vec![datetime_field_name(&function.unit)],
1196            vec![&function.this],
1197            scope,
1198            dialect,
1199        )),
1200        Expression::TimestampTrunc(function) => Some(transform_function_from_parts(
1201            "TIMESTAMP_TRUNC",
1202            vec![datetime_field_name(&function.unit)],
1203            vec![&function.this],
1204            scope,
1205            dialect,
1206        )),
1207        Expression::TimeTrunc(function) => {
1208            let mut args = vec![function.this.as_ref()];
1209            if let Some(zone) = function.zone.as_deref() {
1210                args.push(zone);
1211            }
1212            Some(transform_function_from_parts(
1213                "TIME_TRUNC",
1214                vec![function.unit.clone()],
1215                args,
1216                scope,
1217                dialect,
1218            ))
1219        }
1220        Expression::Extract(function) => Some(transform_function_from_parts(
1221            "EXTRACT",
1222            vec![datetime_field_name(&function.field)],
1223            vec![&function.this],
1224            scope,
1225            dialect,
1226        )),
1227        Expression::DateAdd(function) => Some(transform_function_from_parts(
1228            "DATE_ADD",
1229            Vec::new(),
1230            vec![&function.this, &function.interval],
1231            scope,
1232            dialect,
1233        )),
1234        Expression::DateSub(function) => Some(transform_function_from_parts(
1235            "DATE_SUB",
1236            Vec::new(),
1237            vec![&function.this, &function.interval],
1238            scope,
1239            dialect,
1240        )),
1241        Expression::DateDiff(function) => Some(transform_function_from_parts(
1242            "DATE_DIFF",
1243            Vec::new(),
1244            vec![&function.this, &function.expression],
1245            scope,
1246            dialect,
1247        )),
1248        _ => None,
1249    }
1250}
1251
1252fn transform_function_from_args(
1253    name: &str,
1254    args: &[Expression],
1255    scope: &Scope,
1256    dialect: DialectType,
1257) -> TransformFunctionFact {
1258    let literal_args = args
1259        .iter()
1260        .filter_map(|arg| literal_argument(arg, dialect))
1261        .collect();
1262    transform_function_from_parts(name, literal_args, args.iter().collect(), scope, dialect)
1263}
1264
1265fn transform_function_from_parts(
1266    name: &str,
1267    literal_args: Vec<String>,
1268    args: Vec<&Expression>,
1269    scope: &Scope,
1270    _dialect: DialectType,
1271) -> TransformFunctionFact {
1272    let column_args = dedupe_column_refs(
1273        args.into_iter()
1274            .flat_map(|arg| fallback_column_references(arg, scope))
1275            .collect(),
1276    );
1277
1278    TransformFunctionFact {
1279        name: name.to_string(),
1280        literal_args,
1281        column_args,
1282    }
1283}
1284
1285fn literal_argument(expression: &Expression, dialect: DialectType) -> Option<String> {
1286    match expression {
1287        Expression::Literal(literal) => Some(literal.value_str().to_string()),
1288        Expression::Boolean(boolean) => Some(boolean.value.to_string()),
1289        Expression::Null(_) => Some("NULL".to_string()),
1290        Expression::Identifier(identifier) => Some(identifier.name.clone()),
1291        Expression::Var(var) => Some(var.this.clone()),
1292        Expression::DataType(data_type) => render_data_type(data_type, dialect),
1293        _ => None,
1294    }
1295}
1296
1297fn datetime_field_name(field: &crate::expressions::DateTimeField) -> String {
1298    match field {
1299        crate::expressions::DateTimeField::Year => "year".to_string(),
1300        crate::expressions::DateTimeField::Month => "month".to_string(),
1301        crate::expressions::DateTimeField::Day => "day".to_string(),
1302        crate::expressions::DateTimeField::Hour => "hour".to_string(),
1303        crate::expressions::DateTimeField::Minute => "minute".to_string(),
1304        crate::expressions::DateTimeField::Second => "second".to_string(),
1305        crate::expressions::DateTimeField::Millisecond => "millisecond".to_string(),
1306        crate::expressions::DateTimeField::Microsecond => "microsecond".to_string(),
1307        crate::expressions::DateTimeField::DayOfWeek => "day_of_week".to_string(),
1308        crate::expressions::DateTimeField::DayOfYear => "day_of_year".to_string(),
1309        crate::expressions::DateTimeField::Week => "week".to_string(),
1310        crate::expressions::DateTimeField::WeekWithModifier(modifier) => {
1311            format!("week({modifier})")
1312        }
1313        crate::expressions::DateTimeField::Quarter => "quarter".to_string(),
1314        crate::expressions::DateTimeField::Epoch => "epoch".to_string(),
1315        crate::expressions::DateTimeField::Timezone => "timezone".to_string(),
1316        crate::expressions::DateTimeField::TimezoneHour => "timezone_hour".to_string(),
1317        crate::expressions::DateTimeField::TimezoneMinute => "timezone_minute".to_string(),
1318        crate::expressions::DateTimeField::Date => "date".to_string(),
1319        crate::expressions::DateTimeField::Time => "time".to_string(),
1320        crate::expressions::DateTimeField::Custom(name) => name.clone(),
1321    }
1322}
1323
1324fn unwrap_projection_alias(expression: &Expression) -> &Expression {
1325    match expression {
1326        Expression::Alias(alias) => unwrap_projection_alias(&alias.this),
1327        Expression::Annotated(annotated) => unwrap_projection_alias(&annotated.this),
1328        Expression::Paren(paren) => unwrap_projection_alias(&paren.this),
1329        _ => expression,
1330    }
1331}
1332
1333fn projection_name(expression: &Expression) -> Option<String> {
1334    match expression {
1335        Expression::Alias(alias) => Some(alias.alias.name.clone()),
1336        Expression::Column(column) => Some(column.name.name.clone()),
1337        Expression::Identifier(identifier) => Some(identifier.name.clone()),
1338        Expression::Star(_) => Some("*".to_string()),
1339        Expression::Annotated(annotated) => projection_name(&annotated.this),
1340        _ => None,
1341    }
1342}
1343
1344fn projection_is_star(expression: &Expression) -> bool {
1345    matches!(expression, Expression::Star(_))
1346        || matches!(expression, Expression::Column(column) if column.name.name == "*")
1347}
1348
1349fn projection_star_table(expression: &Expression) -> Option<String> {
1350    match expression {
1351        Expression::Star(star) => star
1352            .table
1353            .as_ref()
1354            .map(|identifier| identifier.name.clone()),
1355        Expression::Column(column) if column.name.name == "*" => column
1356            .table
1357            .as_ref()
1358            .map(|identifier| identifier.name.clone()),
1359        _ => None,
1360    }
1361}
1362
1363fn transform_kind(expression: &Expression) -> TransformKind {
1364    if projection_is_star(expression) {
1365        TransformKind::Star
1366    } else if is_cast_expression(expression) {
1367        TransformKind::Cast
1368    } else if contains_aggregate(expression) {
1369        TransformKind::Aggregation
1370    } else if matches!(
1371        expression,
1372        Expression::Column(_) | Expression::Identifier(_)
1373    ) {
1374        TransformKind::Direct
1375    } else if is_simple_constant(expression) {
1376        TransformKind::Constant
1377    } else {
1378        TransformKind::Expression
1379    }
1380}
1381
1382fn is_cast_expression(expression: &Expression) -> bool {
1383    matches!(
1384        expression,
1385        Expression::Cast(_) | Expression::TryCast(_) | Expression::SafeCast(_)
1386    )
1387}
1388
1389fn cast_type(expression: &Expression, dialect: DialectType) -> Option<String> {
1390    match expression {
1391        Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
1392            render_data_type(&cast.to, dialect)
1393        }
1394        _ => None,
1395    }
1396}
1397
1398fn render_data_type(data_type: &DataType, dialect: DialectType) -> Option<String> {
1399    Dialect::get(dialect)
1400        .generate(&Expression::DataType(data_type.clone()))
1401        .ok()
1402}
1403
1404fn is_simple_constant(expression: &Expression) -> bool {
1405    match expression {
1406        Expression::Literal(_) | Expression::Boolean(_) | Expression::Null(_) => true,
1407        Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
1408            is_simple_constant(&cast.this)
1409        }
1410        Expression::Neg(unary) | Expression::BitwiseNot(unary) => is_simple_constant(&unary.this),
1411        _ => false,
1412    }
1413}
1414
1415impl NullabilityContext<'_> {
1416    // Inference is optional metadata: cycles and excessive dependency depth
1417    // must yield Unknown, not recurse indefinitely or claim non-nullability.
1418    const MAX_DEPTH: usize = 128;
1419
1420    fn output(&self, scope_id: usize, ordinal: usize, depth: usize) -> ProjectionNullability {
1421        let key = (scope_id, ordinal);
1422        if let Some(value) = self.outputs.borrow().get(&key) {
1423            return *value;
1424        }
1425        if depth >= Self::MAX_DEPTH || !self.resolving.borrow_mut().insert(key) {
1426            return ProjectionNullability::Unknown;
1427        }
1428        let value = self.output_inner(scope_id, ordinal, depth + 1);
1429        self.resolving.borrow_mut().remove(&key);
1430        self.outputs.borrow_mut().insert(key, value);
1431        value
1432    }
1433
1434    fn output_inner(&self, scope_id: usize, ordinal: usize, depth: usize) -> ProjectionNullability {
1435        use ProjectionNullability::*;
1436        let frame = &self.scopes[scope_id];
1437        let query = crate::scope::scope_query(&frame.scope.expression);
1438        if let Expression::Select(select) = query {
1439            return select
1440                .expressions
1441                .get(ordinal)
1442                .map(|expression| self.expression(scope_id, expression, depth))
1443                .unwrap_or(Unknown);
1444        }
1445        if !is_set_operation(query) || frame.branches.len() != 2 {
1446            return Unknown;
1447        }
1448        let (left, right) =
1449            match crate::set_operation::set_operation_layout(query, Some(self.dialect)) {
1450                Ok(Some(layout)) => match layout.outputs.get(ordinal) {
1451                    Some(output) => (output.left_ordinal, output.right_ordinal),
1452                    None => return Unknown,
1453                },
1454                Ok(None) => (Some(ordinal), Some(ordinal)),
1455                Err(_) => return Unknown,
1456            };
1457        let branch = |id, ordinal: Option<usize>| {
1458            ordinal
1459                .map(|ordinal| self.output(id, ordinal, depth))
1460                .unwrap_or(Nullable)
1461        };
1462        let left = branch(frame.branches[0], left);
1463        if matches!(query, Expression::Except(_)) {
1464            return left;
1465        }
1466        let right = branch(frame.branches[1], right);
1467        if matches!(query, Expression::Intersect(_)) {
1468            return match (left, right) {
1469                (NonNull, _) | (_, NonNull) => NonNull,
1470                (Nullable, Nullable) => Nullable,
1471                _ => Unknown,
1472            };
1473        }
1474        match (left, right) {
1475            (NonNull, NonNull) => NonNull,
1476            (Nullable, _) | (_, Nullable) => Nullable,
1477            _ => Unknown,
1478        }
1479    }
1480
1481    fn expression(
1482        &self,
1483        scope_id: usize,
1484        expression: &Expression,
1485        depth: usize,
1486    ) -> ProjectionNullability {
1487        use ProjectionNullability::*;
1488        if depth >= Self::MAX_DEPTH {
1489            return Unknown;
1490        }
1491        let depth = depth + 1;
1492        match expression {
1493            Expression::Alias(alias) => self.expression(scope_id, &alias.this, depth),
1494            Expression::Annotated(annotated) => self.expression(scope_id, &annotated.this, depth),
1495            Expression::Paren(paren) => self.expression(scope_id, &paren.this, depth),
1496            Expression::Literal(_)
1497            | Expression::Boolean(_)
1498            | Expression::Count(_)
1499            | Expression::CountIf(_) => NonNull,
1500            Expression::Null(_) => Nullable,
1501            Expression::Cast(cast) => self.expression(scope_id, &cast.this, depth),
1502            Expression::Column(column) => {
1503                if column.span.or(column.name.span).is_some_and(|span| {
1504                    self.uncertain_columns.contains_key(&(span.start, span.end))
1505                }) {
1506                    Unknown
1507                } else {
1508                    self.column(scope_id, &column.name, column.table.as_ref(), depth)
1509                }
1510            }
1511            Expression::Identifier(identifier) => self.column(scope_id, identifier, None, depth),
1512            Expression::Coalesce(func) => {
1513                let mut all_nullable = !func.expressions.is_empty();
1514                for expression in &func.expressions {
1515                    match self.expression(scope_id, expression, depth) {
1516                        NonNull => return NonNull,
1517                        Nullable => {}
1518                        Unknown => all_nullable = false,
1519                    }
1520                }
1521                if all_nullable {
1522                    Nullable
1523                } else {
1524                    Unknown
1525                }
1526            }
1527            _ => Unknown,
1528        }
1529    }
1530
1531    fn source_scope(
1532        &self,
1533        frame: &NullabilityScope<'_>,
1534        name: &str,
1535        source: &SourceInfo,
1536    ) -> Option<usize> {
1537        if let Some(Expression::Table(table)) = frame.bindings.get(name).copied() {
1538            if table.schema.is_none() && table.catalog.is_none() {
1539                return frame
1540                    .ctes
1541                    .get(&crate::set_operation::identifier_key(
1542                        &table.name,
1543                        Some(self.dialect),
1544                    ))
1545                    .copied();
1546            }
1547        }
1548        if source.kind == SourceKind::DerivedTable && source.is_scope {
1549            return frame.derived.iter().copied().find(|id| {
1550                crate::scope::scope_query(&self.scopes[*id].scope.expression)
1551                    == crate::scope::scope_query(&source.expression)
1552            });
1553        }
1554        None
1555    }
1556
1557    fn source_columns(
1558        &self,
1559        frame: &NullabilityScope<'_>,
1560        name: &str,
1561        source: &SourceInfo,
1562    ) -> Option<Vec<Identifier>> {
1563        if let Some(id) = self.source_scope(frame, name, source) {
1564            let expression = if matches!(
1565                frame.bindings.get(name).copied(),
1566                Some(Expression::Table(_))
1567            ) {
1568                &self.scopes[id].scope.expression
1569            } else {
1570                &source.expression
1571            };
1572            return crate::set_operation::query_output_identifiers(expression, Some(self.dialect))
1573                .ok();
1574        }
1575        if source.kind == SourceKind::Table {
1576            let mut resolver = crate::resolver::Resolver::new(
1577                &frame.selected,
1578                self.mapping_schema.unwrap_or(&self.empty_schema),
1579                false,
1580            );
1581            let columns = resolver.get_source_columns(name).ok()?;
1582            if columns.is_empty() || columns.iter().any(|name| name == "*") {
1583                return None;
1584            }
1585            Some(columns.into_iter().map(Identifier::new).collect())
1586        } else {
1587            crate::set_operation::query_output_identifiers(&source.expression, Some(self.dialect))
1588                .ok()
1589        }
1590    }
1591
1592    fn column(
1593        &self,
1594        scope_id: usize,
1595        column: &Identifier,
1596        qualifier: Option<&Identifier>,
1597        depth: usize,
1598    ) -> ProjectionNullability {
1599        use ProjectionNullability::*;
1600        let frame = &self.scopes[scope_id];
1601        let column_key = crate::set_operation::identifier_key(column, Some(self.dialect));
1602        let same_column = |name: &Identifier| {
1603            crate::set_operation::identifier_key(name, Some(self.dialect)) == column_key
1604        };
1605        let binding = if let Some(qualifier) = qualifier {
1606            let key = crate::set_operation::identifier_key(qualifier, Some(self.dialect));
1607            let mut matches = frame
1608                .bindings
1609                .iter()
1610                .filter(|(_, expression)| {
1611                    expression_source_identifier(expression).is_some_and(|identifier| {
1612                        crate::set_operation::identifier_key(identifier, Some(self.dialect)) == key
1613                    })
1614                })
1615                .filter_map(|(name, _)| frame.selected.sources.get_key_value(name));
1616            let binding = matches.next();
1617            if matches.next().is_some() {
1618                return Unknown;
1619            }
1620            binding
1621        } else {
1622            let mut binding = None;
1623            for (name, source) in &frame.selected.sources {
1624                // An open source can also provide this column. Do not pick a
1625                // known source just because the other source lacks metadata.
1626                let Some(columns) = self.source_columns(frame, name, source) else {
1627                    return Unknown;
1628                };
1629                if columns.iter().any(&same_column) {
1630                    if binding.is_some() {
1631                        return Unknown;
1632                    }
1633                    binding = Some((name, source));
1634                }
1635            }
1636            binding
1637        };
1638        let Some((name, source)) = binding else {
1639            return Unknown;
1640        };
1641        if frame
1642            .bindings
1643            .get(name)
1644            .and_then(|expression| expression_source_identifier(expression))
1645            .is_some_and(|identifier| {
1646                frame
1647                    .nullable_sources
1648                    .contains(&crate::set_operation::identifier_key(
1649                        identifier,
1650                        Some(self.dialect),
1651                    ))
1652            })
1653        {
1654            return Nullable;
1655        }
1656        let target = self.source_scope(frame, name, source);
1657        if target.is_none() && source.kind == SourceKind::Table {
1658            let info = self.schema.and_then(|schema| {
1659                source_table_name(source).and_then(|table| schema.column(&table, &column.name))
1660            });
1661            return match info {
1662                Some(info) if info.primary_key || info.nullable == Some(false) => NonNull,
1663                Some(info) if info.nullable == Some(true) => Nullable,
1664                _ => Unknown,
1665            };
1666        }
1667        let Some(columns) = self.source_columns(frame, name, source) else {
1668            return Unknown;
1669        };
1670        let mut ordinals = columns
1671            .iter()
1672            .enumerate()
1673            .filter(|(_, name)| same_column(name))
1674            .map(|(ordinal, _)| ordinal);
1675        let Some(ordinal) = ordinals.next() else {
1676            return Unknown;
1677        };
1678        if ordinals.next().is_some() {
1679            return Unknown;
1680        }
1681        target
1682            .map(|id| self.output(id, ordinal, depth))
1683            .unwrap_or(Unknown)
1684    }
1685}
1686
1687fn terminal_references_from_lineage(node: &LineageNode) -> Vec<ColumnReferenceFact> {
1688    let mut refs = Vec::new();
1689    collect_terminal_references(node, &mut refs);
1690    dedupe_column_refs(refs)
1691}
1692
1693fn collect_terminal_references(node: &LineageNode, refs: &mut Vec<ColumnReferenceFact>) {
1694    if node.downstream.is_empty() {
1695        if let Some(reference) = column_reference_from_lineage_node(node) {
1696            refs.push(reference);
1697        }
1698        return;
1699    }
1700
1701    for child in &node.downstream {
1702        collect_terminal_references(child, refs);
1703    }
1704}
1705
1706fn column_reference_from_lineage_node(node: &LineageNode) -> Option<ColumnReferenceFact> {
1707    match &node.expression {
1708        Expression::Column(column) => {
1709            let source_name = non_empty_string(node.source_name.clone());
1710            let table =
1711                lineage_node_table(node).or_else(|| column.table.as_ref().map(|t| t.name.clone()));
1712            let confidence = if node.source_kind == SourceKind::Unknown && source_name.is_none() {
1713                ReferenceConfidence::Unknown
1714            } else {
1715                ReferenceConfidence::Resolved
1716            };
1717            Some(ColumnReferenceFact {
1718                source_name,
1719                source_alias: node.source_alias.clone(),
1720                source_kind: node.source_kind,
1721                table,
1722                column: column.name.name.clone(),
1723                unqualified: column.table.is_none(),
1724                confidence,
1725            })
1726        }
1727        Expression::Star(_) => Some(ColumnReferenceFact {
1728            source_name: non_empty_string(node.source_name.clone()),
1729            source_alias: node.source_alias.clone(),
1730            source_kind: node.source_kind,
1731            table: lineage_node_table(node),
1732            column: "*".to_string(),
1733            unqualified: true,
1734            confidence: if node.source_kind == SourceKind::Unknown {
1735                ReferenceConfidence::Unknown
1736            } else {
1737                ReferenceConfidence::Resolved
1738            },
1739        }),
1740        _ => None,
1741    }
1742}
1743
1744fn lineage_node_table(node: &LineageNode) -> Option<String> {
1745    match &node.source {
1746        Expression::Table(table) => Some(table_name(table)),
1747        _ => None,
1748    }
1749}
1750
1751fn fallback_column_references(expression: &Expression, scope: &Scope) -> Vec<ColumnReferenceFact> {
1752    let mut refs = Vec::new();
1753    let source_count = scope.sources.len();
1754    let single_source = if source_count == 1 {
1755        scope.sources.iter().next()
1756    } else {
1757        None
1758    };
1759
1760    for column_expr in expression.find_all(|candidate| matches!(candidate, Expression::Column(_))) {
1761        if let Expression::Column(column) = column_expr {
1762            if column.name.name == "*" {
1763                continue;
1764            }
1765            let source = column
1766                .table
1767                .as_ref()
1768                .and_then(|table| scope.sources.get(&table.name));
1769            let (source_name, source_alias, source_kind, table, confidence) =
1770                if let Some(table_identifier) = &column.table {
1771                    if let Some(source) = source {
1772                        (
1773                            Some(table_identifier.name.clone()),
1774                            source.alias.clone(),
1775                            source.kind,
1776                            source_table_name(source)
1777                                .or_else(|| Some(table_identifier.name.clone())),
1778                            ReferenceConfidence::Resolved,
1779                        )
1780                    } else {
1781                        (
1782                            Some(table_identifier.name.clone()),
1783                            None,
1784                            SourceKind::Unknown,
1785                            Some(table_identifier.name.clone()),
1786                            ReferenceConfidence::Unknown,
1787                        )
1788                    }
1789                } else if let Some((name, source)) = single_source {
1790                    (
1791                        Some(name.clone()),
1792                        source.alias.clone(),
1793                        source.kind,
1794                        source_table_name(source).or_else(|| Some(name.clone())),
1795                        ReferenceConfidence::Resolved,
1796                    )
1797                } else if source_count > 1 {
1798                    (
1799                        None,
1800                        None,
1801                        SourceKind::Unknown,
1802                        None,
1803                        ReferenceConfidence::Ambiguous,
1804                    )
1805                } else {
1806                    (
1807                        None,
1808                        None,
1809                        SourceKind::Unknown,
1810                        None,
1811                        ReferenceConfidence::Unknown,
1812                    )
1813                };
1814
1815            refs.push(ColumnReferenceFact {
1816                source_name,
1817                source_alias,
1818                source_kind,
1819                table,
1820                column: column.name.name.clone(),
1821                unqualified: column.table.is_none(),
1822                confidence,
1823            });
1824        }
1825    }
1826
1827    dedupe_column_refs(refs)
1828}
1829
1830fn dedupe_column_refs(refs: Vec<ColumnReferenceFact>) -> Vec<ColumnReferenceFact> {
1831    let mut seen = HashSet::new();
1832    let mut deduped = Vec::new();
1833
1834    for reference in refs {
1835        let key = (
1836            reference.source_name.clone(),
1837            reference.source_alias.clone(),
1838            reference.table.clone(),
1839            reference.column.clone(),
1840            format!("{:?}", reference.source_kind),
1841            reference.unqualified,
1842            format!("{:?}", reference.confidence),
1843        );
1844        if seen.insert(key) {
1845            deduped.push(reference);
1846        }
1847    }
1848
1849    deduped
1850}
1851
1852fn relation_facts(
1853    scope: &Scope,
1854    mapping_schema: Option<&crate::schema::MappingSchema>,
1855    dialect: DialectType,
1856) -> Vec<RelationFact> {
1857    let mut relations = Vec::new();
1858    let mut seen = HashSet::new();
1859    collect_relation_facts(scope, mapping_schema, dialect, &mut seen, &mut relations);
1860
1861    relations.sort_by(|left, right| {
1862        left.name
1863            .cmp(&right.name)
1864            .then_with(|| left.alias.cmp(&right.alias))
1865    });
1866    relations
1867}
1868
1869fn collect_relation_facts(
1870    scope: &Scope,
1871    mapping_schema: Option<&crate::schema::MappingSchema>,
1872    dialect: DialectType,
1873    seen: &mut HashSet<String>,
1874    relations: &mut Vec<RelationFact>,
1875) {
1876    for relation in scope.sources.iter().map(|(source_name, source)| {
1877        let identity = source_table_identity(source);
1878        RelationFact {
1879            name: source
1880                .lineage_name
1881                .clone()
1882                .or_else(|| identity.as_ref().map(|identity| identity.name.clone()))
1883                .unwrap_or_else(|| source_name.clone()),
1884            alias: source.alias.clone().or_else(|| source_alias(source)),
1885            kind: source.kind,
1886            columns: source_columns(source, mapping_schema, dialect),
1887            catalog: identity
1888                .as_ref()
1889                .and_then(|identity| identity.catalog.clone()),
1890            schema: identity
1891                .as_ref()
1892                .and_then(|identity| identity.schema.clone()),
1893            table: identity
1894                .as_ref()
1895                .and_then(|identity| identity.table.clone()),
1896        }
1897    }) {
1898        let key = format!("{:?}|{}|{:?}", relation.kind, relation.name, relation.alias);
1899        if seen.insert(key) {
1900            relations.push(relation);
1901        }
1902    }
1903
1904    for branch_scope in &scope.union_scopes {
1905        collect_relation_facts(branch_scope, mapping_schema, dialect, seen, relations);
1906    }
1907}
1908
1909fn base_table_facts(
1910    scope: &Scope,
1911    mapping_schema: Option<&crate::schema::MappingSchema>,
1912    dialect: DialectType,
1913) -> Vec<RelationFact> {
1914    let mut relations = Vec::new();
1915    let mut seen = HashSet::new();
1916
1917    collect_base_table_facts(scope, mapping_schema, dialect, &mut seen, &mut relations);
1918
1919    relations.sort_by(|left, right| left.name.cmp(&right.name));
1920    relations
1921}
1922
1923fn collect_base_table_facts(
1924    scope: &Scope,
1925    mapping_schema: Option<&crate::schema::MappingSchema>,
1926    dialect: DialectType,
1927    seen: &mut HashSet<String>,
1928    relations: &mut Vec<RelationFact>,
1929) {
1930    for source in scope.sources.values() {
1931        if source.kind != SourceKind::Table {
1932            continue;
1933        }
1934
1935        let Some(identity) = source_table_identity(source) else {
1936            continue;
1937        };
1938
1939        if seen.insert(identity.name.clone()) {
1940            relations.push(RelationFact {
1941                name: identity.name,
1942                alias: source.alias.clone().or_else(|| source_alias(source)),
1943                kind: SourceKind::Table,
1944                columns: source_columns(source, mapping_schema, dialect),
1945                catalog: identity.catalog,
1946                schema: identity.schema,
1947                table: identity.table,
1948            });
1949        }
1950    }
1951
1952    for child_scope in scope
1953        .cte_scopes
1954        .iter()
1955        .chain(scope.union_scopes.iter())
1956        .chain(scope.table_scopes.iter())
1957        .chain(scope.derived_table_scopes.iter())
1958        .chain(scope.subquery_scopes.iter())
1959    {
1960        collect_base_table_facts(child_scope, mapping_schema, dialect, seen, relations);
1961    }
1962}
1963
1964fn source_columns(
1965    source: &SourceInfo,
1966    mapping_schema: Option<&crate::schema::MappingSchema>,
1967    dialect: DialectType,
1968) -> Vec<String> {
1969    match &source.expression {
1970        Expression::Table(table) => mapping_schema
1971            .and_then(|schema| schema.column_names(&table_name(table)).ok())
1972            .unwrap_or_default(),
1973        Expression::Select(_)
1974        | Expression::Union(_)
1975        | Expression::Intersect(_)
1976        | Expression::Except(_) => {
1977            get_output_column_names_for_dialect(&source.expression, Some(dialect))
1978        }
1979        Expression::Subquery(subquery) => {
1980            get_output_column_names_for_dialect(&subquery.this, Some(dialect))
1981        }
1982        Expression::Cte(cte) if !cte.columns.is_empty() => cte
1983            .columns
1984            .iter()
1985            .map(|column| column.name.clone())
1986            .collect(),
1987        Expression::Cte(cte) => get_output_column_names_for_dialect(&cte.this, Some(dialect)),
1988        _ => Vec::new(),
1989    }
1990}
1991
1992fn source_table_name(source: &SourceInfo) -> Option<String> {
1993    source_table_identity(source).map(|identity| identity.name)
1994}
1995
1996fn source_alias(source: &SourceInfo) -> Option<String> {
1997    match &source.expression {
1998        Expression::Table(table) => table.alias.as_ref().map(|alias| alias.name.clone()),
1999        Expression::Subquery(subquery) => subquery.alias.as_ref().map(|alias| alias.name.clone()),
2000        _ => None,
2001    }
2002}
2003
2004fn table_name(table: &TableRef) -> String {
2005    let mut parts = Vec::new();
2006    if let Some(catalog) = &table.catalog {
2007        parts.push(catalog.name.clone());
2008    }
2009    if let Some(schema) = &table.schema {
2010        parts.push(schema.name.clone());
2011    }
2012    parts.push(table.name.name.clone());
2013    parts.join(".")
2014}
2015
2016#[derive(Debug, Clone)]
2017struct RelationIdentity {
2018    name: String,
2019    catalog: Option<String>,
2020    schema: Option<String>,
2021    table: Option<String>,
2022}
2023
2024fn source_table_identity(source: &SourceInfo) -> Option<RelationIdentity> {
2025    match &source.expression {
2026        Expression::Table(table) => Some(table_identity(table)),
2027        _ => None,
2028    }
2029}
2030
2031fn table_identity(table: &TableRef) -> RelationIdentity {
2032    RelationIdentity {
2033        name: table_name(table),
2034        catalog: table.catalog.as_ref().map(|catalog| catalog.name.clone()),
2035        schema: table.schema.as_ref().map(|schema| schema.name.clone()),
2036        table: Some(table.name.name.clone()),
2037    }
2038}
2039
2040fn set_operation_facts(
2041    expression: &Expression,
2042    scope: &Scope,
2043    dialect: DialectType,
2044    nullability: &NullabilityContext<'_>,
2045) -> Vec<SetOperationFact> {
2046    let mut facts = Vec::new();
2047    collect_set_operation_facts(expression, scope, dialect, nullability, &mut facts);
2048    facts
2049}
2050
2051fn collect_set_operation_facts(
2052    expression: &Expression,
2053    scope: &Scope,
2054    dialect: DialectType,
2055    nullability: &NullabilityContext<'_>,
2056    facts: &mut Vec<SetOperationFact>,
2057) {
2058    match expression {
2059        Expression::Union(union) => {
2060            facts.push(SetOperationFact {
2061                kind: "union".to_string(),
2062                all: union.all,
2063                distinct: union.distinct,
2064                output_columns: get_output_column_names_for_dialect(expression, Some(dialect)),
2065                branches: set_operation_branches(
2066                    &union.left,
2067                    &union.right,
2068                    scope,
2069                    dialect,
2070                    SetOperationBranchRole::Value,
2071                    nullability,
2072                ),
2073            });
2074            collect_set_operation_facts(
2075                &union.left,
2076                scope.union_scopes.first().unwrap_or(scope),
2077                dialect,
2078                nullability,
2079                facts,
2080            );
2081            collect_set_operation_facts(
2082                &union.right,
2083                scope.union_scopes.get(1).unwrap_or(scope),
2084                dialect,
2085                nullability,
2086                facts,
2087            );
2088        }
2089        Expression::Intersect(intersect) => {
2090            facts.push(SetOperationFact {
2091                kind: "intersect".to_string(),
2092                all: intersect.all,
2093                distinct: intersect.distinct,
2094                output_columns: get_output_column_names_for_dialect(expression, Some(dialect)),
2095                branches: set_operation_branches(
2096                    &intersect.left,
2097                    &intersect.right,
2098                    scope,
2099                    dialect,
2100                    SetOperationBranchRole::Filter,
2101                    nullability,
2102                ),
2103            });
2104            collect_set_operation_facts(
2105                &intersect.left,
2106                scope.union_scopes.first().unwrap_or(scope),
2107                dialect,
2108                nullability,
2109                facts,
2110            );
2111            collect_set_operation_facts(
2112                &intersect.right,
2113                scope.union_scopes.get(1).unwrap_or(scope),
2114                dialect,
2115                nullability,
2116                facts,
2117            );
2118        }
2119        Expression::Except(except) => {
2120            facts.push(SetOperationFact {
2121                kind: "except".to_string(),
2122                all: except.all,
2123                distinct: except.distinct,
2124                output_columns: get_output_column_names_for_dialect(expression, Some(dialect)),
2125                branches: set_operation_branches(
2126                    &except.left,
2127                    &except.right,
2128                    scope,
2129                    dialect,
2130                    SetOperationBranchRole::Filter,
2131                    nullability,
2132                ),
2133            });
2134            collect_set_operation_facts(
2135                &except.left,
2136                scope.union_scopes.first().unwrap_or(scope),
2137                dialect,
2138                nullability,
2139                facts,
2140            );
2141            collect_set_operation_facts(
2142                &except.right,
2143                scope.union_scopes.get(1).unwrap_or(scope),
2144                dialect,
2145                nullability,
2146                facts,
2147            );
2148        }
2149        Expression::Subquery(subquery) => {
2150            collect_set_operation_facts(&subquery.this, scope, dialect, nullability, facts);
2151        }
2152        _ => {}
2153    }
2154}
2155
2156fn set_operation_branches(
2157    left: &Expression,
2158    right: &Expression,
2159    scope: &Scope,
2160    dialect: DialectType,
2161    right_role: SetOperationBranchRole,
2162    nullability: &NullabilityContext<'_>,
2163) -> Vec<SetOperationBranchFact> {
2164    vec![
2165        SetOperationBranchFact {
2166            index: 0,
2167            role: SetOperationBranchRole::Value,
2168            projections: projection_facts_for_query(
2169                left,
2170                scope.union_scopes.first().unwrap_or(scope),
2171                dialect,
2172                nullability,
2173            ),
2174        },
2175        SetOperationBranchFact {
2176            index: 1,
2177            role: right_role,
2178            projections: projection_facts_for_query(
2179                right,
2180                scope.union_scopes.get(1).unwrap_or(scope),
2181                dialect,
2182                nullability,
2183            ),
2184        },
2185    ]
2186}
2187
2188fn non_empty_string(value: String) -> Option<String> {
2189    if value.is_empty() {
2190        None
2191    } else {
2192        Some(value)
2193    }
2194}