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