Skip to main content

polyglot_sql/
resolver.rs

1//! Column Resolver Module
2//!
3//! This module provides functionality for resolving column references to their
4//! source tables. It handles:
5//! - Finding which table a column belongs to
6//! - Resolving ambiguous column references
7//! - Handling join context for disambiguation
8//! - Supporting set operations (UNION, INTERSECT, EXCEPT)
9//!
10//! Based on the Python implementation in `sqlglot/optimizer/resolver.py`.
11
12use crate::dialects::DialectType;
13use crate::expressions::{Expression, Identifier, TableRef};
14#[cfg(feature = "generate")]
15use crate::generator::Generator;
16use crate::schema::{normalize_name, Schema};
17use crate::scope::{Scope, SourceInfo, SourceKind};
18use crate::traversal::ExpressionWalk;
19use std::collections::{HashMap, HashSet};
20use thiserror::Error;
21
22/// Errors that can occur during column resolution
23#[derive(Debug, Error, Clone)]
24pub enum ResolverError {
25    #[error("Unknown table: {0}")]
26    UnknownTable(String),
27
28    #[error("Ambiguous column: {column} appears in multiple sources: {sources}")]
29    AmbiguousColumn { column: String, sources: String },
30
31    #[error("Column not found: {0}")]
32    ColumnNotFound(String),
33
34    #[error("Unknown set operation: {0}")]
35    UnknownSetOperation(String),
36
37    #[error("Invalid set operation: {0}")]
38    InvalidSetOperation(String),
39}
40
41/// Result type for resolver operations
42pub type ResolverResult<T> = Result<T, ResolverError>;
43
44/// Helper for resolving columns to their source tables.
45///
46/// This is a struct so we can lazily load some things and easily share
47/// them across functions.
48pub struct Resolver<'a> {
49    /// The scope being analyzed
50    pub scope: &'a Scope,
51    /// The schema for table/column information
52    schema: &'a dyn Schema,
53    /// The dialect being used
54    pub dialect: Option<DialectType>,
55    /// Whether to infer schema from context
56    infer_schema: bool,
57    /// Cached source columns: source_name -> column names
58    source_columns_cache: HashMap<String, Vec<String>>,
59    /// Cached unambiguous columns: column_name -> source_name
60    unambiguous_columns_cache: Option<HashMap<String, String>>,
61    /// Cached set of all available columns
62    all_columns_cache: Option<HashSet<String>>,
63    /// Actual correlated parents, nearest first. Physical schema entries alone
64    /// do not make a relation visible in a query.
65    outer_scopes: Vec<&'a Scope>,
66}
67
68impl<'a> Resolver<'a> {
69    /// Create a new resolver for a scope
70    pub fn new(scope: &'a Scope, schema: &'a dyn Schema, infer_schema: bool) -> Self {
71        Self {
72            scope,
73            schema,
74            dialect: schema.dialect(),
75            infer_schema,
76            source_columns_cache: HashMap::new(),
77            unambiguous_columns_cache: None,
78            all_columns_cache: None,
79            outer_scopes: Vec::new(),
80        }
81    }
82
83    pub(crate) fn with_outer_scopes(mut self, scopes: &'a [Scope]) -> Self {
84        self.outer_scopes = scopes.iter().rev().collect();
85        self
86    }
87
88    pub(crate) fn outer_source_exists(&self, name: &str) -> bool {
89        self.outer_scopes.iter().any(|scope| {
90            scope.sources.keys().any(|source| {
91                normalize_name(source, self.dialect, true, true)
92                    == normalize_name(name, self.dialect, true, true)
93            })
94        })
95    }
96
97    pub(crate) fn find_column_in_outer_scopes(&self, column: &str) -> Option<String> {
98        let mut shadowed = HashSet::new();
99        shadowed.extend(
100            self.scope
101                .sources
102                .keys()
103                .map(|name| normalize_name(name, self.dialect, true, true)),
104        );
105        for outer in &self.outer_scopes {
106            let mut visible = (*outer).clone();
107            visible
108                .sources
109                .retain(|name, _| shadowed.insert(normalize_name(name, self.dialect, true, true)));
110            let mut resolver = Resolver::new(&visible, self.schema, self.infer_schema);
111            if resolver.is_ambiguous(column) {
112                return None;
113            }
114            if let Some(table) = resolver.get_table(column) {
115                return Some(table);
116            }
117        }
118        None
119    }
120
121    /// Get the table for a column name.
122    ///
123    /// Returns the table name if it can be found/inferred.
124    pub fn get_table(&mut self, column_name: &str) -> Option<String> {
125        // Try to find table from all sources (unambiguous lookup)
126        let table_name = self.get_table_name_from_sources(column_name, None);
127
128        // If we found a table, return it
129        if table_name.is_some() {
130            return table_name;
131        }
132
133        // If schema inference is enabled and exactly one source has no schema,
134        // assume the column belongs to that source
135        if self.infer_schema {
136            let sources_without_schema: Vec<_> = self
137                .get_all_source_columns()
138                .iter()
139                .filter(|(_, columns)| columns.is_empty() || columns.contains(&"*".to_string()))
140                .map(|(name, _)| name.clone())
141                .collect();
142
143            if sources_without_schema.len() == 1 {
144                return Some(sources_without_schema[0].clone());
145            }
146        }
147
148        None
149    }
150
151    /// Get the table for a column, returning an Identifier
152    pub fn get_table_identifier(&mut self, column_name: &str) -> Option<Identifier> {
153        self.get_table(column_name).map(Identifier::new)
154    }
155
156    /// Check if a table exists in the schema (not necessarily in the current scope).
157    /// Catalogue membership alone does not imply lexical visibility.
158    pub fn table_exists_in_schema(&self, table_name: &str) -> bool {
159        self.schema.column_names(table_name).is_ok()
160    }
161
162    /// Find the table for a column by searching all schema tables not in the current scope.
163    /// Returns Some(table_name) if the column is found in exactly one non-local
164    /// catalogue table. This is not a correlated-scope lookup: qualification
165    /// must use the actual lexical parents instead.
166    pub fn find_column_in_outer_schema_tables(&self, column_name: &str) -> Option<String> {
167        let tables = self.schema.find_tables_for_column(column_name);
168        // Filter to tables NOT in the current scope
169        let outer_tables: Vec<String> = tables
170            .into_iter()
171            .filter(|t| !self.scope.sources.contains_key(t))
172            .collect();
173        // Only return if unambiguous (exactly one outer table has this column)
174        if outer_tables.len() == 1 {
175            Some(outer_tables.into_iter().next().unwrap())
176        } else {
177            None
178        }
179    }
180
181    /// Get all available columns across all sources in this scope
182    pub fn all_columns(&mut self) -> &HashSet<String> {
183        if self.all_columns_cache.is_none() {
184            let mut all = HashSet::new();
185            for columns in self.get_all_source_columns().values() {
186                all.extend(columns.iter().cloned());
187            }
188            self.all_columns_cache = Some(all);
189        }
190        self.all_columns_cache
191            .as_ref()
192            .expect("cache populated above")
193    }
194
195    /// Get column names for a source.
196    ///
197    /// Returns the list of column names available from the given source.
198    pub fn get_source_columns(&mut self, source_name: &str) -> ResolverResult<Vec<String>> {
199        // Check cache first
200        if let Some(columns) = self.source_columns_cache.get(source_name) {
201            return Ok(columns.clone());
202        }
203
204        // Get the source info
205        let source_info = self
206            .scope
207            .sources
208            .get(source_name)
209            .ok_or_else(|| ResolverError::UnknownTable(source_name.to_string()))?;
210
211        let columns = self.extract_columns_from_source(source_info)?;
212
213        // Cache the result
214        self.source_columns_cache
215            .insert(source_name.to_string(), columns.clone());
216
217        Ok(columns)
218    }
219
220    /// Extract column names from a source expression
221    fn extract_columns_from_source(&self, source_info: &SourceInfo) -> ResolverResult<Vec<String>> {
222        match &source_info.expression {
223            // ClickHouse ARRAY JOIN item, e.g. `arr AS item`.
224            Expression::Alias(alias) if source_info.kind == SourceKind::Virtual => {
225                Ok(alias_output_columns(alias))
226            }
227            expression => self.get_source_columns_for_expression(expression),
228        }
229    }
230
231    fn get_source_columns_for_expression(
232        &self,
233        expression: &Expression,
234    ) -> ResolverResult<Vec<String>> {
235        let columns = match expression {
236            Expression::Table(table) => {
237                // For tables, try to get columns from schema.
238                // Build the fully qualified name (catalog.schema.table) to
239                // match how MappingSchema stores hierarchical keys.
240                let table_name = qualified_table_name(table);
241                match self.schema.column_names(&table_name) {
242                    Ok(cols) => cols,
243                    Err(_) => Vec::new(), // Schema might not have this table
244                }
245            }
246            Expression::Subquery(subquery) => {
247                // For subqueries, get named_selects from the inner query
248                apply_alias_columns(
249                    self.get_named_selects(&subquery.this),
250                    &subquery.column_aliases,
251                )
252            }
253            Expression::Values(values) => self.get_values_column_names(values),
254            Expression::Select(select) => {
255                // For derived tables that are SELECT expressions
256                self.get_select_column_names(select)
257            }
258            Expression::Union(union) => {
259                // For UNION, columns come from the set operation
260                self.get_source_columns_from_set_op(&Expression::Union(union.clone()))?
261            }
262            Expression::Intersect(intersect) => {
263                self.get_source_columns_from_set_op(&Expression::Intersect(intersect.clone()))?
264            }
265            Expression::Except(except) => {
266                self.get_source_columns_from_set_op(&Expression::Except(except.clone()))?
267            }
268            Expression::Cte(cte) => {
269                if !cte.columns.is_empty() {
270                    cte.columns.iter().map(|c| c.name.clone()).collect()
271                } else {
272                    self.get_named_selects(&cte.this)
273                }
274            }
275            Expression::Pivot(pivot) => self.get_pivot_output_columns(pivot),
276            Expression::Unpivot(unpivot) => self.get_unpivot_output_columns(unpivot),
277            Expression::Alias(alias) if matches!(&alias.this, Expression::Unnest(_)) => {
278                alias_output_columns(alias)
279            }
280            Expression::Alias(alias) => {
281                let columns = self.get_source_columns_for_expression(&alias.this)?;
282                apply_alias_columns(columns, &alias.column_aliases)
283            }
284            Expression::Unnest(unnest) => unnest_output_columns(unnest),
285            Expression::Lateral(lateral) => lateral_output_columns(lateral),
286            Expression::LateralView(lateral_view) => lateral_view_output_columns(lateral_view),
287            Expression::Paren(paren) => self.get_source_columns_for_expression(&paren.this)?,
288            _ => Vec::new(),
289        };
290
291        Ok(columns)
292    }
293
294    /// Get named selects (column names) from an expression
295    fn get_named_selects(&self, expr: &Expression) -> Vec<String> {
296        if let Ok(identifiers) = crate::set_operation::query_output_identifiers(expr, self.dialect)
297        {
298            return identifiers
299                .into_iter()
300                .map(|identifier| identifier.name)
301                .collect();
302        }
303
304        match expr {
305            Expression::Select(select) => self.get_select_column_names(select),
306            Expression::Union(union) => {
307                // For unions, use the left side's columns
308                self.get_named_selects(&union.left)
309            }
310            Expression::Intersect(intersect) => self.get_named_selects(&intersect.left),
311            Expression::Except(except) => self.get_named_selects(&except.left),
312            Expression::Subquery(subquery) => apply_alias_columns(
313                self.get_named_selects(&subquery.this),
314                &subquery.column_aliases,
315            ),
316            Expression::Values(values) => self.get_values_column_names(values),
317            Expression::Alias(alias) => {
318                let columns = self.get_named_selects(&alias.this);
319                apply_alias_columns(columns, &alias.column_aliases)
320            }
321            Expression::Paren(paren) => self.get_named_selects(&paren.this),
322            _ => Vec::new(),
323        }
324    }
325
326    /// Get column names from a SELECT expression
327    fn get_select_column_names(&self, select: &crate::expressions::Select) -> Vec<String> {
328        select
329            .expressions
330            .iter()
331            .filter_map(|expr| self.get_expression_alias(expr))
332            .collect()
333    }
334
335    fn get_values_column_names(&self, values: &crate::expressions::Values) -> Vec<String> {
336        let width = values
337            .expressions
338            .first()
339            .map_or(0, |row| row.expressions.len());
340        let prefix = match self.dialect {
341            Some(DialectType::Snowflake) => "COLUMN",
342            Some(DialectType::PostgreSQL) => "column",
343            // Default VALUES names are not standardized. Preserve explicit
344            // aliases without inventing names for other dialects.
345            _ => {
346                return values
347                    .column_aliases
348                    .iter()
349                    .map(|alias| alias.name.clone())
350                    .collect()
351            }
352        };
353        apply_alias_columns(
354            (1..=width)
355                .map(|ordinal| format!("{prefix}{ordinal}"))
356                .collect(),
357            &values.column_aliases,
358        )
359    }
360
361    /// Get the alias or name for a select expression
362    fn get_expression_alias(&self, expr: &Expression) -> Option<String> {
363        match expr {
364            Expression::Alias(alias) => Some(alias.alias.name.clone()),
365            Expression::Column(col) => Some(col.name.name.clone()),
366            Expression::Star(_) => Some("*".to_string()),
367            Expression::Identifier(id) => Some(id.name.clone()),
368            _ => None,
369        }
370    }
371
372    fn get_pivot_output_columns(&self, pivot: &crate::expressions::Pivot) -> Vec<String> {
373        if pivot.unpivot {
374            return self.get_pivot_unpivot_output_columns(pivot);
375        }
376
377        let pre_columns = self.get_source_output_columns(&pivot.this);
378        if pre_columns.is_empty() || pre_columns.iter().any(|column| column == "*") {
379            return Vec::new();
380        }
381
382        let excluded = pivot_excluded_source_columns(pivot, self.dialect);
383        let generated = pivot_generated_output_columns(pivot, self.dialect);
384        if excluded.is_empty() || generated.is_empty() {
385            return Vec::new();
386        }
387
388        let mut columns: Vec<String> = pre_columns
389            .into_iter()
390            .filter(|column| !excluded.contains(&normalize_column_name(column, self.dialect)))
391            .collect();
392        columns.extend(generated);
393        apply_alias_columns(columns, &pivot.alias_columns)
394    }
395
396    fn get_pivot_unpivot_output_columns(&self, pivot: &crate::expressions::Pivot) -> Vec<String> {
397        let pre_columns = self.get_source_output_columns(&pivot.this);
398        if pre_columns.is_empty() || pre_columns.iter().any(|column| column == "*") {
399            return Vec::new();
400        }
401
402        let input_columns: HashSet<String> = pivot
403            .expressions
404            .iter()
405            .flat_map(expression_column_names)
406            .map(|column| normalize_column_name(&column, self.dialect))
407            .collect();
408        let mut columns: Vec<String> = pre_columns
409            .into_iter()
410            .filter(|column| !input_columns.contains(&normalize_column_name(column, self.dialect)))
411            .collect();
412
413        if let Some(Expression::UnpivotColumns(unpivot_columns)) = pivot.into.as_deref() {
414            if let Some(name) = expression_name(&unpivot_columns.this) {
415                columns.push(name);
416            }
417            for value_column in &unpivot_columns.expressions {
418                if let Some(name) = expression_name(value_column) {
419                    columns.push(name);
420                }
421            }
422        }
423
424        apply_alias_columns(columns, &pivot.alias_columns)
425    }
426
427    fn get_unpivot_output_columns(&self, unpivot: &crate::expressions::Unpivot) -> Vec<String> {
428        let pre_columns = self.get_source_output_columns(&unpivot.this);
429        if pre_columns.is_empty() || pre_columns.iter().any(|column| column == "*") {
430            return Vec::new();
431        }
432
433        let input_columns: HashSet<String> = unpivot
434            .columns
435            .iter()
436            .flat_map(expression_column_names)
437            .map(|column| normalize_column_name(&column, self.dialect))
438            .collect();
439        let mut columns: Vec<String> = pre_columns
440            .into_iter()
441            .filter(|column| !input_columns.contains(&normalize_column_name(column, self.dialect)))
442            .collect();
443        columns.push(unpivot.name_column.name.clone());
444        columns.push(unpivot.value_column.name.clone());
445        columns.extend(
446            unpivot
447                .extra_value_columns
448                .iter()
449                .map(|column| column.name.clone()),
450        );
451        apply_alias_columns(columns, &unpivot.alias_columns)
452    }
453
454    fn get_source_output_columns(&self, source: &Expression) -> Vec<String> {
455        match source {
456            Expression::Table(table) => {
457                if table.schema.is_none() && table.catalog.is_none() {
458                    if let Some(source) = self.scope.cte_sources.get(&table.name.name) {
459                        return self.extract_columns_from_source(source).unwrap_or_default();
460                    }
461                }
462
463                let table_name = qualified_table_name(table);
464                self.schema.column_names(&table_name).unwrap_or_default()
465            }
466            Expression::Subquery(subquery) => apply_alias_columns(
467                self.get_named_selects(&subquery.this),
468                &subquery.column_aliases,
469            ),
470            Expression::Values(values) => self.get_values_column_names(values),
471            Expression::Select(select) => self.get_select_column_names(select),
472            Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_) => self
473                .get_source_columns_from_set_op(source)
474                .unwrap_or_default(),
475            Expression::Alias(alias) if matches!(&alias.this, Expression::Unnest(_)) => {
476                alias_output_columns(alias)
477            }
478            Expression::Alias(alias) => {
479                let columns = self.get_source_output_columns(&alias.this);
480                apply_alias_columns(columns, &alias.column_aliases)
481            }
482            Expression::Unnest(unnest) => unnest_output_columns(unnest),
483            Expression::Lateral(lateral) => lateral_output_columns(lateral),
484            Expression::LateralView(lateral_view) => lateral_view_output_columns(lateral_view),
485            Expression::Cte(cte) => {
486                if cte.columns.is_empty() {
487                    self.get_named_selects(&cte.this)
488                } else {
489                    cte.columns
490                        .iter()
491                        .map(|column| column.name.clone())
492                        .collect()
493                }
494            }
495            Expression::Paren(paren) => self.get_source_output_columns(&paren.this),
496            _ => Vec::new(),
497        }
498    }
499
500    /// Get columns from a set operation (UNION, INTERSECT, EXCEPT)
501    pub fn get_source_columns_from_set_op(
502        &self,
503        expression: &Expression,
504    ) -> ResolverResult<Vec<String>> {
505        if matches!(
506            expression,
507            Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_)
508        ) {
509            match crate::set_operation::set_operation_layout(expression, self.dialect) {
510                Ok(Some(layout)) => {
511                    return Ok(layout
512                        .outputs
513                        .into_iter()
514                        .map(|output| output.identifier.name)
515                        .collect())
516                }
517                Ok(None) => {}
518                Err(error) if error.is_indeterminate() => {}
519                Err(error) => return Err(ResolverError::InvalidSetOperation(error.to_string())),
520            }
521        }
522
523        match expression {
524            Expression::Select(select) => Ok(self.get_select_column_names(select)),
525            Expression::Subquery(subquery) => {
526                if matches!(
527                    &subquery.this,
528                    Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_)
529                ) {
530                    self.get_source_columns_from_set_op(&subquery.this)
531                } else {
532                    Ok(self.get_named_selects(&subquery.this))
533                }
534            }
535            Expression::Alias(alias) => {
536                let columns = self.get_source_columns_from_set_op(&alias.this)?;
537                Ok(apply_alias_columns(columns, &alias.column_aliases))
538            }
539            Expression::Paren(paren) => self.get_source_columns_from_set_op(&paren.this),
540            Expression::Union(union) => {
541                // Standard UNION: columns come from the left side
542                self.get_source_columns_from_set_op(&union.left)
543            }
544            Expression::Intersect(intersect) => {
545                self.get_source_columns_from_set_op(&intersect.left)
546            }
547            Expression::Except(except) => self.get_source_columns_from_set_op(&except.left),
548            _ => Err(ResolverError::UnknownSetOperation(format!(
549                "{:?}",
550                expression
551            ))),
552        }
553    }
554
555    /// Get all source columns for all sources in the scope
556    fn get_all_source_columns(&mut self) -> HashMap<String, Vec<String>> {
557        let source_names: Vec<_> = self.scope.sources.keys().cloned().collect();
558
559        let mut result = HashMap::new();
560        for source_name in source_names {
561            if let Ok(columns) = self.get_source_columns(&source_name) {
562                result.insert(source_name, columns);
563            }
564        }
565        result
566    }
567
568    /// Get the table name for a column from the sources
569    fn get_table_name_from_sources(
570        &mut self,
571        column_name: &str,
572        source_columns: Option<&HashMap<String, Vec<String>>>,
573    ) -> Option<String> {
574        let normalized_column_name = normalize_column_name(column_name, self.dialect);
575        let unambiguous = match source_columns {
576            Some(cols) => self.compute_unambiguous_columns(cols),
577            None => {
578                if self.unambiguous_columns_cache.is_none() {
579                    let all_source_columns = self.get_all_source_columns();
580                    self.unambiguous_columns_cache =
581                        Some(self.compute_unambiguous_columns(&all_source_columns));
582                }
583                self.unambiguous_columns_cache
584                    .clone()
585                    .expect("cache populated above")
586            }
587        };
588
589        unambiguous.get(&normalized_column_name).cloned()
590    }
591
592    /// Compute unambiguous columns mapping
593    ///
594    /// A column is unambiguous if it appears in exactly one source.
595    fn compute_unambiguous_columns(
596        &self,
597        source_columns: &HashMap<String, Vec<String>>,
598    ) -> HashMap<String, String> {
599        if source_columns.is_empty() {
600            return HashMap::new();
601        }
602
603        let mut column_to_sources: HashMap<String, Vec<String>> = HashMap::new();
604
605        for (source_name, columns) in source_columns {
606            for column in columns {
607                column_to_sources
608                    .entry(normalize_column_name(column, self.dialect))
609                    .or_default()
610                    .push(source_name.clone());
611            }
612        }
613
614        // Keep only columns that appear in exactly one source
615        column_to_sources
616            .into_iter()
617            .filter(|(_, sources)| sources.len() == 1)
618            .map(|(column, sources)| (column, sources.into_iter().next().unwrap()))
619            .collect()
620    }
621
622    /// Check if a column is ambiguous (appears in multiple sources)
623    pub fn is_ambiguous(&mut self, column_name: &str) -> bool {
624        let normalized_column_name = normalize_column_name(column_name, self.dialect);
625        let all_source_columns = self.get_all_source_columns();
626        let sources_with_column: Vec<_> = all_source_columns
627            .iter()
628            .filter(|(_, columns)| {
629                columns.iter().any(|column| {
630                    normalize_column_name(column, self.dialect) == normalized_column_name
631                })
632            })
633            .map(|(name, _)| name.clone())
634            .collect();
635
636        sources_with_column.len() > 1
637    }
638
639    /// Get all sources that contain a given column
640    pub fn sources_for_column(&mut self, column_name: &str) -> Vec<String> {
641        let normalized_column_name = normalize_column_name(column_name, self.dialect);
642        let all_source_columns = self.get_all_source_columns();
643        all_source_columns
644            .iter()
645            .filter(|(_, columns)| {
646                columns.iter().any(|column| {
647                    normalize_column_name(column, self.dialect) == normalized_column_name
648                })
649            })
650            .map(|(name, _)| name.clone())
651            .collect()
652    }
653
654    /// Try to disambiguate a column based on join context
655    ///
656    /// In join conditions, a column can sometimes be disambiguated based on
657    /// which tables have been joined up to that point.
658    pub fn disambiguate_in_join_context(
659        &mut self,
660        column_name: &str,
661        available_sources: &[String],
662    ) -> Option<String> {
663        let normalized_column_name = normalize_column_name(column_name, self.dialect);
664        let mut matching_sources = Vec::new();
665
666        for source_name in available_sources {
667            if let Ok(columns) = self.get_source_columns(source_name) {
668                if columns.iter().any(|column| {
669                    normalize_column_name(column, self.dialect) == normalized_column_name
670                }) {
671                    matching_sources.push(source_name.clone());
672                }
673            }
674        }
675
676        if matching_sources.len() == 1 {
677            Some(matching_sources.remove(0))
678        } else {
679            None
680        }
681    }
682}
683
684fn normalize_column_name(name: &str, dialect: Option<DialectType>) -> String {
685    normalize_name(name, dialect, false, true)
686}
687
688fn apply_alias_columns(mut columns: Vec<String>, alias_columns: &[Identifier]) -> Vec<String> {
689    for (idx, alias) in alias_columns.iter().enumerate() {
690        if let Some(column) = columns.get_mut(idx) {
691            *column = alias.name.clone();
692        }
693    }
694    columns
695}
696
697fn unnest_output_columns(unnest: &crate::expressions::UnnestFunc) -> Vec<String> {
698    unnest
699        .alias
700        .iter()
701        .map(|alias| alias.name.clone())
702        .chain(unnest.offset_alias.iter().map(|alias| alias.name.clone()))
703        .collect()
704}
705
706fn alias_output_columns(alias: &crate::expressions::Alias) -> Vec<String> {
707    if alias.column_aliases.is_empty() {
708        vec![alias.alias.name.clone()]
709    } else {
710        alias
711            .column_aliases
712            .iter()
713            .map(|column| column.name.clone())
714            .collect()
715    }
716}
717
718fn lateral_output_columns(lateral: &crate::expressions::Lateral) -> Vec<String> {
719    if lateral.column_aliases.is_empty() {
720        default_virtual_output_columns(&lateral.this)
721    } else {
722        lateral.column_aliases.clone()
723    }
724}
725
726fn lateral_view_output_columns(lateral_view: &crate::expressions::LateralView) -> Vec<String> {
727    lateral_view
728        .column_aliases
729        .iter()
730        .map(|column| column.name.clone())
731        .collect()
732}
733
734fn default_virtual_output_columns(expression: &Expression) -> Vec<String> {
735    match expression {
736        Expression::Unnest(unnest) => unnest_output_columns(unnest),
737        Expression::Alias(alias) if matches!(&alias.this, Expression::Unnest(_)) => {
738            alias_output_columns(alias)
739        }
740        Expression::Function(function) if function.name.eq_ignore_ascii_case("FLATTEN") => {
741            ["seq", "key", "path", "index", "value", "this"]
742                .into_iter()
743                .map(String::from)
744                .collect()
745        }
746        _ => Vec::new(),
747    }
748}
749
750fn pivot_excluded_source_columns(
751    pivot: &crate::expressions::Pivot,
752    dialect: Option<DialectType>,
753) -> HashSet<String> {
754    pivot
755        .fields
756        .iter()
757        .chain(pivot.expressions.iter())
758        .chain(pivot.using.iter())
759        .flat_map(expression_column_names)
760        .map(|column| normalize_column_name(&column, dialect))
761        .collect()
762}
763
764fn pivot_generated_output_columns(
765    pivot: &crate::expressions::Pivot,
766    _dialect: Option<DialectType>,
767) -> Vec<String> {
768    let fields = pivot_field_output_names(pivot);
769    let aggregations = if pivot.using.is_empty() {
770        &pivot.expressions
771    } else {
772        &pivot.using
773    };
774
775    if fields.is_empty() || aggregations.is_empty() {
776        return Vec::new();
777    }
778
779    let needs_suffix = aggregations.len() > 1;
780    let mut outputs = Vec::new();
781    for field in fields {
782        for aggregation in aggregations {
783            if let Some(suffix) = pivot_aggregation_output_suffix(aggregation, needs_suffix) {
784                outputs.push(format!("{field}_{suffix}"));
785            } else {
786                outputs.push(field.clone());
787            }
788        }
789    }
790    outputs
791}
792
793fn pivot_field_output_names(pivot: &crate::expressions::Pivot) -> Vec<String> {
794    pivot
795        .fields
796        .iter()
797        .filter_map(|field| match field {
798            Expression::In(in_expr) => Some(
799                in_expr
800                    .expressions
801                    .iter()
802                    .filter_map(expression_name)
803                    .collect::<Vec<_>>(),
804            ),
805            _ => None,
806        })
807        .flatten()
808        .collect()
809}
810
811fn pivot_aggregation_output_suffix(expr: &Expression, needs_suffix: bool) -> Option<String> {
812    match expr {
813        Expression::Alias(alias) => Some(alias.alias.name.clone()),
814        _ if needs_suffix => pivot_generated_aggregation_suffix(expr),
815        _ => None,
816    }
817}
818
819#[cfg(feature = "generate")]
820fn pivot_generated_aggregation_suffix(expr: &Expression) -> Option<String> {
821    Generator::sql(expr).ok().map(|sql| sql.to_lowercase())
822}
823
824#[cfg(not(feature = "generate"))]
825fn pivot_generated_aggregation_suffix(expr: &Expression) -> Option<String> {
826    expression_name(expr).or_else(|| Some(expr.variant_name().to_string()))
827}
828
829fn expression_name(expr: &Expression) -> Option<String> {
830    match expr {
831        Expression::PivotAlias(alias) => expression_name(&alias.alias),
832        Expression::Alias(alias) => Some(alias.alias.name.clone()),
833        Expression::Identifier(identifier) => Some(identifier.name.clone()),
834        Expression::Column(column) => Some(column.name.name.clone()),
835        Expression::Literal(literal) => Some(literal.value_str().to_string()),
836        Expression::Var(var) => Some(var.this.clone()),
837        Expression::Tuple(tuple) => tuple.expressions.first().and_then(expression_name),
838        _ => None,
839    }
840}
841
842fn expression_column_names(expr: &Expression) -> Vec<String> {
843    expr.find_all(|node| matches!(node, Expression::Column(_)))
844        .into_iter()
845        .filter_map(|node| match node {
846            Expression::Column(column) => Some(column.name.name.clone()),
847            _ => None,
848        })
849        .collect()
850}
851
852/// Resolve a column to its source table.
853///
854/// This is a convenience function that creates a Resolver and calls get_table.
855pub fn resolve_column(
856    scope: &Scope,
857    schema: &dyn Schema,
858    column_name: &str,
859    infer_schema: bool,
860) -> Option<String> {
861    let mut resolver = Resolver::new(scope, schema, infer_schema);
862    resolver.get_table(column_name)
863}
864
865/// Check if a column is ambiguous in the given scope.
866pub fn is_column_ambiguous(scope: &Scope, schema: &dyn Schema, column_name: &str) -> bool {
867    let mut resolver = Resolver::new(scope, schema, true);
868    resolver.is_ambiguous(column_name)
869}
870
871/// Build the fully qualified table name (catalog.schema.table) from a TableRef.
872fn qualified_table_name(table: &TableRef) -> String {
873    let mut parts = Vec::new();
874    if let Some(catalog) = &table.catalog {
875        parts.push(catalog.name.clone());
876    }
877    if let Some(schema) = &table.schema {
878        parts.push(schema.name.clone());
879    }
880    parts.push(table.name.name.clone());
881    parts.join(".")
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887    use crate::dialects::Dialect;
888    use crate::expressions::DataType;
889    use crate::parser::Parser;
890    use crate::schema::MappingSchema;
891    use crate::scope::build_scope;
892
893    fn create_test_schema() -> MappingSchema {
894        let mut schema = MappingSchema::new();
895        // Add tables with columns
896        schema
897            .add_table(
898                "users",
899                &[
900                    (
901                        "id".to_string(),
902                        DataType::Int {
903                            length: None,
904                            integer_spelling: false,
905                        },
906                    ),
907                    ("name".to_string(), DataType::Text),
908                    ("email".to_string(), DataType::Text),
909                ],
910                None,
911            )
912            .unwrap();
913        schema
914            .add_table(
915                "orders",
916                &[
917                    (
918                        "id".to_string(),
919                        DataType::Int {
920                            length: None,
921                            integer_spelling: false,
922                        },
923                    ),
924                    (
925                        "user_id".to_string(),
926                        DataType::Int {
927                            length: None,
928                            integer_spelling: false,
929                        },
930                    ),
931                    (
932                        "amount".to_string(),
933                        DataType::Double {
934                            precision: None,
935                            scale: None,
936                        },
937                    ),
938                ],
939                None,
940            )
941            .unwrap();
942        schema
943    }
944
945    #[test]
946    fn test_resolver_basic() {
947        let ast = Parser::parse_sql("SELECT id, name FROM users").expect("Failed to parse");
948        let scope = build_scope(&ast[0]);
949        let schema = create_test_schema();
950        let mut resolver = Resolver::new(&scope, &schema, true);
951
952        // 'name' should resolve to 'users' since it's the only source
953        let table = resolver.get_table("name");
954        assert_eq!(table, Some("users".to_string()));
955    }
956
957    #[test]
958    fn test_resolver_ambiguous_column() {
959        let ast =
960            Parser::parse_sql("SELECT id FROM users JOIN orders ON users.id = orders.user_id")
961                .expect("Failed to parse");
962        let scope = build_scope(&ast[0]);
963        let schema = create_test_schema();
964        let mut resolver = Resolver::new(&scope, &schema, true);
965
966        // 'id' appears in both tables, so it's ambiguous
967        assert!(resolver.is_ambiguous("id"));
968
969        // 'name' only appears in users
970        assert!(!resolver.is_ambiguous("name"));
971
972        // 'amount' only appears in orders
973        assert!(!resolver.is_ambiguous("amount"));
974    }
975
976    #[test]
977    fn test_resolver_unambiguous_column() {
978        let ast = Parser::parse_sql(
979            "SELECT name, amount FROM users JOIN orders ON users.id = orders.user_id",
980        )
981        .expect("Failed to parse");
982        let scope = build_scope(&ast[0]);
983        let schema = create_test_schema();
984        let mut resolver = Resolver::new(&scope, &schema, true);
985
986        // 'name' should resolve to 'users'
987        let table = resolver.get_table("name");
988        assert_eq!(table, Some("users".to_string()));
989
990        // 'amount' should resolve to 'orders'
991        let table = resolver.get_table("amount");
992        assert_eq!(table, Some("orders".to_string()));
993    }
994
995    #[test]
996    fn test_resolver_with_alias() {
997        let ast = Parser::parse_sql("SELECT u.id FROM users AS u").expect("Failed to parse");
998        let scope = build_scope(&ast[0]);
999        let schema = create_test_schema();
1000        let _resolver = Resolver::new(&scope, &schema, true);
1001
1002        // Source should be indexed by alias 'u'
1003        assert!(scope.sources.contains_key("u"));
1004    }
1005
1006    #[test]
1007    fn test_sources_for_column() {
1008        let ast = Parser::parse_sql("SELECT * FROM users JOIN orders ON users.id = orders.user_id")
1009            .expect("Failed to parse");
1010        let scope = build_scope(&ast[0]);
1011        let schema = create_test_schema();
1012        let mut resolver = Resolver::new(&scope, &schema, true);
1013
1014        // 'id' should be in both users and orders
1015        let sources = resolver.sources_for_column("id");
1016        assert!(sources.contains(&"users".to_string()));
1017        assert!(sources.contains(&"orders".to_string()));
1018
1019        // 'email' should only be in users
1020        let sources = resolver.sources_for_column("email");
1021        assert_eq!(sources, vec!["users".to_string()]);
1022    }
1023
1024    #[test]
1025    fn test_all_columns() {
1026        let ast = Parser::parse_sql("SELECT * FROM users").expect("Failed to parse");
1027        let scope = build_scope(&ast[0]);
1028        let schema = create_test_schema();
1029        let mut resolver = Resolver::new(&scope, &schema, true);
1030
1031        let all = resolver.all_columns();
1032        assert!(all.contains("id"));
1033        assert!(all.contains("name"));
1034        assert!(all.contains("email"));
1035    }
1036
1037    #[test]
1038    fn test_resolver_cte_projected_alias_column() {
1039        let ast = Parser::parse_sql(
1040            "WITH my_cte AS (SELECT id AS emp_id FROM users) SELECT emp_id FROM my_cte",
1041        )
1042        .expect("Failed to parse");
1043        let scope = build_scope(&ast[0]);
1044        let schema = create_test_schema();
1045        let mut resolver = Resolver::new(&scope, &schema, true);
1046
1047        let table = resolver.get_table("emp_id");
1048        assert_eq!(table, Some("my_cte".to_string()));
1049    }
1050
1051    #[test]
1052    fn test_resolver_uses_name_aligned_set_operation_outputs() {
1053        for dialect_type in [DialectType::DuckDB, DialectType::Snowflake] {
1054            let expr = Dialect::get(dialect_type)
1055                .parse(
1056                    "SELECT combined.right_value FROM (\
1057                     SELECT 1 AS left_value UNION ALL BY NAME \
1058                     SELECT 2 AS right_value\
1059                     ) AS combined",
1060                )
1061                .expect("parse")
1062                .remove(0);
1063            let scope = build_scope(&expr);
1064            let schema = MappingSchema::with_dialect(dialect_type);
1065            let mut resolver = Resolver::new(&scope, &schema, true);
1066
1067            assert_eq!(
1068                resolver.get_table("right_value"),
1069                Some("combined".to_string()),
1070                "right-only output was not exposed for {dialect_type:?}"
1071            );
1072        }
1073
1074        let dialect_type = DialectType::BigQuery;
1075        let expr = Dialect::get(dialect_type)
1076            .parse(
1077                "SELECT combined.c FROM (\
1078                 SELECT 1 AS a, 2 AS b FULL OUTER UNION ALL BY NAME \
1079                 SELECT 3 AS b, 4 AS c\
1080                 ) AS combined",
1081            )
1082            .expect("parse")
1083            .remove(0);
1084        let scope = build_scope(&expr);
1085        let schema = MappingSchema::with_dialect(dialect_type);
1086        let mut resolver = Resolver::new(&scope, &schema, true);
1087        assert_eq!(resolver.get_table("c"), Some("combined".to_string()));
1088    }
1089
1090    #[test]
1091    fn test_resolve_column_helper() {
1092        let ast = Parser::parse_sql("SELECT name FROM users").expect("Failed to parse");
1093        let scope = build_scope(&ast[0]);
1094        let schema = create_test_schema();
1095
1096        let table = resolve_column(&scope, &schema, "name", true);
1097        assert_eq!(table, Some("users".to_string()));
1098    }
1099
1100    #[test]
1101    fn test_resolver_bigquery_mixed_case_column_names() {
1102        let dialect = Dialect::get(DialectType::BigQuery);
1103        let expr = dialect
1104            .parse("SELECT Name AS name FROM teams")
1105            .unwrap()
1106            .into_iter()
1107            .next()
1108            .expect("expected one expression");
1109        let scope = build_scope(&expr);
1110
1111        let mut schema = MappingSchema::with_dialect(DialectType::BigQuery);
1112        schema
1113            .add_table(
1114                "teams",
1115                &[("Name".into(), DataType::String { length: None })],
1116                None,
1117            )
1118            .expect("schema setup");
1119
1120        let mut resolver = Resolver::new(&scope, &schema, true);
1121        let table = resolver.get_table("Name");
1122        assert_eq!(table, Some("teams".to_string()));
1123
1124        let table = resolver.get_table("name");
1125        assert_eq!(table, Some("teams".to_string()));
1126    }
1127}