Skip to main content

polyglot_sql/optimizer/
qualify_columns.rs

1//! Column Qualification Module
2//!
3//! This module provides functionality for qualifying column references in SQL queries,
4//! adding table qualifiers to column names and expanding star expressions.
5//!
6//! Ported from sqlglot's optimizer/qualify_columns.py
7
8use crate::dialects::transform_recursive;
9use crate::dialects::DialectType;
10use crate::expressions::{
11    Alias, BinaryOp, Column, DotAccess, Expression, Identifier, Join, JoinKind, LateralView,
12    Literal, Over, Paren, Select, TableRef, VarArgFunc, With,
13};
14use crate::resolver::{Resolver, ResolverError};
15use crate::schema::{normalize_name, Schema};
16use crate::scope::{build_scope, traverse_scope, Scope};
17use std::cell::RefCell;
18use std::collections::{HashMap, HashSet};
19use thiserror::Error;
20
21/// Errors that can occur during column qualification
22#[derive(Debug, Error, Clone)]
23pub enum QualifyColumnsError {
24    #[error("Unknown table: {0}")]
25    UnknownTable(String),
26
27    #[error("Unknown column: {0}")]
28    UnknownColumn(String),
29
30    #[error("Ambiguous column: {0}")]
31    AmbiguousColumn(String),
32
33    #[error("Cannot automatically join: {0}")]
34    CannotAutoJoin(String),
35
36    #[error("Unknown output column: {0}")]
37    UnknownOutputColumn(String),
38
39    #[error("Column could not be resolved: {column}{for_table}")]
40    ColumnNotResolved { column: String, for_table: String },
41
42    #[error("Resolver error: {0}")]
43    ResolverError(#[from] ResolverError),
44}
45
46/// Result type for column qualification operations
47pub type QualifyColumnsResult<T> = Result<T, QualifyColumnsError>;
48
49/// Options for column qualification
50#[derive(Debug, Clone, Default)]
51pub struct QualifyColumnsOptions {
52    /// Whether to expand references to aliases
53    pub expand_alias_refs: bool,
54    /// Whether to expand star expressions to explicit columns
55    pub expand_stars: bool,
56    /// Whether to infer schema if not provided
57    pub infer_schema: Option<bool>,
58    /// Whether to allow partial qualification
59    pub allow_partial_qualification: bool,
60    /// The dialect for dialect-specific behavior
61    pub dialect: Option<DialectType>,
62}
63
64impl QualifyColumnsOptions {
65    /// Create new options with defaults
66    pub fn new() -> Self {
67        Self {
68            expand_alias_refs: true,
69            expand_stars: true,
70            infer_schema: None,
71            allow_partial_qualification: false,
72            dialect: None,
73        }
74    }
75
76    /// Set whether to expand alias refs
77    pub fn with_expand_alias_refs(mut self, expand: bool) -> Self {
78        self.expand_alias_refs = expand;
79        self
80    }
81
82    /// Set whether to expand stars
83    pub fn with_expand_stars(mut self, expand: bool) -> Self {
84        self.expand_stars = expand;
85        self
86    }
87
88    /// Set the dialect
89    pub fn with_dialect(mut self, dialect: DialectType) -> Self {
90        self.dialect = Some(dialect);
91        self
92    }
93
94    /// Set whether to allow partial qualification
95    pub fn with_allow_partial(mut self, allow: bool) -> Self {
96        self.allow_partial_qualification = allow;
97        self
98    }
99}
100
101/// Rewrite SQL AST to have fully qualified columns.
102///
103/// # Example
104/// ```ignore
105/// // SELECT col FROM tbl => SELECT tbl.col AS col FROM tbl
106/// ```
107///
108/// # Arguments
109/// * `expression` - Expression to qualify
110/// * `schema` - Database schema for column lookup
111/// * `options` - Qualification options
112///
113/// # Returns
114/// The qualified expression
115pub fn qualify_columns(
116    expression: Expression,
117    schema: &dyn Schema,
118    options: &QualifyColumnsOptions,
119) -> QualifyColumnsResult<Expression> {
120    let infer_schema = options.infer_schema.unwrap_or(schema.is_empty());
121    let dialect = options.dialect.or_else(|| schema.dialect());
122    let first_error: RefCell<Option<QualifyColumnsError>> = RefCell::new(None);
123
124    let transformed = transform_recursive(expression, &|node| {
125        if first_error.borrow().is_some() {
126            return Ok(node);
127        }
128
129        match node {
130            Expression::Select(mut select) => {
131                if let Some(with) = &mut select.with {
132                    pushdown_cte_alias_columns_with(with);
133                }
134
135                let scope_expr = Expression::Select(select.clone());
136                let scope = build_scope(&scope_expr);
137                let mut resolver = Resolver::new(&scope, schema, infer_schema);
138
139                // 1. Expand USING → ON before column qualification
140                let column_tables = if first_error.borrow().is_none() {
141                    match expand_using(&mut select, &scope, &mut resolver) {
142                        Ok(ct) => ct,
143                        Err(err) => {
144                            *first_error.borrow_mut() = Some(err);
145                            HashMap::new()
146                        }
147                    }
148                } else {
149                    HashMap::new()
150                };
151
152                // 2. Expand alias references before qualification so same-select
153                // aliases do not get treated as unresolved physical columns.
154                if first_error.borrow().is_none() && options.expand_alias_refs {
155                    if let Err(err) = expand_alias_refs(&mut select, &mut resolver, dialect) {
156                        *first_error.borrow_mut() = Some(err);
157                    }
158                }
159
160                // 3. A parsed `column.field` is indistinguishable from `table.column`.
161                // Normalize an apparent qualifier that resolves as a column before
162                // regular qualification rejects it as an unknown table.
163                if first_error.borrow().is_none() {
164                    if let Err(err) =
165                        normalize_dotted_columns_in_scope(&mut select, &scope, &mut resolver)
166                    {
167                        *first_error.borrow_mut() = Some(err);
168                    }
169                }
170
171                // 4. Qualify columns (add table qualifiers)
172                if first_error.borrow().is_none() {
173                    if let Err(err) = qualify_columns_in_scope(
174                        &mut select,
175                        &scope,
176                        &mut resolver,
177                        options.allow_partial_qualification,
178                    ) {
179                        *first_error.borrow_mut() = Some(err);
180                    }
181                }
182
183                // 5. Expand star expressions (with USING deduplication)
184                if first_error.borrow().is_none() && options.expand_stars {
185                    if let Err(err) =
186                        expand_stars(&mut select, &scope, &mut resolver, &column_tables)
187                    {
188                        *first_error.borrow_mut() = Some(err);
189                    }
190                }
191
192                // 6. Qualify outputs
193                if first_error.borrow().is_none() {
194                    if let Err(err) = qualify_outputs_select(&mut select) {
195                        *first_error.borrow_mut() = Some(err);
196                    }
197                }
198
199                // 7. Expand GROUP BY positional refs
200                if first_error.borrow().is_none() {
201                    if let Err(err) = expand_group_by(&mut select, dialect) {
202                        *first_error.borrow_mut() = Some(err);
203                    }
204                }
205
206                Ok(Expression::Select(select))
207            }
208            _ => Ok(node),
209        }
210    })
211    .map_err(|err| QualifyColumnsError::CannotAutoJoin(err.to_string()))?;
212
213    if let Some(err) = first_error.into_inner() {
214        return Err(err);
215    }
216
217    Ok(transformed)
218}
219
220/// Validate that all columns in an expression are qualified.
221///
222/// # Returns
223/// The expression if valid, or an error if unqualified columns exist.
224pub fn validate_qualify_columns(expression: &Expression) -> QualifyColumnsResult<()> {
225    let mut all_unqualified = Vec::new();
226
227    for scope in traverse_scope(expression) {
228        if let Expression::Select(_) = &scope.expression {
229            // Get unqualified columns from this scope
230            let unqualified = get_unqualified_columns(&scope);
231
232            // Check for external columns that couldn't be resolved
233            let external = get_external_columns(&scope);
234            if !external.is_empty() && !is_correlated_subquery(&scope) {
235                let first = &external[0];
236                let for_table = if first.table.is_some() {
237                    format!(" for table: '{}'", first.table.as_ref().unwrap())
238                } else {
239                    String::new()
240                };
241                return Err(QualifyColumnsError::ColumnNotResolved {
242                    column: first.name.clone(),
243                    for_table,
244                });
245            }
246
247            all_unqualified.extend(unqualified);
248        }
249    }
250
251    if !all_unqualified.is_empty() {
252        let first = &all_unqualified[0];
253        return Err(QualifyColumnsError::AmbiguousColumn(first.name.clone()));
254    }
255
256    Ok(())
257}
258
259/// Get the alias or table name from a table expression in FROM/JOIN context.
260fn get_source_name(expr: &Expression) -> Option<String> {
261    match expr {
262        Expression::Table(t) => Some(
263            t.alias
264                .as_ref()
265                .map(|a| a.name.clone())
266                .unwrap_or_else(|| t.name.name.clone()),
267        ),
268        Expression::Subquery(sq) => sq.alias.as_ref().map(|a| a.name.clone()),
269        Expression::Pivot(pivot) => Some(pivot_source_name(
270            &pivot.this,
271            pivot.alias.as_ref().map(|alias| alias.name.as_str()),
272        )),
273        Expression::Unpivot(unpivot) => Some(pivot_source_name(
274            &unpivot.this,
275            unpivot.alias.as_ref().map(|alias| alias.name.as_str()),
276        )),
277        _ => None,
278    }
279}
280
281fn pivot_source_name(source: &Expression, explicit_alias: Option<&str>) -> String {
282    if let Some(alias) = explicit_alias {
283        return alias.to_string();
284    }
285
286    match source {
287        Expression::Table(table) => table
288            .alias
289            .as_ref()
290            .map(|alias| alias.name.clone())
291            .unwrap_or_else(|| table.name.name.clone()),
292        Expression::Subquery(subquery) => subquery
293            .alias
294            .as_ref()
295            .map(|alias| alias.name.clone())
296            .unwrap_or_else(|| "_0".to_string()),
297        Expression::Paren(paren) => pivot_source_name(&paren.this, explicit_alias),
298        _ => "_0".to_string(),
299    }
300}
301
302/// Get ordered source names from a SELECT's FROM + JOIN clauses.
303/// FROM tables come first, then JOIN tables in declaration order.
304fn get_ordered_source_names(select: &Select) -> Vec<String> {
305    let mut ordered = Vec::new();
306    if let Some(from) = &select.from {
307        for expr in &from.expressions {
308            if let Some(name) = get_source_name(expr) {
309                ordered.push(name);
310            }
311        }
312    }
313    for join in &select.joins {
314        if is_semi_or_anti_join_kind(join.kind) {
315            continue;
316        }
317        if let Some(name) = get_source_name(&join.this) {
318            ordered.push(name);
319        }
320    }
321    ordered
322}
323
324fn is_semi_or_anti_join_kind(kind: JoinKind) -> bool {
325    matches!(
326        kind,
327        JoinKind::Semi
328            | JoinKind::Anti
329            | JoinKind::LeftSemi
330            | JoinKind::LeftAnti
331            | JoinKind::RightSemi
332            | JoinKind::RightAnti
333    )
334}
335
336/// Create a COALESCE expression over qualified columns from the given tables.
337fn make_coalesce(column_name: &str, tables: &[String]) -> Expression {
338    let args: Vec<Expression> = tables
339        .iter()
340        .map(|t| Expression::qualified_column(t.as_str(), column_name))
341        .collect();
342    Expression::Coalesce(Box::new(VarArgFunc {
343        expressions: args,
344        original_name: None,
345        inferred_type: None,
346    }))
347}
348
349/// Expand JOIN USING clauses into ON conditions and track which columns
350/// participate in USING joins for later COALESCE rewriting.
351///
352/// Returns a mapping from column name → ordered list of table names that
353/// participate in USING for that column.
354fn expand_using(
355    select: &mut Select,
356    _scope: &Scope,
357    resolver: &mut Resolver,
358) -> QualifyColumnsResult<HashMap<String, Vec<String>>> {
359    // columns: normalized column name → first source that owns it
360    // (first-seen-wins)
361    let mut columns: HashMap<String, String> = HashMap::new();
362    // Preserve the first-seen column order for deterministic NATURAL JOIN
363    // expansion. HashMap iteration order must not affect the generated USING list.
364    let mut column_order: Vec<String> = Vec::new();
365
366    // column_tables: normalized column name → ordered list of tables that
367    // participate in USING
368    let mut column_tables: HashMap<String, Vec<String>> = HashMap::new();
369
370    // Get non-join source names from FROM clause
371    let join_names: HashSet<String> = select
372        .joins
373        .iter()
374        .filter_map(|j| get_source_name(&j.this))
375        .collect();
376
377    let all_ordered = get_ordered_source_names(select);
378    let mut ordered: Vec<String> = all_ordered
379        .iter()
380        .filter(|name| !join_names.contains(name.as_str()))
381        .cloned()
382        .collect();
383    let mut accumulated_schema_known = true;
384
385    if join_names.is_empty() {
386        return Ok(column_tables);
387    }
388
389    // Helper closure to update columns map from a source
390    fn update_source_columns(
391        source_name: &str,
392        columns: &mut HashMap<String, String>,
393        column_order: &mut Vec<String>,
394        resolver: &mut Resolver,
395    ) -> bool {
396        let Ok(source_cols) = resolver.get_source_columns(source_name) else {
397            return false;
398        };
399        let schema_known = !source_cols.is_empty()
400            && !source_cols
401                .iter()
402                .any(|column| column == "*" || column.is_empty());
403        for col_name in source_cols {
404            let normalized = normalize_column_name(&col_name, resolver.dialect);
405            if let std::collections::hash_map::Entry::Vacant(entry) = columns.entry(normalized) {
406                entry.insert(source_name.to_string());
407                column_order.push(col_name);
408            }
409        }
410        schema_known
411    }
412
413    // Pre-populate columns from FROM (base) sources
414    for source_name in &ordered {
415        accumulated_schema_known &=
416            update_source_columns(source_name, &mut columns, &mut column_order, resolver);
417    }
418
419    for i in 0..select.joins.len() {
420        // Get source_table (most recently seen non-join table)
421        let source_table = ordered.last().cloned().unwrap_or_default();
422        if !source_table.is_empty() {
423            accumulated_schema_known &=
424                update_source_columns(&source_table, &mut columns, &mut column_order, resolver);
425        }
426
427        // Get join_table name and append to ordered
428        let join_table = get_source_name(&select.joins[i].this).unwrap_or_default();
429        ordered.push(join_table.clone());
430
431        let join_columns: Vec<String> =
432            resolver.get_source_columns(&join_table).unwrap_or_default();
433        let star = normalize_column_name("*", resolver.dialect);
434        let right_schema_known = !join_columns.is_empty()
435            && !join_columns
436                .iter()
437                .any(|column| normalize_column_name(column, resolver.dialect) == star);
438
439        // NATURAL JOIN is an implicit USING join over every column common to
440        // the accumulated left side and the current right side. Only expand
441        // when both schemas are known; otherwise preserve NATURAL rather than
442        // guessing join keys.
443        let join_kind = select.joins[i].kind;
444        if select.joins[i].using.is_empty()
445            && matches!(
446                join_kind,
447                JoinKind::Natural
448                    | JoinKind::NaturalLeft
449                    | JoinKind::NaturalRight
450                    | JoinKind::NaturalFull
451            )
452        {
453            let left_schema_known =
454                accumulated_schema_known && !columns.is_empty() && !columns.contains_key(&star);
455
456            if left_schema_known && right_schema_known {
457                let right_columns: HashSet<String> = join_columns
458                    .iter()
459                    .map(|column| normalize_column_name(column, resolver.dialect))
460                    .collect();
461                let implicit_using: Vec<Identifier> = column_order
462                    .iter()
463                    .filter(|column| {
464                        right_columns.contains(&normalize_column_name(column, resolver.dialect))
465                    })
466                    .map(|column| Identifier::new(column))
467                    .collect();
468
469                if !implicit_using.is_empty() {
470                    select.joins[i].using = implicit_using;
471                    select.joins[i].kind = match join_kind {
472                        JoinKind::Natural => JoinKind::Inner,
473                        JoinKind::NaturalLeft => JoinKind::Left,
474                        JoinKind::NaturalRight => JoinKind::Right,
475                        JoinKind::NaturalFull => JoinKind::Full,
476                        _ => unreachable!("checked NATURAL join kind above"),
477                    };
478                }
479            }
480        }
481
482        // Preserve NATURAL joins with unknown schemas or no common columns,
483        // and skip ordinary joins without a USING clause.
484        if select.joins[i].using.is_empty() {
485            accumulated_schema_known &= right_schema_known;
486            continue;
487        }
488
489        let using_identifiers: Vec<String> = select.joins[i]
490            .using
491            .iter()
492            .map(|id| id.name.clone())
493            .collect();
494
495        let using_count = using_identifiers.len();
496        let is_semi_or_anti = matches!(
497            select.joins[i].kind,
498            crate::expressions::JoinKind::Semi
499                | crate::expressions::JoinKind::Anti
500                | crate::expressions::JoinKind::LeftSemi
501                | crate::expressions::JoinKind::LeftAnti
502                | crate::expressions::JoinKind::RightSemi
503                | crate::expressions::JoinKind::RightAnti
504        );
505
506        let mut conditions: Vec<Expression> = Vec::new();
507
508        for identifier in &using_identifiers {
509            let normalized_identifier = normalize_column_name(identifier, resolver.dialect);
510            let table = columns
511                .get(&normalized_identifier)
512                .cloned()
513                .unwrap_or_else(|| source_table.clone());
514
515            // Build LHS of the equality
516            let lhs = if i == 0 || using_count == 1 {
517                // Simple qualified column for first join or single USING column
518                Expression::qualified_column(table.as_str(), identifier.as_str())
519            } else {
520                // For subsequent joins with multiple USING columns,
521                // COALESCE over all previous sources that have this column
522                let coalesce_cols: Vec<String> = ordered[..ordered.len() - 1]
523                    .iter()
524                    .filter(|t| {
525                        resolver
526                            .get_source_columns(t)
527                            .unwrap_or_default()
528                            .iter()
529                            .any(|column| {
530                                normalize_column_name(column, resolver.dialect)
531                                    == normalized_identifier
532                            })
533                    })
534                    .cloned()
535                    .collect();
536
537                if coalesce_cols.len() > 1 {
538                    make_coalesce(identifier, &coalesce_cols)
539                } else {
540                    Expression::qualified_column(table.as_str(), identifier.as_str())
541                }
542            };
543
544            // Build RHS: qualified column from join table
545            let rhs = Expression::qualified_column(join_table.as_str(), identifier.as_str());
546
547            conditions.push(Expression::Eq(Box::new(BinaryOp::new(lhs, rhs))));
548
549            // Track tables for COALESCE rewriting (skip for semi/anti joins)
550            if !is_semi_or_anti {
551                let tables = column_tables
552                    .entry(normalized_identifier)
553                    .or_insert_with(Vec::new);
554                if !tables.contains(&table) {
555                    tables.push(table.clone());
556                }
557                if !tables.contains(&join_table) {
558                    tables.push(join_table.clone());
559                }
560            }
561        }
562
563        // Combine conditions with AND (left fold)
564        let on_condition = conditions
565            .into_iter()
566            .reduce(|acc, cond| Expression::And(Box::new(BinaryOp::new(acc, cond))))
567            .expect("at least one USING column");
568
569        // Set ON condition and clear USING
570        select.joins[i].on = Some(on_condition);
571        select.joins[i].using = vec![];
572        accumulated_schema_known &= right_schema_known;
573    }
574
575    // Phase 2: Rewrite unqualified USING column references to COALESCE
576    if !column_tables.is_empty() {
577        // Rewrite select.expressions (projections)
578        let mut new_expressions = Vec::with_capacity(select.expressions.len());
579        for expr in &select.expressions {
580            match expr {
581                Expression::Column(col) if col.table.is_none() => {
582                    let normalized = normalize_column_name(&col.name.name, resolver.dialect);
583                    let Some(tables) = column_tables.get(&normalized) else {
584                        new_expressions.push(expr.clone());
585                        continue;
586                    };
587                    let coalesce = make_coalesce(&col.name.name, tables);
588                    // Wrap in alias to preserve column name in projections
589                    new_expressions.push(Expression::Alias(Box::new(Alias {
590                        this: coalesce,
591                        alias: col.name.clone(),
592                        column_aliases: vec![],
593                        alias_explicit_as: false,
594                        alias_keyword: None,
595                        pre_alias_comments: vec![],
596                        trailing_comments: vec![],
597                        inferred_type: None,
598                    })));
599                }
600                _ => {
601                    let mut rewritten = expr.clone();
602                    rewrite_using_columns_in_expression(
603                        &mut rewritten,
604                        &column_tables,
605                        resolver.dialect,
606                    );
607                    new_expressions.push(rewritten);
608                }
609            }
610        }
611        select.expressions = new_expressions;
612
613        // Rewrite WHERE
614        if let Some(where_clause) = &mut select.where_clause {
615            rewrite_using_columns_in_expression(
616                &mut where_clause.this,
617                &column_tables,
618                resolver.dialect,
619            );
620        }
621
622        // Rewrite GROUP BY
623        if let Some(group_by) = &mut select.group_by {
624            for expr in &mut group_by.expressions {
625                rewrite_using_columns_in_expression(expr, &column_tables, resolver.dialect);
626            }
627        }
628
629        // Rewrite HAVING
630        if let Some(having) = &mut select.having {
631            rewrite_using_columns_in_expression(&mut having.this, &column_tables, resolver.dialect);
632        }
633
634        // Rewrite QUALIFY
635        if let Some(qualify) = &mut select.qualify {
636            rewrite_using_columns_in_expression(
637                &mut qualify.this,
638                &column_tables,
639                resolver.dialect,
640            );
641        }
642
643        // Rewrite ORDER BY
644        if let Some(order_by) = &mut select.order_by {
645            for ordered in &mut order_by.expressions {
646                rewrite_using_columns_in_expression(
647                    &mut ordered.this,
648                    &column_tables,
649                    resolver.dialect,
650                );
651            }
652        }
653    }
654
655    Ok(column_tables)
656}
657
658/// Recursively replace unqualified USING column references with COALESCE.
659fn rewrite_using_columns_in_expression(
660    expr: &mut Expression,
661    column_tables: &HashMap<String, Vec<String>>,
662    dialect: Option<DialectType>,
663) {
664    let transformed = transform_recursive(expr.clone(), &|node| match node {
665        Expression::Column(col) if col.table.is_none() => {
666            let normalized = normalize_column_name(&col.name.name, dialect);
667            if let Some(tables) = column_tables.get(&normalized) {
668                Ok(make_coalesce(&col.name.name, tables))
669            } else {
670                Ok(Expression::Column(col))
671            }
672        }
673        other => Ok(other),
674    });
675
676    if let Ok(next) = transformed {
677        *expr = next;
678    }
679}
680
681/// Normalize ambiguous two-part column references into struct/JSON field access.
682///
683/// SQL parsers cannot distinguish `table.column` from `column.field` without a
684/// scope and schema. If the apparent table is not a source but resolves as an
685/// unambiguous column in the current scope, rewrite it to a [`DotAccess`] rooted
686/// at the qualified source column. Validation calls this same helper so it uses
687/// exactly the same interpretation as schema-aware analysis and lineage.
688pub(crate) fn normalize_dotted_columns(
689    select: &mut Select,
690    schema: &dyn Schema,
691    infer_schema: bool,
692) -> QualifyColumnsResult<()> {
693    let scope_expression = Expression::Select(Box::new(select.clone()));
694    let scope = build_scope(&scope_expression);
695    let mut resolver = Resolver::new(&scope, schema, infer_schema);
696    normalize_dotted_columns_in_scope(select, &scope, &mut resolver)
697}
698
699fn normalize_dotted_columns_in_scope(
700    select: &mut Select,
701    scope: &Scope,
702    resolver: &mut Resolver,
703) -> QualifyColumnsResult<()> {
704    for expression in &mut select.expressions {
705        normalize_dotted_columns_in_expression(expression, scope, resolver)?;
706    }
707    if let Some(where_clause) = &mut select.where_clause {
708        normalize_dotted_columns_in_expression(&mut where_clause.this, scope, resolver)?;
709    }
710    if let Some(group_by) = &mut select.group_by {
711        for expression in &mut group_by.expressions {
712            normalize_dotted_columns_in_expression(expression, scope, resolver)?;
713        }
714    }
715    if let Some(having) = &mut select.having {
716        normalize_dotted_columns_in_expression(&mut having.this, scope, resolver)?;
717    }
718    if let Some(qualify) = &mut select.qualify {
719        normalize_dotted_columns_in_expression(&mut qualify.this, scope, resolver)?;
720    }
721    if let Some(order_by) = &mut select.order_by {
722        for ordered in &mut order_by.expressions {
723            normalize_dotted_columns_in_expression(&mut ordered.this, scope, resolver)?;
724        }
725    }
726    for join in &mut select.joins {
727        normalize_dotted_columns_in_expression(&mut join.this, scope, resolver)?;
728        if let Some(on) = &mut join.on {
729            normalize_dotted_columns_in_expression(on, scope, resolver)?;
730        }
731    }
732    Ok(())
733}
734
735fn normalize_dotted_columns_in_expression(
736    expression: &mut Expression,
737    scope: &Scope,
738    resolver: &mut Resolver,
739) -> QualifyColumnsResult<()> {
740    let resolver = RefCell::new(resolver);
741    let transformed = transform_recursive(expression.clone(), &|node| {
742        let Expression::Column(column) = node else {
743            return Ok(node);
744        };
745        let Some(root) = column.table.as_ref() else {
746            return Ok(Expression::Column(column));
747        };
748
749        let root_is_source = scope
750            .sources
751            .keys()
752            .any(|source| source.eq_ignore_ascii_case(&root.name));
753        if root_is_source {
754            return Ok(Expression::Column(column));
755        }
756
757        let Some(source_name) = resolver.borrow_mut().get_table(&root.name) else {
758            return Ok(Expression::Column(column));
759        };
760
761        let root_column = Expression::boxed_column(Column {
762            name: root.clone(),
763            table: Some(Identifier::new(source_name)),
764            join_mark: column.join_mark,
765            trailing_comments: column.trailing_comments.clone(),
766            span: column.span,
767            inferred_type: None,
768        });
769
770        Ok(Expression::Dot(Box::new(DotAccess {
771            this: root_column,
772            field: column.name.clone(),
773            inferred_type: None,
774        })))
775    })
776    .map_err(|error| QualifyColumnsError::CannotAutoJoin(error.to_string()))?;
777
778    *expression = transformed;
779    Ok(())
780}
781
782/// Qualify columns in a scope by adding table qualifiers
783fn qualify_columns_in_scope(
784    select: &mut Select,
785    scope: &Scope,
786    resolver: &mut Resolver,
787    allow_partial: bool,
788) -> QualifyColumnsResult<()> {
789    for expr in &mut select.expressions {
790        qualify_columns_in_expression(expr, scope, resolver, allow_partial)?;
791    }
792    if let Some(where_clause) = &mut select.where_clause {
793        qualify_columns_in_expression(&mut where_clause.this, scope, resolver, allow_partial)?;
794    }
795    if let Some(group_by) = &mut select.group_by {
796        for expr in &mut group_by.expressions {
797            qualify_columns_in_expression(expr, scope, resolver, allow_partial)?;
798        }
799    }
800    if let Some(having) = &mut select.having {
801        qualify_columns_in_expression(&mut having.this, scope, resolver, allow_partial)?;
802    }
803    if let Some(qualify) = &mut select.qualify {
804        qualify_columns_in_expression(&mut qualify.this, scope, resolver, allow_partial)?;
805    }
806    if let Some(order_by) = &mut select.order_by {
807        for ordered in &mut order_by.expressions {
808            qualify_columns_in_expression(&mut ordered.this, scope, resolver, allow_partial)?;
809        }
810    }
811    for join in &mut select.joins {
812        qualify_columns_in_expression(&mut join.this, scope, resolver, allow_partial)?;
813        if let Some(on) = &mut join.on {
814            qualify_columns_in_expression(on, scope, resolver, allow_partial)?;
815        }
816    }
817    Ok(())
818}
819
820/// Expand alias references in a scope.
821///
822/// For example:
823/// `SELECT y.foo AS bar, bar * 2 AS baz FROM y`
824/// becomes:
825/// `SELECT y.foo AS bar, y.foo * 2 AS baz FROM y`
826fn expand_alias_refs(
827    select: &mut Select,
828    _resolver: &mut Resolver,
829    _dialect: Option<DialectType>,
830) -> QualifyColumnsResult<()> {
831    let mut alias_to_expression: HashMap<String, (Expression, usize)> = HashMap::new();
832
833    for (i, expr) in select.expressions.iter_mut().enumerate() {
834        replace_alias_refs_in_expression(expr, &alias_to_expression, false);
835        if let Expression::Alias(alias) = expr {
836            alias_to_expression.insert(alias.alias.name.clone(), (alias.this.clone(), i + 1));
837        }
838    }
839
840    if let Some(where_clause) = &mut select.where_clause {
841        replace_alias_refs_in_expression(&mut where_clause.this, &alias_to_expression, false);
842    }
843    if let Some(group_by) = &mut select.group_by {
844        for expr in &mut group_by.expressions {
845            replace_alias_refs_in_expression(expr, &alias_to_expression, true);
846        }
847    }
848    if let Some(having) = &mut select.having {
849        replace_alias_refs_in_expression(&mut having.this, &alias_to_expression, false);
850    }
851    if let Some(qualify) = &mut select.qualify {
852        replace_alias_refs_in_expression(&mut qualify.this, &alias_to_expression, false);
853    }
854    if let Some(order_by) = &mut select.order_by {
855        for ordered in &mut order_by.expressions {
856            replace_alias_refs_in_expression(&mut ordered.this, &alias_to_expression, false);
857        }
858    }
859
860    Ok(())
861}
862
863/// Expand GROUP BY positional references.
864///
865/// For example:
866/// `SELECT a, b FROM t GROUP BY 1, 2`
867/// becomes:
868/// `SELECT a, b FROM t GROUP BY a, b`
869fn expand_group_by(select: &mut Select, _dialect: Option<DialectType>) -> QualifyColumnsResult<()> {
870    let projections = select.expressions.clone();
871
872    if let Some(group_by) = &mut select.group_by {
873        for group_expr in &mut group_by.expressions {
874            if let Some(index) = positional_reference(group_expr) {
875                let replacement = select_expression_at_position(&projections, index)?;
876                *group_expr = replacement;
877            }
878        }
879    }
880    Ok(())
881}
882
883/// Expand star expressions to explicit column lists, with USING deduplication.
884///
885/// For example:
886/// `SELECT * FROM users`
887/// becomes:
888/// `SELECT users.id, users.name, users.email FROM users`
889///
890/// With USING joins, USING columns appear once as COALESCE and are
891/// deduplicated across sources.
892fn expand_stars(
893    select: &mut Select,
894    _scope: &Scope,
895    resolver: &mut Resolver,
896    column_tables: &HashMap<String, Vec<String>>,
897) -> QualifyColumnsResult<()> {
898    let mut new_selections: Vec<Expression> = Vec::new();
899    let mut has_star = false;
900    let mut coalesced_columns: HashSet<String> = HashSet::new();
901
902    // Use ordered source names (not unordered HashMap keys)
903    let ordered_sources = get_ordered_source_names(select);
904
905    for expr in &select.expressions {
906        match expr {
907            Expression::Star(star) => {
908                has_star = true;
909                if let Some(table) = &star.table {
910                    let table_name = &table.name;
911                    if !ordered_sources.contains(table_name) {
912                        return Err(QualifyColumnsError::UnknownTable(table_name.clone()));
913                    }
914                    if let Ok(columns) = resolver.get_source_columns(table_name) {
915                        if columns.contains(&"*".to_string()) || columns.is_empty() {
916                            return Ok(());
917                        }
918                        for col_name in &columns {
919                            let normalized = normalize_column_name(col_name, resolver.dialect);
920                            if coalesced_columns.contains(&normalized) {
921                                continue;
922                            }
923                            if let Some(tables) = column_tables.get(&normalized) {
924                                if tables.contains(table_name) {
925                                    coalesced_columns.insert(normalized);
926                                    let coalesce = make_coalesce(col_name, tables);
927                                    new_selections.push(Expression::Alias(Box::new(Alias {
928                                        this: coalesce,
929                                        alias: Identifier::new(col_name),
930                                        column_aliases: vec![],
931                                        alias_explicit_as: false,
932                                        alias_keyword: None,
933                                        pre_alias_comments: vec![],
934                                        trailing_comments: vec![],
935                                        inferred_type: None,
936                                    })));
937                                    continue;
938                                }
939                            }
940                            new_selections
941                                .push(create_qualified_column(col_name, Some(table_name)));
942                        }
943                    }
944                } else {
945                    for source_name in &ordered_sources {
946                        if let Ok(columns) = resolver.get_source_columns(source_name) {
947                            if columns.contains(&"*".to_string()) || columns.is_empty() {
948                                return Ok(());
949                            }
950                            for col_name in &columns {
951                                let normalized = normalize_column_name(col_name, resolver.dialect);
952                                if coalesced_columns.contains(&normalized) {
953                                    // Already emitted as COALESCE, skip
954                                    continue;
955                                }
956                                if let Some(tables) = column_tables.get(&normalized) {
957                                    if tables.contains(source_name) {
958                                        // Emit COALESCE and mark as coalesced
959                                        coalesced_columns.insert(normalized);
960                                        let coalesce = make_coalesce(col_name, tables);
961                                        new_selections.push(Expression::Alias(Box::new(Alias {
962                                            this: coalesce,
963                                            alias: Identifier::new(col_name),
964                                            column_aliases: vec![],
965                                            alias_explicit_as: false,
966                                            alias_keyword: None,
967                                            pre_alias_comments: vec![],
968                                            trailing_comments: vec![],
969                                            inferred_type: None,
970                                        })));
971                                        continue;
972                                    }
973                                }
974                                new_selections
975                                    .push(create_qualified_column(col_name, Some(source_name)));
976                            }
977                        }
978                    }
979                }
980            }
981            Expression::Column(col) if is_star_column(col) => {
982                has_star = true;
983                if let Some(table) = &col.table {
984                    let table_name = &table.name;
985                    if !ordered_sources.contains(table_name) {
986                        return Err(QualifyColumnsError::UnknownTable(table_name.clone()));
987                    }
988                    if let Ok(columns) = resolver.get_source_columns(table_name) {
989                        if columns.contains(&"*".to_string()) || columns.is_empty() {
990                            return Ok(());
991                        }
992                        for col_name in &columns {
993                            let normalized = normalize_column_name(col_name, resolver.dialect);
994                            if coalesced_columns.contains(&normalized) {
995                                continue;
996                            }
997                            if let Some(tables) = column_tables.get(&normalized) {
998                                if tables.contains(table_name) {
999                                    coalesced_columns.insert(normalized);
1000                                    let coalesce = make_coalesce(col_name, tables);
1001                                    new_selections.push(Expression::Alias(Box::new(Alias {
1002                                        this: coalesce,
1003                                        alias: Identifier::new(col_name),
1004                                        column_aliases: vec![],
1005                                        alias_explicit_as: false,
1006                                        alias_keyword: None,
1007                                        pre_alias_comments: vec![],
1008                                        trailing_comments: vec![],
1009                                        inferred_type: None,
1010                                    })));
1011                                    continue;
1012                                }
1013                            }
1014                            new_selections
1015                                .push(create_qualified_column(col_name, Some(table_name)));
1016                        }
1017                    }
1018                }
1019            }
1020            _ => new_selections.push(expr.clone()),
1021        }
1022    }
1023
1024    if has_star {
1025        select.expressions = new_selections;
1026    }
1027
1028    Ok(())
1029}
1030
1031/// Ensure all output columns in a SELECT are aliased.
1032///
1033/// For example:
1034/// `SELECT a + b FROM t`
1035/// becomes:
1036/// `SELECT a + b AS _col_0 FROM t`
1037pub fn qualify_outputs(scope: &Scope) -> QualifyColumnsResult<()> {
1038    if let Expression::Select(mut select) = scope.expression.clone() {
1039        qualify_outputs_select(&mut select)?;
1040    }
1041    Ok(())
1042}
1043
1044fn qualify_outputs_select(select: &mut Select) -> QualifyColumnsResult<()> {
1045    let mut new_selections: Vec<Expression> = Vec::new();
1046
1047    for (i, expr) in select.expressions.iter().enumerate() {
1048        match expr {
1049            Expression::Alias(_) => new_selections.push(expr.clone()),
1050            Expression::Column(col) => {
1051                new_selections.push(create_alias(expr.clone(), &col.name.name));
1052            }
1053            Expression::Star(_) => new_selections.push(expr.clone()),
1054            _ => {
1055                let alias_name = get_output_name(expr).unwrap_or_else(|| format!("_col_{}", i));
1056                new_selections.push(create_alias(expr.clone(), &alias_name));
1057            }
1058        }
1059    }
1060
1061    select.expressions = new_selections;
1062    Ok(())
1063}
1064
1065fn qualify_columns_in_expression(
1066    expr: &mut Expression,
1067    scope: &Scope,
1068    resolver: &mut Resolver,
1069    allow_partial: bool,
1070) -> QualifyColumnsResult<()> {
1071    let first_error: RefCell<Option<QualifyColumnsError>> = RefCell::new(None);
1072    let resolver_cell: RefCell<&mut Resolver> = RefCell::new(resolver);
1073
1074    let transformed = transform_recursive(expr.clone(), &|node| {
1075        if first_error.borrow().is_some() {
1076            return Ok(node);
1077        }
1078
1079        match node {
1080            Expression::Column(mut col) => {
1081                if let Err(err) = qualify_single_column(
1082                    &mut col,
1083                    scope,
1084                    &mut resolver_cell.borrow_mut(),
1085                    allow_partial,
1086                ) {
1087                    *first_error.borrow_mut() = Some(err);
1088                }
1089                Ok(Expression::Column(col))
1090            }
1091            _ => Ok(node),
1092        }
1093    })
1094    .map_err(|err| QualifyColumnsError::CannotAutoJoin(err.to_string()))?;
1095
1096    if let Some(err) = first_error.into_inner() {
1097        return Err(err);
1098    }
1099
1100    *expr = transformed;
1101    Ok(())
1102}
1103
1104fn qualify_single_column(
1105    col: &mut Column,
1106    scope: &Scope,
1107    resolver: &mut Resolver,
1108    allow_partial: bool,
1109) -> QualifyColumnsResult<()> {
1110    if is_star_column(col) {
1111        return Ok(());
1112    }
1113
1114    if let Some(table) = &col.table {
1115        let table_name = &table.name;
1116        if !scope.sources.contains_key(table_name) {
1117            // Allow correlated references: if the table exists in the schema
1118            // but not in the current scope, it may be referencing an outer scope
1119            // (e.g., in a correlated scalar subquery).
1120            if resolver.table_exists_in_schema(table_name) {
1121                return Ok(());
1122            }
1123            return Err(QualifyColumnsError::UnknownTable(table_name.clone()));
1124        }
1125
1126        if let Ok(source_columns) = resolver.get_source_columns(table_name) {
1127            let normalized_column_name = normalize_column_name(&col.name.name, resolver.dialect);
1128            if !allow_partial
1129                && !source_columns.is_empty()
1130                && !source_columns.iter().any(|column| {
1131                    normalize_column_name(column, resolver.dialect) == normalized_column_name
1132                })
1133                && !source_columns.contains(&"*".to_string())
1134            {
1135                return Err(QualifyColumnsError::UnknownColumn(col.name.name.clone()));
1136            }
1137        }
1138        return Ok(());
1139    }
1140
1141    if let Some(table_name) = resolver.get_table(&col.name.name) {
1142        col.table = Some(Identifier::new(table_name));
1143        return Ok(());
1144    }
1145
1146    // Check for correlated reference: column might belong to an outer scope table.
1147    // Search all schema tables not in the current scope for this column.
1148    if let Some(outer_table) = resolver.find_column_in_outer_schema_tables(&col.name.name) {
1149        col.table = Some(Identifier::new(outer_table));
1150        return Ok(());
1151    }
1152
1153    if !allow_partial {
1154        return Err(QualifyColumnsError::UnknownColumn(col.name.name.clone()));
1155    }
1156
1157    Ok(())
1158}
1159
1160fn normalize_column_name(name: &str, dialect: Option<DialectType>) -> String {
1161    normalize_name(name, dialect, false, true)
1162}
1163
1164fn replace_alias_refs_in_expression(
1165    expr: &mut Expression,
1166    alias_to_expression: &HashMap<String, (Expression, usize)>,
1167    literal_index: bool,
1168) {
1169    let transformed = transform_recursive(expr.clone(), &|node| match node {
1170        Expression::Column(col) if col.table.is_none() => {
1171            if let Some((alias_expr, index)) = alias_to_expression.get(&col.name.name) {
1172                if literal_index && matches!(alias_expr, Expression::Literal(_)) {
1173                    return Ok(Expression::number(*index as i64));
1174                }
1175                return Ok(Expression::Paren(Box::new(Paren {
1176                    this: alias_expr.clone(),
1177                    trailing_comments: vec![],
1178                })));
1179            }
1180            Ok(Expression::Column(col))
1181        }
1182        other => Ok(other),
1183    });
1184
1185    if let Ok(next) = transformed {
1186        *expr = next;
1187    }
1188}
1189
1190fn positional_reference(expr: &Expression) -> Option<usize> {
1191    match expr {
1192        Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
1193            let Literal::Number(value) = lit.as_ref() else {
1194                unreachable!()
1195            };
1196            value.parse::<usize>().ok()
1197        }
1198        _ => None,
1199    }
1200}
1201
1202fn select_expression_at_position(
1203    projections: &[Expression],
1204    index: usize,
1205) -> QualifyColumnsResult<Expression> {
1206    if index == 0 || index > projections.len() {
1207        return Err(QualifyColumnsError::UnknownOutputColumn(index.to_string()));
1208    }
1209
1210    let projection = projections[index - 1].clone();
1211    Ok(match projection {
1212        Expression::Alias(alias) => alias.this.clone(),
1213        other => other,
1214    })
1215}
1216
1217/// Returns the set of SQL reserved words for a given dialect.
1218/// If no dialect is provided, returns a comprehensive default set.
1219fn get_reserved_words(dialect: Option<DialectType>) -> HashSet<&'static str> {
1220    // Core SQL reserved words that are common across all dialects
1221    let mut words: HashSet<&'static str> = [
1222        // SQL standard reserved words
1223        "ADD",
1224        "ALL",
1225        "ALTER",
1226        "AND",
1227        "ANY",
1228        "AS",
1229        "ASC",
1230        "BETWEEN",
1231        "BY",
1232        "CASE",
1233        "CAST",
1234        "CHECK",
1235        "COLUMN",
1236        "CONSTRAINT",
1237        "CREATE",
1238        "CROSS",
1239        "CURRENT",
1240        "CURRENT_DATE",
1241        "CURRENT_TIME",
1242        "CURRENT_TIMESTAMP",
1243        "CURRENT_USER",
1244        "DATABASE",
1245        "DEFAULT",
1246        "DELETE",
1247        "DESC",
1248        "DISTINCT",
1249        "DROP",
1250        "ELSE",
1251        "END",
1252        "ESCAPE",
1253        "EXCEPT",
1254        "EXISTS",
1255        "FALSE",
1256        "FETCH",
1257        "FOR",
1258        "FOREIGN",
1259        "FROM",
1260        "FULL",
1261        "GRANT",
1262        "GROUP",
1263        "HAVING",
1264        "IF",
1265        "IN",
1266        "INDEX",
1267        "INNER",
1268        "INSERT",
1269        "INTERSECT",
1270        "INTO",
1271        "IS",
1272        "JOIN",
1273        "KEY",
1274        "LEFT",
1275        "LIKE",
1276        "LIMIT",
1277        "NATURAL",
1278        "NOT",
1279        "NULL",
1280        "OFFSET",
1281        "ON",
1282        "OR",
1283        "ORDER",
1284        "OUTER",
1285        "PRIMARY",
1286        "REFERENCES",
1287        "REPLACE",
1288        "RETURNING",
1289        "RIGHT",
1290        "ROLLBACK",
1291        "ROW",
1292        "ROWS",
1293        "SELECT",
1294        "SESSION_USER",
1295        "SET",
1296        "SOME",
1297        "TABLE",
1298        "THEN",
1299        "TO",
1300        "TRUE",
1301        "TRUNCATE",
1302        "UNION",
1303        "UNIQUE",
1304        "UPDATE",
1305        "USING",
1306        "VALUES",
1307        "VIEW",
1308        "WHEN",
1309        "WHERE",
1310        "WINDOW",
1311        "WITH",
1312    ]
1313    .iter()
1314    .copied()
1315    .collect();
1316
1317    // Add dialect-specific reserved words
1318    match dialect {
1319        Some(DialectType::MySQL) => {
1320            words.extend(
1321                [
1322                    "ANALYZE",
1323                    "BOTH",
1324                    "CHANGE",
1325                    "CONDITION",
1326                    "DATABASES",
1327                    "DAY_HOUR",
1328                    "DAY_MICROSECOND",
1329                    "DAY_MINUTE",
1330                    "DAY_SECOND",
1331                    "DELAYED",
1332                    "DETERMINISTIC",
1333                    "DIV",
1334                    "DUAL",
1335                    "EACH",
1336                    "ELSEIF",
1337                    "ENCLOSED",
1338                    "EXPLAIN",
1339                    "FLOAT4",
1340                    "FLOAT8",
1341                    "FORCE",
1342                    "HOUR_MICROSECOND",
1343                    "HOUR_MINUTE",
1344                    "HOUR_SECOND",
1345                    "IGNORE",
1346                    "INFILE",
1347                    "INT1",
1348                    "INT2",
1349                    "INT3",
1350                    "INT4",
1351                    "INT8",
1352                    "ITERATE",
1353                    "KEYS",
1354                    "KILL",
1355                    "LEADING",
1356                    "LEAVE",
1357                    "LINES",
1358                    "LOAD",
1359                    "LOCK",
1360                    "LONG",
1361                    "LONGBLOB",
1362                    "LONGTEXT",
1363                    "LOOP",
1364                    "LOW_PRIORITY",
1365                    "MATCH",
1366                    "MEDIUMBLOB",
1367                    "MEDIUMINT",
1368                    "MEDIUMTEXT",
1369                    "MINUTE_MICROSECOND",
1370                    "MINUTE_SECOND",
1371                    "MOD",
1372                    "MODIFIES",
1373                    "NO_WRITE_TO_BINLOG",
1374                    "OPTIMIZE",
1375                    "OPTIONALLY",
1376                    "OUT",
1377                    "OUTFILE",
1378                    "PURGE",
1379                    "READS",
1380                    "REGEXP",
1381                    "RELEASE",
1382                    "RENAME",
1383                    "REPEAT",
1384                    "REQUIRE",
1385                    "RESIGNAL",
1386                    "RETURN",
1387                    "REVOKE",
1388                    "RLIKE",
1389                    "SCHEMA",
1390                    "SCHEMAS",
1391                    "SECOND_MICROSECOND",
1392                    "SENSITIVE",
1393                    "SEPARATOR",
1394                    "SHOW",
1395                    "SIGNAL",
1396                    "SPATIAL",
1397                    "SQL",
1398                    "SQLEXCEPTION",
1399                    "SQLSTATE",
1400                    "SQLWARNING",
1401                    "SQL_BIG_RESULT",
1402                    "SQL_CALC_FOUND_ROWS",
1403                    "SQL_SMALL_RESULT",
1404                    "SSL",
1405                    "STARTING",
1406                    "STRAIGHT_JOIN",
1407                    "TERMINATED",
1408                    "TINYBLOB",
1409                    "TINYINT",
1410                    "TINYTEXT",
1411                    "TRAILING",
1412                    "TRIGGER",
1413                    "UNDO",
1414                    "UNLOCK",
1415                    "UNSIGNED",
1416                    "USAGE",
1417                    "UTC_DATE",
1418                    "UTC_TIME",
1419                    "UTC_TIMESTAMP",
1420                    "VARBINARY",
1421                    "VARCHARACTER",
1422                    "WHILE",
1423                    "WRITE",
1424                    "XOR",
1425                    "YEAR_MONTH",
1426                    "ZEROFILL",
1427                ]
1428                .iter()
1429                .copied(),
1430            );
1431        }
1432        Some(DialectType::PostgreSQL) | Some(DialectType::CockroachDB) => {
1433            words.extend(
1434                [
1435                    "ANALYSE",
1436                    "ANALYZE",
1437                    "ARRAY",
1438                    "AUTHORIZATION",
1439                    "BINARY",
1440                    "BOTH",
1441                    "COLLATE",
1442                    "CONCURRENTLY",
1443                    "DO",
1444                    "FREEZE",
1445                    "ILIKE",
1446                    "INITIALLY",
1447                    "ISNULL",
1448                    "LATERAL",
1449                    "LEADING",
1450                    "LOCALTIME",
1451                    "LOCALTIMESTAMP",
1452                    "NOTNULL",
1453                    "ONLY",
1454                    "OVERLAPS",
1455                    "PLACING",
1456                    "SIMILAR",
1457                    "SYMMETRIC",
1458                    "TABLESAMPLE",
1459                    "TRAILING",
1460                    "VARIADIC",
1461                    "VERBOSE",
1462                ]
1463                .iter()
1464                .copied(),
1465            );
1466        }
1467        Some(DialectType::BigQuery) => {
1468            words.extend(
1469                [
1470                    "ASSERT_ROWS_MODIFIED",
1471                    "COLLATE",
1472                    "CONTAINS",
1473                    "CUBE",
1474                    "DEFINE",
1475                    "ENUM",
1476                    "EXTRACT",
1477                    "FOLLOWING",
1478                    "GROUPING",
1479                    "GROUPS",
1480                    "HASH",
1481                    "IGNORE",
1482                    "LATERAL",
1483                    "LOOKUP",
1484                    "MERGE",
1485                    "NEW",
1486                    "NO",
1487                    "NULLS",
1488                    "OF",
1489                    "OVER",
1490                    "PARTITION",
1491                    "PRECEDING",
1492                    "PROTO",
1493                    "RANGE",
1494                    "RECURSIVE",
1495                    "RESPECT",
1496                    "ROLLUP",
1497                    "STRUCT",
1498                    "TABLESAMPLE",
1499                    "TREAT",
1500                    "UNBOUNDED",
1501                    "WITHIN",
1502                ]
1503                .iter()
1504                .copied(),
1505            );
1506        }
1507        Some(DialectType::Snowflake) => {
1508            words.extend(
1509                [
1510                    "ACCOUNT",
1511                    "BOTH",
1512                    "CONNECT",
1513                    "FOLLOWING",
1514                    "ILIKE",
1515                    "INCREMENT",
1516                    "ISSUE",
1517                    "LATERAL",
1518                    "LEADING",
1519                    "LOCALTIME",
1520                    "LOCALTIMESTAMP",
1521                    "MINUS",
1522                    "QUALIFY",
1523                    "REGEXP",
1524                    "RLIKE",
1525                    "SOME",
1526                    "START",
1527                    "TABLESAMPLE",
1528                    "TOP",
1529                    "TRAILING",
1530                    "TRY_CAST",
1531                ]
1532                .iter()
1533                .copied(),
1534            );
1535        }
1536        Some(DialectType::TSQL) | Some(DialectType::Fabric) => {
1537            words.extend(
1538                [
1539                    "BACKUP",
1540                    "BREAK",
1541                    "BROWSE",
1542                    "BULK",
1543                    "CASCADE",
1544                    "CHECKPOINT",
1545                    "CLOSE",
1546                    "CLUSTERED",
1547                    "COALESCE",
1548                    "COMPUTE",
1549                    "CONTAINS",
1550                    "CONTAINSTABLE",
1551                    "CONTINUE",
1552                    "CONVERT",
1553                    "DBCC",
1554                    "DEALLOCATE",
1555                    "DENY",
1556                    "DISK",
1557                    "DISTRIBUTED",
1558                    "DUMP",
1559                    "ERRLVL",
1560                    "EXEC",
1561                    "EXECUTE",
1562                    "EXIT",
1563                    "EXTERNAL",
1564                    "FILE",
1565                    "FILLFACTOR",
1566                    "FREETEXT",
1567                    "FREETEXTTABLE",
1568                    "FUNCTION",
1569                    "GOTO",
1570                    "HOLDLOCK",
1571                    "IDENTITY",
1572                    "IDENTITYCOL",
1573                    "IDENTITY_INSERT",
1574                    "KILL",
1575                    "LINENO",
1576                    "MERGE",
1577                    "NONCLUSTERED",
1578                    "NULLIF",
1579                    "OF",
1580                    "OFF",
1581                    "OFFSETS",
1582                    "OPEN",
1583                    "OPENDATASOURCE",
1584                    "OPENQUERY",
1585                    "OPENROWSET",
1586                    "OPENXML",
1587                    "OVER",
1588                    "PERCENT",
1589                    "PIVOT",
1590                    "PLAN",
1591                    "PRINT",
1592                    "PROC",
1593                    "PROCEDURE",
1594                    "PUBLIC",
1595                    "RAISERROR",
1596                    "READ",
1597                    "READTEXT",
1598                    "RECONFIGURE",
1599                    "REPLICATION",
1600                    "RESTORE",
1601                    "RESTRICT",
1602                    "REVERT",
1603                    "ROWCOUNT",
1604                    "ROWGUIDCOL",
1605                    "RULE",
1606                    "SAVE",
1607                    "SECURITYAUDIT",
1608                    "SEMANTICKEYPHRASETABLE",
1609                    "SEMANTICSIMILARITYDETAILSTABLE",
1610                    "SEMANTICSIMILARITYTABLE",
1611                    "SETUSER",
1612                    "SHUTDOWN",
1613                    "STATISTICS",
1614                    "SYSTEM_USER",
1615                    "TEXTSIZE",
1616                    "TOP",
1617                    "TRAN",
1618                    "TRANSACTION",
1619                    "TRIGGER",
1620                    "TSEQUAL",
1621                    "UNPIVOT",
1622                    "UPDATETEXT",
1623                    "WAITFOR",
1624                    "WRITETEXT",
1625                ]
1626                .iter()
1627                .copied(),
1628            );
1629        }
1630        Some(DialectType::ClickHouse) => {
1631            words.extend(
1632                [
1633                    "ANTI",
1634                    "ARRAY",
1635                    "ASOF",
1636                    "FINAL",
1637                    "FORMAT",
1638                    "GLOBAL",
1639                    "INF",
1640                    "KILL",
1641                    "MATERIALIZED",
1642                    "NAN",
1643                    "PREWHERE",
1644                    "SAMPLE",
1645                    "SEMI",
1646                    "SETTINGS",
1647                    "TOP",
1648                ]
1649                .iter()
1650                .copied(),
1651            );
1652        }
1653        Some(DialectType::DuckDB) => {
1654            words.extend(
1655                [
1656                    "ANALYSE",
1657                    "ANALYZE",
1658                    "ARRAY",
1659                    "BOTH",
1660                    "LATERAL",
1661                    "LEADING",
1662                    "LOCALTIME",
1663                    "LOCALTIMESTAMP",
1664                    "PLACING",
1665                    "QUALIFY",
1666                    "SIMILAR",
1667                    "TABLESAMPLE",
1668                    "TRAILING",
1669                ]
1670                .iter()
1671                .copied(),
1672            );
1673        }
1674        Some(DialectType::Hive) | Some(DialectType::Spark) | Some(DialectType::Databricks) => {
1675            words.extend(
1676                [
1677                    "BOTH",
1678                    "CLUSTER",
1679                    "DISTRIBUTE",
1680                    "EXCHANGE",
1681                    "EXTENDED",
1682                    "FUNCTION",
1683                    "LATERAL",
1684                    "LEADING",
1685                    "MACRO",
1686                    "OVER",
1687                    "PARTITION",
1688                    "PERCENT",
1689                    "RANGE",
1690                    "READS",
1691                    "REDUCE",
1692                    "REGEXP",
1693                    "REVOKE",
1694                    "RLIKE",
1695                    "ROLLUP",
1696                    "SEMI",
1697                    "SORT",
1698                    "TABLESAMPLE",
1699                    "TRAILING",
1700                    "TRANSFORM",
1701                    "UNBOUNDED",
1702                    "UNIQUEJOIN",
1703                ]
1704                .iter()
1705                .copied(),
1706            );
1707        }
1708        Some(DialectType::Trino) | Some(DialectType::Presto) | Some(DialectType::Athena) => {
1709            words.extend(
1710                [
1711                    "CUBE",
1712                    "DEALLOCATE",
1713                    "DESCRIBE",
1714                    "EXECUTE",
1715                    "EXTRACT",
1716                    "GROUPING",
1717                    "LATERAL",
1718                    "LOCALTIME",
1719                    "LOCALTIMESTAMP",
1720                    "NORMALIZE",
1721                    "PREPARE",
1722                    "ROLLUP",
1723                    "SOME",
1724                    "TABLESAMPLE",
1725                    "UESCAPE",
1726                    "UNNEST",
1727                ]
1728                .iter()
1729                .copied(),
1730            );
1731        }
1732        Some(DialectType::Oracle) => {
1733            words.extend(
1734                [
1735                    "ACCESS",
1736                    "AUDIT",
1737                    "CLUSTER",
1738                    "COMMENT",
1739                    "COMPRESS",
1740                    "CONNECT",
1741                    "EXCLUSIVE",
1742                    "FILE",
1743                    "IDENTIFIED",
1744                    "IMMEDIATE",
1745                    "INCREMENT",
1746                    "INITIAL",
1747                    "LEVEL",
1748                    "LOCK",
1749                    "LONG",
1750                    "MAXEXTENTS",
1751                    "MINUS",
1752                    "MODE",
1753                    "NOAUDIT",
1754                    "NOCOMPRESS",
1755                    "NOWAIT",
1756                    "NUMBER",
1757                    "OF",
1758                    "OFFLINE",
1759                    "ONLINE",
1760                    "PCTFREE",
1761                    "PRIOR",
1762                    "RAW",
1763                    "RENAME",
1764                    "RESOURCE",
1765                    "REVOKE",
1766                    "SHARE",
1767                    "SIZE",
1768                    "START",
1769                    "SUCCESSFUL",
1770                    "SYNONYM",
1771                    "SYSDATE",
1772                    "TRIGGER",
1773                    "UID",
1774                    "VALIDATE",
1775                    "VARCHAR2",
1776                    "WHENEVER",
1777                ]
1778                .iter()
1779                .copied(),
1780            );
1781        }
1782        Some(DialectType::Redshift) => {
1783            words.extend(
1784                [
1785                    "AZ64",
1786                    "BZIP2",
1787                    "DELTA",
1788                    "DELTA32K",
1789                    "DISTSTYLE",
1790                    "ENCODE",
1791                    "GZIP",
1792                    "ILIKE",
1793                    "LIMIT",
1794                    "LUNS",
1795                    "LZO",
1796                    "LZOP",
1797                    "MOSTLY13",
1798                    "MOSTLY32",
1799                    "MOSTLY8",
1800                    "RAW",
1801                    "SIMILAR",
1802                    "SNAPSHOT",
1803                    "SORTKEY",
1804                    "SYSDATE",
1805                    "TOP",
1806                    "ZSTD",
1807                ]
1808                .iter()
1809                .copied(),
1810            );
1811        }
1812        _ => {
1813            // For Generic or unknown dialects, add a broad set of commonly reserved words
1814            words.extend(
1815                [
1816                    "ANALYZE",
1817                    "ARRAY",
1818                    "BOTH",
1819                    "CUBE",
1820                    "GROUPING",
1821                    "LATERAL",
1822                    "LEADING",
1823                    "LOCALTIME",
1824                    "LOCALTIMESTAMP",
1825                    "OVER",
1826                    "PARTITION",
1827                    "QUALIFY",
1828                    "RANGE",
1829                    "ROLLUP",
1830                    "SIMILAR",
1831                    "SOME",
1832                    "TABLESAMPLE",
1833                    "TRAILING",
1834                ]
1835                .iter()
1836                .copied(),
1837            );
1838        }
1839    }
1840
1841    words
1842}
1843
1844/// Check whether an identifier name needs quoting.
1845///
1846/// An identifier needs quoting if:
1847/// - It is empty
1848/// - It starts with a digit
1849/// - It contains characters other than `[a-zA-Z0-9_]`
1850/// - It is a SQL reserved word (case-insensitive)
1851fn needs_quoting(name: &str, reserved_words: &HashSet<&str>) -> bool {
1852    if name.is_empty() {
1853        return false;
1854    }
1855
1856    // Starts with a digit
1857    if name.as_bytes()[0].is_ascii_digit() {
1858        return true;
1859    }
1860
1861    // Contains non-identifier characters
1862    if !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
1863        return true;
1864    }
1865
1866    // Is a reserved word (case-insensitive check)
1867    let upper = name.to_uppercase();
1868    reserved_words.contains(upper.as_str())
1869}
1870
1871/// Conditionally set `quoted = true` on an identifier if it needs quoting.
1872fn maybe_quote(id: &mut Identifier, reserved_words: &HashSet<&str>) {
1873    // Don't re-quote something already quoted, and don't quote empty identifiers
1874    // or wildcard identifiers
1875    if id.quoted || id.name.is_empty() || id.name == "*" {
1876        return;
1877    }
1878    if needs_quoting(&id.name, reserved_words) {
1879        id.quoted = true;
1880    }
1881}
1882
1883/// Recursively walk an expression and quote identifiers that need quoting.
1884fn quote_identifiers_recursive(expr: &mut Expression, reserved_words: &HashSet<&str>) {
1885    match expr {
1886        // ── Leaf nodes with Identifier ────────────────────────────
1887        Expression::Identifier(id) => {
1888            maybe_quote(id, reserved_words);
1889        }
1890
1891        Expression::Column(col) => {
1892            maybe_quote(&mut col.name, reserved_words);
1893            if let Some(ref mut table) = col.table {
1894                maybe_quote(table, reserved_words);
1895            }
1896        }
1897
1898        Expression::Table(table_ref) => {
1899            maybe_quote(&mut table_ref.name, reserved_words);
1900            if let Some(ref mut schema) = table_ref.schema {
1901                maybe_quote(schema, reserved_words);
1902            }
1903            if let Some(ref mut catalog) = table_ref.catalog {
1904                maybe_quote(catalog, reserved_words);
1905            }
1906            if let Some(ref mut alias) = table_ref.alias {
1907                maybe_quote(alias, reserved_words);
1908            }
1909            for ca in &mut table_ref.column_aliases {
1910                maybe_quote(ca, reserved_words);
1911            }
1912            for p in &mut table_ref.partitions {
1913                maybe_quote(p, reserved_words);
1914            }
1915            // Recurse into hints and other child expressions
1916            for h in &mut table_ref.hints {
1917                quote_identifiers_recursive(h, reserved_words);
1918            }
1919            if let Some(ref mut ver) = table_ref.version {
1920                quote_identifiers_recursive(&mut ver.this, reserved_words);
1921                if let Some(ref mut e) = ver.expression {
1922                    quote_identifiers_recursive(e, reserved_words);
1923                }
1924            }
1925        }
1926
1927        Expression::Star(star) => {
1928            if let Some(ref mut table) = star.table {
1929                maybe_quote(table, reserved_words);
1930            }
1931            if let Some(ref mut except_ids) = star.except {
1932                for id in except_ids {
1933                    maybe_quote(id, reserved_words);
1934                }
1935            }
1936            if let Some(ref mut replace_aliases) = star.replace {
1937                for alias in replace_aliases {
1938                    maybe_quote(&mut alias.alias, reserved_words);
1939                    quote_identifiers_recursive(&mut alias.this, reserved_words);
1940                }
1941            }
1942            if let Some(ref mut rename_pairs) = star.rename {
1943                for (from, to) in rename_pairs {
1944                    maybe_quote(from, reserved_words);
1945                    maybe_quote(to, reserved_words);
1946                }
1947            }
1948        }
1949
1950        // ── Alias ─────────────────────────────────────────────────
1951        Expression::Alias(alias) => {
1952            maybe_quote(&mut alias.alias, reserved_words);
1953            for ca in &mut alias.column_aliases {
1954                maybe_quote(ca, reserved_words);
1955            }
1956            quote_identifiers_recursive(&mut alias.this, reserved_words);
1957        }
1958
1959        // ── SELECT ────────────────────────────────────────────────
1960        Expression::Select(select) => {
1961            for e in &mut select.expressions {
1962                quote_identifiers_recursive(e, reserved_words);
1963            }
1964            if let Some(ref mut from) = select.from {
1965                for e in &mut from.expressions {
1966                    quote_identifiers_recursive(e, reserved_words);
1967                }
1968            }
1969            for join in &mut select.joins {
1970                quote_join(join, reserved_words);
1971            }
1972            for lv in &mut select.lateral_views {
1973                quote_lateral_view(lv, reserved_words);
1974            }
1975            if let Some(ref mut prewhere) = select.prewhere {
1976                quote_identifiers_recursive(prewhere, reserved_words);
1977            }
1978            if let Some(ref mut wh) = select.where_clause {
1979                quote_identifiers_recursive(&mut wh.this, reserved_words);
1980            }
1981            if let Some(ref mut gb) = select.group_by {
1982                for e in &mut gb.expressions {
1983                    quote_identifiers_recursive(e, reserved_words);
1984                }
1985            }
1986            if let Some(ref mut hv) = select.having {
1987                quote_identifiers_recursive(&mut hv.this, reserved_words);
1988            }
1989            if let Some(ref mut q) = select.qualify {
1990                quote_identifiers_recursive(&mut q.this, reserved_words);
1991            }
1992            if let Some(ref mut ob) = select.order_by {
1993                for o in &mut ob.expressions {
1994                    quote_identifiers_recursive(&mut o.this, reserved_words);
1995                }
1996            }
1997            if let Some(ref mut lim) = select.limit {
1998                quote_identifiers_recursive(&mut lim.this, reserved_words);
1999            }
2000            if let Some(ref mut off) = select.offset {
2001                quote_identifiers_recursive(&mut off.this, reserved_words);
2002            }
2003            if let Some(ref mut with) = select.with {
2004                quote_with(with, reserved_words);
2005            }
2006            if let Some(ref mut windows) = select.windows {
2007                for nw in windows {
2008                    maybe_quote(&mut nw.name, reserved_words);
2009                    quote_over(&mut nw.spec, reserved_words);
2010                }
2011            }
2012            if let Some(ref mut distinct_on) = select.distinct_on {
2013                for e in distinct_on {
2014                    quote_identifiers_recursive(e, reserved_words);
2015                }
2016            }
2017            if let Some(ref mut limit_by) = select.limit_by {
2018                for e in limit_by {
2019                    quote_identifiers_recursive(e, reserved_words);
2020                }
2021            }
2022            if let Some(ref mut settings) = select.settings {
2023                for e in settings {
2024                    quote_identifiers_recursive(e, reserved_words);
2025                }
2026            }
2027            if let Some(ref mut format) = select.format {
2028                quote_identifiers_recursive(format, reserved_words);
2029            }
2030        }
2031
2032        // ── Set operations ────────────────────────────────────────
2033        Expression::Union(u) => {
2034            quote_identifiers_recursive(&mut u.left, reserved_words);
2035            quote_identifiers_recursive(&mut u.right, reserved_words);
2036            if let Some(ref mut with) = u.with {
2037                quote_with(with, reserved_words);
2038            }
2039            if let Some(ref mut ob) = u.order_by {
2040                for o in &mut ob.expressions {
2041                    quote_identifiers_recursive(&mut o.this, reserved_words);
2042                }
2043            }
2044            if let Some(ref mut lim) = u.limit {
2045                quote_identifiers_recursive(lim, reserved_words);
2046            }
2047            if let Some(ref mut off) = u.offset {
2048                quote_identifiers_recursive(off, reserved_words);
2049            }
2050        }
2051        Expression::Intersect(i) => {
2052            quote_identifiers_recursive(&mut i.left, reserved_words);
2053            quote_identifiers_recursive(&mut i.right, reserved_words);
2054            if let Some(ref mut with) = i.with {
2055                quote_with(with, reserved_words);
2056            }
2057            if let Some(ref mut ob) = i.order_by {
2058                for o in &mut ob.expressions {
2059                    quote_identifiers_recursive(&mut o.this, reserved_words);
2060                }
2061            }
2062        }
2063        Expression::Except(e) => {
2064            quote_identifiers_recursive(&mut e.left, reserved_words);
2065            quote_identifiers_recursive(&mut e.right, reserved_words);
2066            if let Some(ref mut with) = e.with {
2067                quote_with(with, reserved_words);
2068            }
2069            if let Some(ref mut ob) = e.order_by {
2070                for o in &mut ob.expressions {
2071                    quote_identifiers_recursive(&mut o.this, reserved_words);
2072                }
2073            }
2074        }
2075
2076        // ── Subquery ──────────────────────────────────────────────
2077        Expression::Subquery(sq) => {
2078            quote_identifiers_recursive(&mut sq.this, reserved_words);
2079            if let Some(ref mut alias) = sq.alias {
2080                maybe_quote(alias, reserved_words);
2081            }
2082            for ca in &mut sq.column_aliases {
2083                maybe_quote(ca, reserved_words);
2084            }
2085            if let Some(ref mut ob) = sq.order_by {
2086                for o in &mut ob.expressions {
2087                    quote_identifiers_recursive(&mut o.this, reserved_words);
2088                }
2089            }
2090        }
2091
2092        // ── DML ───────────────────────────────────────────────────
2093        Expression::Insert(ins) => {
2094            quote_table_ref(&mut ins.table, reserved_words);
2095            for c in &mut ins.columns {
2096                maybe_quote(c, reserved_words);
2097            }
2098            for row in &mut ins.values {
2099                for e in row {
2100                    quote_identifiers_recursive(e, reserved_words);
2101                }
2102            }
2103            if let Some(ref mut q) = ins.query {
2104                quote_identifiers_recursive(q, reserved_words);
2105            }
2106            for (id, val) in &mut ins.partition {
2107                maybe_quote(id, reserved_words);
2108                if let Some(ref mut v) = val {
2109                    quote_identifiers_recursive(v, reserved_words);
2110                }
2111            }
2112            for e in &mut ins.returning {
2113                quote_identifiers_recursive(e, reserved_words);
2114            }
2115            if let Some(ref mut on_conflict) = ins.on_conflict {
2116                quote_identifiers_recursive(on_conflict, reserved_words);
2117            }
2118            if let Some(ref mut with) = ins.with {
2119                quote_with(with, reserved_words);
2120            }
2121            if let Some(ref mut alias) = ins.alias {
2122                maybe_quote(alias, reserved_words);
2123            }
2124            if let Some(ref mut src_alias) = ins.source_alias {
2125                maybe_quote(src_alias, reserved_words);
2126            }
2127        }
2128
2129        Expression::Update(upd) => {
2130            quote_table_ref(&mut upd.table, reserved_words);
2131            for tr in &mut upd.extra_tables {
2132                quote_table_ref(tr, reserved_words);
2133            }
2134            for join in &mut upd.table_joins {
2135                quote_join(join, reserved_words);
2136            }
2137            for (id, val) in &mut upd.set {
2138                maybe_quote(id, reserved_words);
2139                quote_identifiers_recursive(val, reserved_words);
2140            }
2141            if let Some(ref mut from) = upd.from_clause {
2142                for e in &mut from.expressions {
2143                    quote_identifiers_recursive(e, reserved_words);
2144                }
2145            }
2146            for join in &mut upd.from_joins {
2147                quote_join(join, reserved_words);
2148            }
2149            if let Some(ref mut wh) = upd.where_clause {
2150                quote_identifiers_recursive(&mut wh.this, reserved_words);
2151            }
2152            for e in &mut upd.returning {
2153                quote_identifiers_recursive(e, reserved_words);
2154            }
2155            if let Some(ref mut with) = upd.with {
2156                quote_with(with, reserved_words);
2157            }
2158        }
2159
2160        Expression::Delete(del) => {
2161            quote_table_ref(&mut del.table, reserved_words);
2162            if let Some(ref mut alias) = del.alias {
2163                maybe_quote(alias, reserved_words);
2164            }
2165            for tr in &mut del.using {
2166                quote_table_ref(tr, reserved_words);
2167            }
2168            if let Some(ref mut wh) = del.where_clause {
2169                quote_identifiers_recursive(&mut wh.this, reserved_words);
2170            }
2171            if let Some(ref mut with) = del.with {
2172                quote_with(with, reserved_words);
2173            }
2174        }
2175
2176        // ── Binary operations ─────────────────────────────────────
2177        Expression::And(bin)
2178        | Expression::Or(bin)
2179        | Expression::Eq(bin)
2180        | Expression::Neq(bin)
2181        | Expression::Lt(bin)
2182        | Expression::Lte(bin)
2183        | Expression::Gt(bin)
2184        | Expression::Gte(bin)
2185        | Expression::Add(bin)
2186        | Expression::Sub(bin)
2187        | Expression::Mul(bin)
2188        | Expression::Div(bin)
2189        | Expression::Mod(bin)
2190        | Expression::BitwiseAnd(bin)
2191        | Expression::BitwiseOr(bin)
2192        | Expression::BitwiseXor(bin)
2193        | Expression::Concat(bin)
2194        | Expression::Adjacent(bin)
2195        | Expression::TsMatch(bin)
2196        | Expression::PropertyEQ(bin)
2197        | Expression::ArrayContainsAll(bin)
2198        | Expression::ArrayContainedBy(bin)
2199        | Expression::ArrayOverlaps(bin)
2200        | Expression::JSONBContainsAllTopKeys(bin)
2201        | Expression::JSONBContainsAnyTopKeys(bin)
2202        | Expression::JSONBDeleteAtPath(bin)
2203        | Expression::ExtendsLeft(bin)
2204        | Expression::ExtendsRight(bin)
2205        | Expression::Is(bin)
2206        | Expression::NullSafeEq(bin)
2207        | Expression::NullSafeNeq(bin)
2208        | Expression::Glob(bin)
2209        | Expression::Match(bin)
2210        | Expression::MemberOf(bin)
2211        | Expression::BitwiseLeftShift(bin)
2212        | Expression::BitwiseRightShift(bin) => {
2213            quote_identifiers_recursive(&mut bin.left, reserved_words);
2214            quote_identifiers_recursive(&mut bin.right, reserved_words);
2215        }
2216
2217        // ── Like operations ───────────────────────────────────────
2218        Expression::Like(like) | Expression::ILike(like) => {
2219            quote_identifiers_recursive(&mut like.left, reserved_words);
2220            quote_identifiers_recursive(&mut like.right, reserved_words);
2221            if let Some(ref mut esc) = like.escape {
2222                quote_identifiers_recursive(esc, reserved_words);
2223            }
2224        }
2225
2226        // ── Unary operations ──────────────────────────────────────
2227        Expression::Not(un) | Expression::Neg(un) | Expression::BitwiseNot(un) => {
2228            quote_identifiers_recursive(&mut un.this, reserved_words);
2229        }
2230
2231        // ── Predicates ────────────────────────────────────────────
2232        Expression::In(in_expr) => {
2233            quote_identifiers_recursive(&mut in_expr.this, reserved_words);
2234            for e in &mut in_expr.expressions {
2235                quote_identifiers_recursive(e, reserved_words);
2236            }
2237            if let Some(ref mut q) = in_expr.query {
2238                quote_identifiers_recursive(q, reserved_words);
2239            }
2240            if let Some(ref mut un) = in_expr.unnest {
2241                quote_identifiers_recursive(un, reserved_words);
2242            }
2243        }
2244
2245        Expression::Between(bw) => {
2246            quote_identifiers_recursive(&mut bw.this, reserved_words);
2247            quote_identifiers_recursive(&mut bw.low, reserved_words);
2248            quote_identifiers_recursive(&mut bw.high, reserved_words);
2249        }
2250
2251        Expression::IsNull(is_null) => {
2252            quote_identifiers_recursive(&mut is_null.this, reserved_words);
2253        }
2254
2255        Expression::IsTrue(is_tf) | Expression::IsFalse(is_tf) => {
2256            quote_identifiers_recursive(&mut is_tf.this, reserved_words);
2257        }
2258
2259        Expression::Exists(ex) => {
2260            quote_identifiers_recursive(&mut ex.this, reserved_words);
2261        }
2262
2263        // ── Functions ─────────────────────────────────────────────
2264        Expression::Function(func) => {
2265            for arg in &mut func.args {
2266                quote_identifiers_recursive(arg, reserved_words);
2267            }
2268        }
2269
2270        Expression::AggregateFunction(agg) => {
2271            for arg in &mut agg.args {
2272                quote_identifiers_recursive(arg, reserved_words);
2273            }
2274            if let Some(ref mut filter) = agg.filter {
2275                quote_identifiers_recursive(filter, reserved_words);
2276            }
2277            for o in &mut agg.order_by {
2278                quote_identifiers_recursive(&mut o.this, reserved_words);
2279            }
2280        }
2281
2282        Expression::WindowFunction(wf) => {
2283            quote_identifiers_recursive(&mut wf.this, reserved_words);
2284            quote_over(&mut wf.over, reserved_words);
2285        }
2286
2287        // ── CASE ──────────────────────────────────────────────────
2288        Expression::Case(case) => {
2289            if let Some(ref mut operand) = case.operand {
2290                quote_identifiers_recursive(operand, reserved_words);
2291            }
2292            for (when, then) in &mut case.whens {
2293                quote_identifiers_recursive(when, reserved_words);
2294                quote_identifiers_recursive(then, reserved_words);
2295            }
2296            if let Some(ref mut else_) = case.else_ {
2297                quote_identifiers_recursive(else_, reserved_words);
2298            }
2299        }
2300
2301        // ── CAST / TryCast / SafeCast ─────────────────────────────
2302        Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
2303            quote_identifiers_recursive(&mut cast.this, reserved_words);
2304            if let Some(ref mut fmt) = cast.format {
2305                quote_identifiers_recursive(fmt, reserved_words);
2306            }
2307        }
2308
2309        // ── Paren / Annotated ─────────────────────────────────────
2310        Expression::Paren(paren) => {
2311            quote_identifiers_recursive(&mut paren.this, reserved_words);
2312        }
2313
2314        Expression::Annotated(ann) => {
2315            quote_identifiers_recursive(&mut ann.this, reserved_words);
2316        }
2317
2318        // ── WITH clause (standalone) ──────────────────────────────
2319        Expression::With(with) => {
2320            quote_with(with, reserved_words);
2321        }
2322
2323        Expression::Cte(cte) => {
2324            maybe_quote(&mut cte.alias, reserved_words);
2325            for c in &mut cte.columns {
2326                maybe_quote(c, reserved_words);
2327            }
2328            quote_identifiers_recursive(&mut cte.this, reserved_words);
2329        }
2330
2331        // ── Clauses (standalone) ──────────────────────────────────
2332        Expression::From(from) => {
2333            for e in &mut from.expressions {
2334                quote_identifiers_recursive(e, reserved_words);
2335            }
2336        }
2337
2338        Expression::Join(join) => {
2339            quote_join(join, reserved_words);
2340        }
2341
2342        Expression::JoinedTable(jt) => {
2343            quote_identifiers_recursive(&mut jt.left, reserved_words);
2344            for join in &mut jt.joins {
2345                quote_join(join, reserved_words);
2346            }
2347            if let Some(ref mut alias) = jt.alias {
2348                maybe_quote(alias, reserved_words);
2349            }
2350        }
2351
2352        Expression::Where(wh) => {
2353            quote_identifiers_recursive(&mut wh.this, reserved_words);
2354        }
2355
2356        Expression::GroupBy(gb) => {
2357            for e in &mut gb.expressions {
2358                quote_identifiers_recursive(e, reserved_words);
2359            }
2360        }
2361
2362        Expression::Having(hv) => {
2363            quote_identifiers_recursive(&mut hv.this, reserved_words);
2364        }
2365
2366        Expression::OrderBy(ob) => {
2367            for o in &mut ob.expressions {
2368                quote_identifiers_recursive(&mut o.this, reserved_words);
2369            }
2370        }
2371
2372        Expression::Ordered(ord) => {
2373            quote_identifiers_recursive(&mut ord.this, reserved_words);
2374        }
2375
2376        Expression::Limit(lim) => {
2377            quote_identifiers_recursive(&mut lim.this, reserved_words);
2378        }
2379
2380        Expression::Offset(off) => {
2381            quote_identifiers_recursive(&mut off.this, reserved_words);
2382        }
2383
2384        Expression::Qualify(q) => {
2385            quote_identifiers_recursive(&mut q.this, reserved_words);
2386        }
2387
2388        Expression::Window(ws) => {
2389            for e in &mut ws.partition_by {
2390                quote_identifiers_recursive(e, reserved_words);
2391            }
2392            for o in &mut ws.order_by {
2393                quote_identifiers_recursive(&mut o.this, reserved_words);
2394            }
2395        }
2396
2397        Expression::Over(over) => {
2398            quote_over(over, reserved_words);
2399        }
2400
2401        Expression::WithinGroup(wg) => {
2402            quote_identifiers_recursive(&mut wg.this, reserved_words);
2403            for o in &mut wg.order_by {
2404                quote_identifiers_recursive(&mut o.this, reserved_words);
2405            }
2406        }
2407
2408        // ── Pivot / Unpivot ───────────────────────────────────────
2409        Expression::Pivot(piv) => {
2410            quote_identifiers_recursive(&mut piv.this, reserved_words);
2411            for e in &mut piv.expressions {
2412                quote_identifiers_recursive(e, reserved_words);
2413            }
2414            for f in &mut piv.fields {
2415                quote_identifiers_recursive(f, reserved_words);
2416            }
2417            if let Some(ref mut alias) = piv.alias {
2418                maybe_quote(alias, reserved_words);
2419            }
2420        }
2421
2422        Expression::Unpivot(unpiv) => {
2423            quote_identifiers_recursive(&mut unpiv.this, reserved_words);
2424            maybe_quote(&mut unpiv.value_column, reserved_words);
2425            maybe_quote(&mut unpiv.name_column, reserved_words);
2426            for e in &mut unpiv.columns {
2427                quote_identifiers_recursive(e, reserved_words);
2428            }
2429            if let Some(ref mut alias) = unpiv.alias {
2430                maybe_quote(alias, reserved_words);
2431            }
2432        }
2433
2434        // ── Values ────────────────────────────────────────────────
2435        Expression::Values(vals) => {
2436            for tuple in &mut vals.expressions {
2437                for e in &mut tuple.expressions {
2438                    quote_identifiers_recursive(e, reserved_words);
2439                }
2440            }
2441            if let Some(ref mut alias) = vals.alias {
2442                maybe_quote(alias, reserved_words);
2443            }
2444            for ca in &mut vals.column_aliases {
2445                maybe_quote(ca, reserved_words);
2446            }
2447        }
2448
2449        // ── Array / Struct / Tuple ────────────────────────────────
2450        Expression::Array(arr) => {
2451            for e in &mut arr.expressions {
2452                quote_identifiers_recursive(e, reserved_words);
2453            }
2454        }
2455
2456        Expression::Struct(st) => {
2457            for (_name, e) in &mut st.fields {
2458                quote_identifiers_recursive(e, reserved_words);
2459            }
2460        }
2461
2462        Expression::Tuple(tup) => {
2463            for e in &mut tup.expressions {
2464                quote_identifiers_recursive(e, reserved_words);
2465            }
2466        }
2467
2468        // ── Subscript / Dot / Method ──────────────────────────────
2469        Expression::Subscript(sub) => {
2470            quote_identifiers_recursive(&mut sub.this, reserved_words);
2471            quote_identifiers_recursive(&mut sub.index, reserved_words);
2472        }
2473
2474        Expression::Dot(dot) => {
2475            quote_identifiers_recursive(&mut dot.this, reserved_words);
2476            maybe_quote(&mut dot.field, reserved_words);
2477        }
2478
2479        Expression::ScopeResolution(sr) => {
2480            if let Some(ref mut this) = sr.this {
2481                quote_identifiers_recursive(this, reserved_words);
2482            }
2483            quote_identifiers_recursive(&mut sr.expression, reserved_words);
2484        }
2485
2486        // ── Lateral ───────────────────────────────────────────────
2487        Expression::Lateral(lat) => {
2488            quote_identifiers_recursive(&mut lat.this, reserved_words);
2489            // lat.alias is Option<String>, not Identifier, so we skip it
2490        }
2491
2492        // ── DPipe (|| concatenation) ──────────────────────────────
2493        Expression::DPipe(dpipe) => {
2494            quote_identifiers_recursive(&mut dpipe.this, reserved_words);
2495            quote_identifiers_recursive(&mut dpipe.expression, reserved_words);
2496        }
2497
2498        // ── Merge ─────────────────────────────────────────────────
2499        Expression::Merge(merge) => {
2500            quote_identifiers_recursive(&mut merge.this, reserved_words);
2501            quote_identifiers_recursive(&mut merge.using, reserved_words);
2502            if let Some(ref mut on) = merge.on {
2503                quote_identifiers_recursive(on, reserved_words);
2504            }
2505            if let Some(ref mut whens) = merge.whens {
2506                quote_identifiers_recursive(whens, reserved_words);
2507            }
2508            if let Some(ref mut with) = merge.with_ {
2509                quote_identifiers_recursive(with, reserved_words);
2510            }
2511            if let Some(ref mut ret) = merge.returning {
2512                quote_identifiers_recursive(ret, reserved_words);
2513            }
2514        }
2515
2516        // ── LateralView (standalone) ──────────────────────────────
2517        Expression::LateralView(lv) => {
2518            quote_lateral_view(lv, reserved_words);
2519        }
2520
2521        // ── Anonymous (generic function) ──────────────────────────
2522        Expression::Anonymous(anon) => {
2523            quote_identifiers_recursive(&mut anon.this, reserved_words);
2524            for e in &mut anon.expressions {
2525                quote_identifiers_recursive(e, reserved_words);
2526            }
2527        }
2528
2529        // ── Filter (e.g., FILTER(WHERE ...)) ──────────────────────
2530        Expression::Filter(filter) => {
2531            quote_identifiers_recursive(&mut filter.this, reserved_words);
2532            quote_identifiers_recursive(&mut filter.expression, reserved_words);
2533        }
2534
2535        // ── Returning ─────────────────────────────────────────────
2536        Expression::Returning(ret) => {
2537            for e in &mut ret.expressions {
2538                quote_identifiers_recursive(e, reserved_words);
2539            }
2540        }
2541
2542        // ── BracedWildcard ────────────────────────────────────────
2543        Expression::BracedWildcard(inner) => {
2544            quote_identifiers_recursive(inner, reserved_words);
2545        }
2546
2547        // ── ReturnStmt ────────────────────────────────────────────
2548        Expression::ReturnStmt(inner) => {
2549            quote_identifiers_recursive(inner, reserved_words);
2550        }
2551
2552        // ── Leaf nodes that never contain identifiers ─────────────
2553        Expression::Literal(_)
2554        | Expression::Boolean(_)
2555        | Expression::Null(_)
2556        | Expression::DataType(_)
2557        | Expression::Raw(_)
2558        | Expression::Placeholder(_)
2559        | Expression::CurrentDate(_)
2560        | Expression::CurrentTime(_)
2561        | Expression::CurrentTimestamp(_)
2562        | Expression::CurrentTimestampLTZ(_)
2563        | Expression::SessionUser(_)
2564        | Expression::RowNumber(_)
2565        | Expression::Rank(_)
2566        | Expression::DenseRank(_)
2567        | Expression::PercentRank(_)
2568        | Expression::CumeDist(_)
2569        | Expression::Random(_)
2570        | Expression::Pi(_)
2571        | Expression::JSONPathRoot(_) => {
2572            // Nothing to do – these are leaves or do not contain identifiers
2573        }
2574
2575        // ── Catch-all: many expression variants follow common patterns.
2576        // Rather than listing every single variant, we leave them unchanged.
2577        // The key identifier-bearing variants are covered above.
2578        _ => {}
2579    }
2580}
2581
2582/// Helper: quote identifiers in a Join.
2583fn quote_join(join: &mut Join, reserved_words: &HashSet<&str>) {
2584    quote_identifiers_recursive(&mut join.this, reserved_words);
2585    if let Some(ref mut on) = join.on {
2586        quote_identifiers_recursive(on, reserved_words);
2587    }
2588    for id in &mut join.using {
2589        maybe_quote(id, reserved_words);
2590    }
2591    if let Some(ref mut mc) = join.match_condition {
2592        quote_identifiers_recursive(mc, reserved_words);
2593    }
2594    for piv in &mut join.pivots {
2595        quote_identifiers_recursive(piv, reserved_words);
2596    }
2597}
2598
2599/// Helper: quote identifiers in a WITH clause.
2600fn quote_with(with: &mut With, reserved_words: &HashSet<&str>) {
2601    for cte in &mut with.ctes {
2602        maybe_quote(&mut cte.alias, reserved_words);
2603        for c in &mut cte.columns {
2604            maybe_quote(c, reserved_words);
2605        }
2606        for k in &mut cte.key_expressions {
2607            maybe_quote(k, reserved_words);
2608        }
2609        quote_identifiers_recursive(&mut cte.this, reserved_words);
2610    }
2611}
2612
2613/// Helper: quote identifiers in an Over clause.
2614fn quote_over(over: &mut Over, reserved_words: &HashSet<&str>) {
2615    if let Some(ref mut wn) = over.window_name {
2616        maybe_quote(wn, reserved_words);
2617    }
2618    for e in &mut over.partition_by {
2619        quote_identifiers_recursive(e, reserved_words);
2620    }
2621    for o in &mut over.order_by {
2622        quote_identifiers_recursive(&mut o.this, reserved_words);
2623    }
2624    if let Some(ref mut alias) = over.alias {
2625        maybe_quote(alias, reserved_words);
2626    }
2627}
2628
2629/// Helper: quote identifiers in a TableRef (used by DML statements).
2630fn quote_table_ref(table_ref: &mut TableRef, reserved_words: &HashSet<&str>) {
2631    maybe_quote(&mut table_ref.name, reserved_words);
2632    if let Some(ref mut schema) = table_ref.schema {
2633        maybe_quote(schema, reserved_words);
2634    }
2635    if let Some(ref mut catalog) = table_ref.catalog {
2636        maybe_quote(catalog, reserved_words);
2637    }
2638    if let Some(ref mut alias) = table_ref.alias {
2639        maybe_quote(alias, reserved_words);
2640    }
2641    for ca in &mut table_ref.column_aliases {
2642        maybe_quote(ca, reserved_words);
2643    }
2644    for p in &mut table_ref.partitions {
2645        maybe_quote(p, reserved_words);
2646    }
2647    for h in &mut table_ref.hints {
2648        quote_identifiers_recursive(h, reserved_words);
2649    }
2650}
2651
2652/// Helper: quote identifiers in a LateralView.
2653fn quote_lateral_view(lv: &mut LateralView, reserved_words: &HashSet<&str>) {
2654    quote_identifiers_recursive(&mut lv.this, reserved_words);
2655    if let Some(ref mut ta) = lv.table_alias {
2656        maybe_quote(ta, reserved_words);
2657    }
2658    for ca in &mut lv.column_aliases {
2659        maybe_quote(ca, reserved_words);
2660    }
2661}
2662
2663/// Quote identifiers that need quoting based on dialect rules.
2664///
2665/// Walks the entire AST recursively and sets `quoted = true` on any
2666/// `Identifier` that:
2667/// - contains special characters (anything not `[a-zA-Z0-9_]`)
2668/// - starts with a digit
2669/// - is a SQL reserved word for the given dialect
2670///
2671/// The function takes ownership of the expression, mutates a clone,
2672/// and returns the modified version.
2673pub fn quote_identifiers(expression: Expression, dialect: Option<DialectType>) -> Expression {
2674    let reserved_words = get_reserved_words(dialect);
2675    let mut result = expression;
2676    quote_identifiers_recursive(&mut result, &reserved_words);
2677    result
2678}
2679
2680/// Pushdown CTE alias columns into the projection.
2681///
2682/// This is useful for dialects like Snowflake where CTE alias columns
2683/// can be referenced in HAVING.
2684pub fn pushdown_cte_alias_columns(_scope: &Scope) {
2685    // Kept for API compatibility. The mutating implementation is applied within
2686    // `qualify_columns` where AST ownership is available.
2687}
2688
2689fn pushdown_cte_alias_columns_with(with: &mut With) {
2690    for cte in &mut with.ctes {
2691        if cte.columns.is_empty() {
2692            continue;
2693        }
2694
2695        if let Expression::Select(select) = &mut cte.this {
2696            let mut next_expressions = Vec::with_capacity(select.expressions.len());
2697
2698            for (i, projection) in select.expressions.iter().enumerate() {
2699                let Some(alias_name) = cte.columns.get(i) else {
2700                    next_expressions.push(projection.clone());
2701                    continue;
2702                };
2703
2704                match projection {
2705                    Expression::Alias(existing) => {
2706                        let mut aliased = existing.clone();
2707                        aliased.alias = alias_name.clone();
2708                        next_expressions.push(Expression::Alias(aliased));
2709                    }
2710                    _ => {
2711                        next_expressions.push(create_alias(projection.clone(), &alias_name.name));
2712                    }
2713                }
2714            }
2715
2716            select.expressions = next_expressions;
2717        }
2718    }
2719}
2720
2721// ============================================================================
2722// Helper functions
2723// ============================================================================
2724
2725/// Get all column references in a scope
2726fn get_scope_columns(scope: &Scope) -> Vec<ColumnRef> {
2727    let mut columns = Vec::new();
2728    collect_columns(&scope.expression, &mut columns);
2729    columns
2730}
2731
2732/// Column reference for tracking
2733#[derive(Debug, Clone)]
2734struct ColumnRef {
2735    table: Option<String>,
2736    name: String,
2737}
2738
2739/// Recursively collect column references from an expression
2740fn collect_columns(expr: &Expression, columns: &mut Vec<ColumnRef>) {
2741    match expr {
2742        Expression::Column(col) => {
2743            columns.push(ColumnRef {
2744                table: col.table.as_ref().map(|t| t.name.clone()),
2745                name: col.name.name.clone(),
2746            });
2747        }
2748        Expression::Select(select) => {
2749            for e in &select.expressions {
2750                collect_columns(e, columns);
2751            }
2752            if let Some(from) = &select.from {
2753                for e in &from.expressions {
2754                    collect_columns(e, columns);
2755                }
2756            }
2757            if let Some(where_clause) = &select.where_clause {
2758                collect_columns(&where_clause.this, columns);
2759            }
2760            if let Some(group_by) = &select.group_by {
2761                for e in &group_by.expressions {
2762                    collect_columns(e, columns);
2763                }
2764            }
2765            if let Some(having) = &select.having {
2766                collect_columns(&having.this, columns);
2767            }
2768            if let Some(order_by) = &select.order_by {
2769                for o in &order_by.expressions {
2770                    collect_columns(&o.this, columns);
2771                }
2772            }
2773            for join in &select.joins {
2774                collect_columns(&join.this, columns);
2775                if let Some(on) = &join.on {
2776                    collect_columns(on, columns);
2777                }
2778            }
2779        }
2780        Expression::Alias(alias) => {
2781            collect_columns(&alias.this, columns);
2782        }
2783        Expression::Function(func) => {
2784            for arg in &func.args {
2785                collect_columns(arg, columns);
2786            }
2787        }
2788        Expression::AggregateFunction(agg) => {
2789            for arg in &agg.args {
2790                collect_columns(arg, columns);
2791            }
2792        }
2793        Expression::And(bin)
2794        | Expression::Or(bin)
2795        | Expression::Eq(bin)
2796        | Expression::Neq(bin)
2797        | Expression::Lt(bin)
2798        | Expression::Lte(bin)
2799        | Expression::Gt(bin)
2800        | Expression::Gte(bin)
2801        | Expression::Add(bin)
2802        | Expression::Sub(bin)
2803        | Expression::Mul(bin)
2804        | Expression::Div(bin) => {
2805            collect_columns(&bin.left, columns);
2806            collect_columns(&bin.right, columns);
2807        }
2808        Expression::Not(unary) | Expression::Neg(unary) => {
2809            collect_columns(&unary.this, columns);
2810        }
2811        Expression::Paren(paren) => {
2812            collect_columns(&paren.this, columns);
2813        }
2814        Expression::Case(case) => {
2815            if let Some(operand) = &case.operand {
2816                collect_columns(operand, columns);
2817            }
2818            for (when, then) in &case.whens {
2819                collect_columns(when, columns);
2820                collect_columns(then, columns);
2821            }
2822            if let Some(else_) = &case.else_ {
2823                collect_columns(else_, columns);
2824            }
2825        }
2826        Expression::Cast(cast) => {
2827            collect_columns(&cast.this, columns);
2828        }
2829        Expression::In(in_expr) => {
2830            collect_columns(&in_expr.this, columns);
2831            for e in &in_expr.expressions {
2832                collect_columns(e, columns);
2833            }
2834            if let Some(query) = &in_expr.query {
2835                collect_columns(query, columns);
2836            }
2837        }
2838        Expression::Between(between) => {
2839            collect_columns(&between.this, columns);
2840            collect_columns(&between.low, columns);
2841            collect_columns(&between.high, columns);
2842        }
2843        Expression::Subquery(subquery) => {
2844            collect_columns(&subquery.this, columns);
2845        }
2846        _ => {}
2847    }
2848}
2849
2850/// Get unqualified columns in a scope
2851fn get_unqualified_columns(scope: &Scope) -> Vec<ColumnRef> {
2852    get_scope_columns(scope)
2853        .into_iter()
2854        .filter(|c| c.table.is_none())
2855        .collect()
2856}
2857
2858/// Get external columns (columns not resolvable in current scope)
2859fn get_external_columns(scope: &Scope) -> Vec<ColumnRef> {
2860    let source_names: HashSet<_> = scope.sources.keys().cloned().collect();
2861
2862    get_scope_columns(scope)
2863        .into_iter()
2864        .filter(|c| {
2865            if let Some(table) = &c.table {
2866                !source_names.contains(table)
2867            } else {
2868                false
2869            }
2870        })
2871        .collect()
2872}
2873
2874/// Check if a scope represents a correlated subquery
2875fn is_correlated_subquery(scope: &Scope) -> bool {
2876    scope.can_be_correlated && !get_external_columns(scope).is_empty()
2877}
2878
2879/// Check if a column represents a star (e.g., table.*)
2880fn is_star_column(col: &Column) -> bool {
2881    col.name.name == "*"
2882}
2883
2884/// Create a qualified column expression
2885fn create_qualified_column(name: &str, table: Option<&str>) -> Expression {
2886    Expression::boxed_column(Column {
2887        name: Identifier::new(name),
2888        table: table.map(Identifier::new),
2889        join_mark: false,
2890        trailing_comments: vec![],
2891        span: None,
2892        inferred_type: None,
2893    })
2894}
2895
2896/// Create an alias expression
2897fn create_alias(expr: Expression, alias_name: &str) -> Expression {
2898    Expression::Alias(Box::new(Alias {
2899        this: expr,
2900        alias: Identifier::new(alias_name),
2901        column_aliases: vec![],
2902        alias_explicit_as: false,
2903        alias_keyword: None,
2904        pre_alias_comments: vec![],
2905        trailing_comments: vec![],
2906        inferred_type: None,
2907    }))
2908}
2909
2910/// Get the output name for an expression
2911fn get_output_name(expr: &Expression) -> Option<String> {
2912    match expr {
2913        Expression::Column(col) => Some(col.name.name.clone()),
2914        Expression::Alias(alias) => Some(alias.alias.name.clone()),
2915        Expression::Identifier(id) => Some(id.name.clone()),
2916        _ => None,
2917    }
2918}
2919
2920#[cfg(test)]
2921mod tests {
2922    use super::*;
2923    use crate::expressions::DataType;
2924    use crate::generator::Generator;
2925    use crate::parser::Parser;
2926    use crate::scope::build_scope;
2927    use crate::{MappingSchema, Schema};
2928
2929    fn gen(expr: &Expression) -> String {
2930        Generator::new().generate(expr).unwrap()
2931    }
2932
2933    fn parse(sql: &str) -> Expression {
2934        Parser::parse_sql(sql).expect("Failed to parse")[0].clone()
2935    }
2936
2937    #[test]
2938    fn test_qualify_columns_options() {
2939        let options = QualifyColumnsOptions::new()
2940            .with_expand_alias_refs(true)
2941            .with_expand_stars(false)
2942            .with_dialect(DialectType::PostgreSQL)
2943            .with_allow_partial(true);
2944
2945        assert!(options.expand_alias_refs);
2946        assert!(!options.expand_stars);
2947        assert_eq!(options.dialect, Some(DialectType::PostgreSQL));
2948        assert!(options.allow_partial_qualification);
2949    }
2950
2951    #[test]
2952    fn test_get_scope_columns() {
2953        let expr = parse("SELECT a, b FROM t WHERE c = 1");
2954        let scope = build_scope(&expr);
2955        let columns = get_scope_columns(&scope);
2956
2957        assert!(columns.iter().any(|c| c.name == "a"));
2958        assert!(columns.iter().any(|c| c.name == "b"));
2959        assert!(columns.iter().any(|c| c.name == "c"));
2960    }
2961
2962    #[test]
2963    fn test_get_unqualified_columns() {
2964        let expr = parse("SELECT t.a, b FROM t");
2965        let scope = build_scope(&expr);
2966        let unqualified = get_unqualified_columns(&scope);
2967
2968        // Only 'b' should be unqualified
2969        assert!(unqualified.iter().any(|c| c.name == "b"));
2970        assert!(!unqualified.iter().any(|c| c.name == "a"));
2971    }
2972
2973    #[test]
2974    fn test_is_star_column() {
2975        let col = Column {
2976            name: Identifier::new("*"),
2977            table: Some(Identifier::new("t")),
2978            join_mark: false,
2979            trailing_comments: vec![],
2980            span: None,
2981            inferred_type: None,
2982        };
2983        assert!(is_star_column(&col));
2984
2985        let col2 = Column {
2986            name: Identifier::new("id"),
2987            table: None,
2988            join_mark: false,
2989            trailing_comments: vec![],
2990            span: None,
2991            inferred_type: None,
2992        };
2993        assert!(!is_star_column(&col2));
2994    }
2995
2996    #[test]
2997    fn test_create_qualified_column() {
2998        let expr = create_qualified_column("id", Some("users"));
2999        let sql = gen(&expr);
3000        assert!(sql.contains("users"));
3001        assert!(sql.contains("id"));
3002    }
3003
3004    #[test]
3005    fn test_create_alias() {
3006        let col = Expression::boxed_column(Column {
3007            name: Identifier::new("value"),
3008            table: None,
3009            join_mark: false,
3010            trailing_comments: vec![],
3011            span: None,
3012            inferred_type: None,
3013        });
3014        let aliased = create_alias(col, "total");
3015        let sql = gen(&aliased);
3016        assert!(sql.contains("AS") || sql.contains("total"));
3017    }
3018
3019    #[test]
3020    fn test_qualify_columns_normalizes_struct_field_access_issue_408() {
3021        let struct_type = DataType::Struct {
3022            fields: vec![crate::expressions::StructField::new(
3023                "field_value".into(),
3024                DataType::Text,
3025            )],
3026            nested: true,
3027        };
3028        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
3029        schema
3030            .add_table(
3031                "source_table",
3032                &[
3033                    ("composite_value".into(), struct_type.clone()),
3034                    (
3035                        "nested_items".into(),
3036                        DataType::Array {
3037                            element_type: Box::new(struct_type),
3038                            dimension: None,
3039                        },
3040                    ),
3041                ],
3042                None,
3043            )
3044            .expect("schema setup");
3045
3046        let cases = [
3047            (
3048                "SELECT composite_value.field_value AS output_value FROM source_table",
3049                "source_table",
3050                "composite_value",
3051            ),
3052            (
3053                "SELECT item.field_value AS output_value FROM source_table s \
3054                 CROSS JOIN UNNEST(s.nested_items) AS expanded(item)",
3055                "expanded",
3056                "item",
3057            ),
3058        ];
3059
3060        for (sql, expected_table, expected_column) in cases {
3061            let qualified = qualify_columns(
3062                parse(sql),
3063                &schema,
3064                &QualifyColumnsOptions::new().with_dialect(DialectType::DuckDB),
3065            )
3066            .unwrap_or_else(|error| panic!("qualification failed for {sql:?}: {error}"));
3067            let Expression::Select(select) = qualified else {
3068                panic!("expected SELECT");
3069            };
3070            let Expression::Alias(alias) = &select.expressions[0] else {
3071                panic!("expected aliased projection");
3072            };
3073            let Expression::Dot(dot) = &alias.this else {
3074                panic!("expected normalized Dot, got {:?}", alias.this);
3075            };
3076            let Expression::Column(base) = &dot.this else {
3077                panic!("expected column-backed Dot");
3078            };
3079
3080            assert_eq!(
3081                base.table.as_ref().map(|table| table.name.as_str()),
3082                Some(expected_table)
3083            );
3084            assert_eq!(base.name.name, expected_column);
3085            assert_eq!(dot.field.name, "field_value");
3086        }
3087    }
3088
3089    #[test]
3090    fn test_validate_qualify_columns_success() {
3091        // All columns qualified
3092        let expr = parse("SELECT t.a, t.b FROM t");
3093        let result = validate_qualify_columns(&expr);
3094        // This may or may not error depending on scope analysis
3095        // The test verifies the function runs without panic
3096        let _ = result;
3097    }
3098
3099    #[test]
3100    fn test_collect_columns_nested() {
3101        let expr = parse("SELECT a + b, c FROM t WHERE d > 0 GROUP BY e HAVING f = 1");
3102        let mut columns = Vec::new();
3103        collect_columns(&expr, &mut columns);
3104
3105        let names: Vec<_> = columns.iter().map(|c| c.name.as_str()).collect();
3106        assert!(names.contains(&"a"));
3107        assert!(names.contains(&"b"));
3108        assert!(names.contains(&"c"));
3109        assert!(names.contains(&"d"));
3110        assert!(names.contains(&"e"));
3111        assert!(names.contains(&"f"));
3112    }
3113
3114    #[test]
3115    fn test_collect_columns_in_case() {
3116        let expr = parse("SELECT CASE WHEN a = 1 THEN b ELSE c END FROM t");
3117        let mut columns = Vec::new();
3118        collect_columns(&expr, &mut columns);
3119
3120        let names: Vec<_> = columns.iter().map(|c| c.name.as_str()).collect();
3121        assert!(names.contains(&"a"));
3122        assert!(names.contains(&"b"));
3123        assert!(names.contains(&"c"));
3124    }
3125
3126    #[test]
3127    fn test_collect_columns_in_subquery() {
3128        let expr = parse("SELECT a FROM t WHERE b IN (SELECT c FROM s)");
3129        let mut columns = Vec::new();
3130        collect_columns(&expr, &mut columns);
3131
3132        let names: Vec<_> = columns.iter().map(|c| c.name.as_str()).collect();
3133        assert!(names.contains(&"a"));
3134        assert!(names.contains(&"b"));
3135        assert!(names.contains(&"c"));
3136    }
3137
3138    #[test]
3139    fn test_qualify_outputs_basic() {
3140        let expr = parse("SELECT a, b + c FROM t");
3141        let scope = build_scope(&expr);
3142        let result = qualify_outputs(&scope);
3143        assert!(result.is_ok());
3144    }
3145
3146    #[test]
3147    fn test_qualify_columns_expands_star_with_schema() {
3148        let expr = parse("SELECT * FROM users");
3149
3150        let mut schema = MappingSchema::new();
3151        schema
3152            .add_table(
3153                "users",
3154                &[
3155                    (
3156                        "id".to_string(),
3157                        DataType::Int {
3158                            length: None,
3159                            integer_spelling: false,
3160                        },
3161                    ),
3162                    ("name".to_string(), DataType::Text),
3163                    ("email".to_string(), DataType::Text),
3164                ],
3165                None,
3166            )
3167            .expect("schema setup");
3168
3169        let result =
3170            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3171        let sql = gen(&result);
3172
3173        assert!(!sql.contains("SELECT *"));
3174        assert!(sql.contains("users.id"));
3175        assert!(sql.contains("users.name"));
3176        assert!(sql.contains("users.email"));
3177    }
3178
3179    #[test]
3180    fn test_qualify_columns_expands_group_by_positions() {
3181        let expr = parse("SELECT a, b FROM t GROUP BY 1, 2");
3182
3183        let mut schema = MappingSchema::new();
3184        schema
3185            .add_table(
3186                "t",
3187                &[
3188                    (
3189                        "a".to_string(),
3190                        DataType::Int {
3191                            length: None,
3192                            integer_spelling: false,
3193                        },
3194                    ),
3195                    (
3196                        "b".to_string(),
3197                        DataType::Int {
3198                            length: None,
3199                            integer_spelling: false,
3200                        },
3201                    ),
3202                ],
3203                None,
3204            )
3205            .expect("schema setup");
3206
3207        let result =
3208            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3209        let sql = gen(&result);
3210
3211        assert!(!sql.contains("GROUP BY 1"));
3212        assert!(!sql.contains("GROUP BY 2"));
3213        assert!(sql.contains("GROUP BY"));
3214        assert!(sql.contains("t.a"));
3215        assert!(sql.contains("t.b"));
3216    }
3217
3218    // ======================================================================
3219    // USING expansion tests
3220    // ======================================================================
3221
3222    #[test]
3223    fn test_expand_using_simple() {
3224        // Already-qualified column: USING→ON rewrite but no COALESCE needed
3225        let expr = parse("SELECT x.b FROM x JOIN y USING (b)");
3226
3227        let mut schema = MappingSchema::new();
3228        schema
3229            .add_table(
3230                "x",
3231                &[
3232                    ("a".to_string(), DataType::BigInt { length: None }),
3233                    ("b".to_string(), DataType::BigInt { length: None }),
3234                ],
3235                None,
3236            )
3237            .expect("schema setup");
3238        schema
3239            .add_table(
3240                "y",
3241                &[
3242                    ("b".to_string(), DataType::BigInt { length: None }),
3243                    ("c".to_string(), DataType::BigInt { length: None }),
3244                ],
3245                None,
3246            )
3247            .expect("schema setup");
3248
3249        let result =
3250            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3251        let sql = gen(&result);
3252
3253        // USING should be replaced with ON
3254        assert!(
3255            !sql.contains("USING"),
3256            "USING should be replaced with ON: {sql}"
3257        );
3258        assert!(
3259            sql.contains("ON x.b = y.b"),
3260            "ON condition should be x.b = y.b: {sql}"
3261        );
3262        // x.b in SELECT should remain as-is (already qualified)
3263        assert!(sql.contains("SELECT x.b"), "SELECT should keep x.b: {sql}");
3264    }
3265
3266    #[test]
3267    fn test_expand_using_unqualified_coalesce() {
3268        // Unqualified USING column in SELECT should become COALESCE
3269        let expr = parse("SELECT b FROM x JOIN y USING(b)");
3270
3271        let mut schema = MappingSchema::new();
3272        schema
3273            .add_table(
3274                "x",
3275                &[
3276                    ("a".to_string(), DataType::BigInt { length: None }),
3277                    ("b".to_string(), DataType::BigInt { length: None }),
3278                ],
3279                None,
3280            )
3281            .expect("schema setup");
3282        schema
3283            .add_table(
3284                "y",
3285                &[
3286                    ("b".to_string(), DataType::BigInt { length: None }),
3287                    ("c".to_string(), DataType::BigInt { length: None }),
3288                ],
3289                None,
3290            )
3291            .expect("schema setup");
3292
3293        let result =
3294            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3295        let sql = gen(&result);
3296
3297        assert!(
3298            sql.contains("COALESCE(x.b, y.b)"),
3299            "Unqualified USING column should become COALESCE: {sql}"
3300        );
3301        assert!(
3302            sql.contains("AS b"),
3303            "COALESCE should be aliased as 'b': {sql}"
3304        );
3305        assert!(
3306            sql.contains("ON x.b = y.b"),
3307            "ON condition should be generated: {sql}"
3308        );
3309    }
3310
3311    #[test]
3312    fn test_expand_using_with_where() {
3313        // USING column in WHERE should become COALESCE
3314        let expr = parse("SELECT b FROM x JOIN y USING(b) WHERE b = 1");
3315
3316        let mut schema = MappingSchema::new();
3317        schema
3318            .add_table(
3319                "x",
3320                &[("b".to_string(), DataType::BigInt { length: None })],
3321                None,
3322            )
3323            .expect("schema setup");
3324        schema
3325            .add_table(
3326                "y",
3327                &[("b".to_string(), DataType::BigInt { length: None })],
3328                None,
3329            )
3330            .expect("schema setup");
3331
3332        let result =
3333            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3334        let sql = gen(&result);
3335
3336        assert!(
3337            sql.contains("WHERE COALESCE(x.b, y.b)"),
3338            "WHERE should use COALESCE for USING column: {sql}"
3339        );
3340    }
3341
3342    #[test]
3343    fn test_expand_using_multi_join() {
3344        // Three-way join with same USING column
3345        let expr = parse("SELECT b FROM x JOIN y USING(b) JOIN z USING(b)");
3346
3347        let mut schema = MappingSchema::new();
3348        for table in &["x", "y", "z"] {
3349            schema
3350                .add_table(
3351                    table,
3352                    &[("b".to_string(), DataType::BigInt { length: None })],
3353                    None,
3354                )
3355                .expect("schema setup");
3356        }
3357
3358        let result =
3359            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3360        let sql = gen(&result);
3361
3362        // SELECT should have 3-table COALESCE
3363        assert!(
3364            sql.contains("COALESCE(x.b, y.b, z.b)"),
3365            "Should have 3-table COALESCE: {sql}"
3366        );
3367        // First join: simple ON
3368        assert!(
3369            sql.contains("ON x.b = y.b"),
3370            "First join ON condition: {sql}"
3371        );
3372    }
3373
3374    #[test]
3375    fn test_expand_using_multi_column() {
3376        // Two USING columns
3377        let expr = parse("SELECT b, c FROM y JOIN z USING(b, c)");
3378
3379        let mut schema = MappingSchema::new();
3380        schema
3381            .add_table(
3382                "y",
3383                &[
3384                    ("b".to_string(), DataType::BigInt { length: None }),
3385                    ("c".to_string(), DataType::BigInt { length: None }),
3386                ],
3387                None,
3388            )
3389            .expect("schema setup");
3390        schema
3391            .add_table(
3392                "z",
3393                &[
3394                    ("b".to_string(), DataType::BigInt { length: None }),
3395                    ("c".to_string(), DataType::BigInt { length: None }),
3396                ],
3397                None,
3398            )
3399            .expect("schema setup");
3400
3401        let result =
3402            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3403        let sql = gen(&result);
3404
3405        assert!(
3406            sql.contains("COALESCE(y.b, z.b)"),
3407            "column 'b' should get COALESCE: {sql}"
3408        );
3409        assert!(
3410            sql.contains("COALESCE(y.c, z.c)"),
3411            "column 'c' should get COALESCE: {sql}"
3412        );
3413        // ON should have both conditions ANDed
3414        assert!(
3415            sql.contains("y.b = z.b") && sql.contains("y.c = z.c"),
3416            "ON should have both equality conditions: {sql}"
3417        );
3418    }
3419
3420    #[test]
3421    fn test_expand_using_star() {
3422        // SELECT * should deduplicate USING columns
3423        let expr = parse("SELECT * FROM x JOIN y USING(b)");
3424
3425        let mut schema = MappingSchema::new();
3426        schema
3427            .add_table(
3428                "x",
3429                &[
3430                    ("a".to_string(), DataType::BigInt { length: None }),
3431                    ("b".to_string(), DataType::BigInt { length: None }),
3432                ],
3433                None,
3434            )
3435            .expect("schema setup");
3436        schema
3437            .add_table(
3438                "y",
3439                &[
3440                    ("b".to_string(), DataType::BigInt { length: None }),
3441                    ("c".to_string(), DataType::BigInt { length: None }),
3442                ],
3443                None,
3444            )
3445            .expect("schema setup");
3446
3447        let result =
3448            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3449        let sql = gen(&result);
3450
3451        // b should appear once as COALESCE
3452        assert!(
3453            sql.contains("COALESCE(x.b, y.b) AS b"),
3454            "USING column should be COALESCE in star expansion: {sql}"
3455        );
3456        // a and c should be normal qualified columns
3457        assert!(sql.contains("x.a"), "non-USING column a from x: {sql}");
3458        assert!(sql.contains("y.c"), "non-USING column c from y: {sql}");
3459        // b should only appear once (not duplicated from both tables)
3460        let coalesce_count = sql.matches("COALESCE").count();
3461        assert_eq!(
3462            coalesce_count, 1,
3463            "b should appear only once as COALESCE: {sql}"
3464        );
3465    }
3466
3467    #[test]
3468    fn test_expand_using_table_star() {
3469        // table.* with USING column
3470        let expr = parse("SELECT x.* FROM x JOIN y USING(b)");
3471
3472        let mut schema = MappingSchema::new();
3473        schema
3474            .add_table(
3475                "x",
3476                &[
3477                    ("a".to_string(), DataType::BigInt { length: None }),
3478                    ("b".to_string(), DataType::BigInt { length: None }),
3479                ],
3480                None,
3481            )
3482            .expect("schema setup");
3483        schema
3484            .add_table(
3485                "y",
3486                &[
3487                    ("b".to_string(), DataType::BigInt { length: None }),
3488                    ("c".to_string(), DataType::BigInt { length: None }),
3489                ],
3490                None,
3491            )
3492            .expect("schema setup");
3493
3494        let result =
3495            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3496        let sql = gen(&result);
3497
3498        // b should become COALESCE (since x participates in USING for b)
3499        assert!(
3500            sql.contains("COALESCE(x.b, y.b)"),
3501            "USING column from x.* should become COALESCE: {sql}"
3502        );
3503        assert!(sql.contains("x.a"), "non-USING column a: {sql}");
3504    }
3505
3506    #[test]
3507    fn test_expand_natural_join_with_derived_table() {
3508        let expr = parse(
3509            "SELECT shared_key AS output_key FROM source_table \
3510             NATURAL JOIN (SELECT shared_key FROM source_table) AS derived",
3511        );
3512
3513        let mut schema = MappingSchema::new();
3514        schema
3515            .add_table(
3516                "source_table",
3517                &[(
3518                    "shared_key".to_string(),
3519                    DataType::VarChar {
3520                        length: None,
3521                        parenthesized_length: false,
3522                    },
3523                )],
3524                None,
3525            )
3526            .expect("schema setup");
3527
3528        let result = qualify_columns(
3529            expr,
3530            &schema,
3531            &QualifyColumnsOptions::new().with_dialect(DialectType::DuckDB),
3532        )
3533        .expect("qualify");
3534        let sql = gen(&result);
3535
3536        assert!(!sql.contains("NATURAL"), "NATURAL should expand: {sql}");
3537        assert!(
3538            sql.contains("ON source_table.shared_key = derived.shared_key"),
3539            "common column should become an equality condition: {sql}"
3540        );
3541        assert!(
3542            sql.contains("COALESCE(source_table.shared_key, derived.shared_key) AS output_key"),
3543            "merged projection should preserve both sources: {sql}"
3544        );
3545    }
3546
3547    #[test]
3548    fn test_expand_natural_join_star_and_chained_sources() {
3549        let expr = parse("SELECT * FROM x NATURAL JOIN y NATURAL JOIN z");
3550
3551        let mut schema = MappingSchema::new();
3552        schema
3553            .add_table(
3554                "x",
3555                &[
3556                    ("a".to_string(), DataType::BigInt { length: None }),
3557                    ("shared".to_string(), DataType::BigInt { length: None }),
3558                ],
3559                None,
3560            )
3561            .expect("schema setup");
3562        schema
3563            .add_table(
3564                "y",
3565                &[
3566                    ("shared".to_string(), DataType::BigInt { length: None }),
3567                    ("b".to_string(), DataType::BigInt { length: None }),
3568                ],
3569                None,
3570            )
3571            .expect("schema setup");
3572        schema
3573            .add_table(
3574                "z",
3575                &[
3576                    ("shared".to_string(), DataType::BigInt { length: None }),
3577                    ("c".to_string(), DataType::BigInt { length: None }),
3578                ],
3579                None,
3580            )
3581            .expect("schema setup");
3582
3583        let result =
3584            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3585        let sql = gen(&result);
3586
3587        assert!(!sql.contains("NATURAL"), "both joins should expand: {sql}");
3588        assert!(sql.contains("ON x.shared = y.shared"), "first join: {sql}");
3589        assert!(sql.contains("ON x.shared = z.shared"), "second join: {sql}");
3590        assert_eq!(
3591            sql.matches("COALESCE(x.shared, y.shared, z.shared) AS shared")
3592                .count(),
3593            1,
3594            "merged star column should be emitted once: {sql}"
3595        );
3596        assert!(sql.contains("x.a"), "left-only column should remain: {sql}");
3597        assert!(
3598            sql.contains("y.b"),
3599            "middle-only column should remain: {sql}"
3600        );
3601        assert!(
3602            sql.contains("z.c"),
3603            "right-only column should remain: {sql}"
3604        );
3605    }
3606
3607    #[test]
3608    fn test_expand_natural_outer_join_kinds() {
3609        let mut schema = MappingSchema::new();
3610        for table in ["x", "y"] {
3611            schema
3612                .add_table(
3613                    table,
3614                    &[("shared".to_string(), DataType::BigInt { length: None })],
3615                    None,
3616                )
3617                .expect("schema setup");
3618        }
3619
3620        for (input_kind, output_kind) in [
3621            ("NATURAL LEFT JOIN", "LEFT JOIN"),
3622            ("NATURAL RIGHT JOIN", "RIGHT JOIN"),
3623            ("NATURAL FULL JOIN", "FULL JOIN"),
3624        ] {
3625            let expr = parse(&format!("SELECT shared FROM x {input_kind} y"));
3626            let result =
3627                qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3628            let sql = gen(&result);
3629
3630            assert!(
3631                !sql.contains("NATURAL"),
3632                "{input_kind} should expand: {sql}"
3633            );
3634            assert!(sql.contains(output_kind), "join kind should remain: {sql}");
3635            assert!(
3636                sql.contains("ON x.shared = y.shared"),
3637                "join condition should be derived: {sql}"
3638            );
3639        }
3640    }
3641
3642    #[test]
3643    fn test_preserve_natural_join_without_known_common_columns() {
3644        let mut no_common_schema = MappingSchema::new();
3645        no_common_schema
3646            .add_table(
3647                "x",
3648                &[("a".to_string(), DataType::BigInt { length: None })],
3649                None,
3650            )
3651            .expect("schema setup");
3652        no_common_schema
3653            .add_table(
3654                "y",
3655                &[("b".to_string(), DataType::BigInt { length: None })],
3656                None,
3657            )
3658            .expect("schema setup");
3659
3660        let no_common = qualify_columns(
3661            parse("SELECT * FROM x NATURAL JOIN y"),
3662            &no_common_schema,
3663            &QualifyColumnsOptions::new(),
3664        )
3665        .expect("qualify");
3666        assert!(
3667            gen(&no_common).contains("NATURAL JOIN"),
3668            "join without common columns should remain NATURAL"
3669        );
3670
3671        let mut partial_schema = MappingSchema::new();
3672        partial_schema
3673            .add_table(
3674                "x",
3675                &[("a".to_string(), DataType::BigInt { length: None })],
3676                None,
3677            )
3678            .expect("schema setup");
3679        let unknown_right = qualify_columns(
3680            parse("SELECT * FROM x NATURAL JOIN unknown_table"),
3681            &partial_schema,
3682            &QualifyColumnsOptions::new().with_allow_partial(true),
3683        )
3684        .expect("partial qualification");
3685        assert!(
3686            gen(&unknown_right).contains("NATURAL JOIN"),
3687            "join with an unknown schema should remain NATURAL"
3688        );
3689
3690        let unknown_left = qualify_columns(
3691            parse("SELECT * FROM unknown_table NATURAL JOIN x"),
3692            &partial_schema,
3693            &QualifyColumnsOptions::new().with_allow_partial(true),
3694        )
3695        .expect("partial qualification");
3696        assert!(
3697            gen(&unknown_left).contains("NATURAL JOIN"),
3698            "join with an unknown left schema should remain NATURAL"
3699        );
3700
3701        let partially_known_chain = qualify_columns(
3702            parse("SELECT * FROM x NATURAL JOIN unknown_table NATURAL JOIN x AS x2"),
3703            &partial_schema,
3704            &QualifyColumnsOptions::new().with_allow_partial(true),
3705        )
3706        .expect("partial qualification");
3707        assert_eq!(
3708            gen(&partially_known_chain).matches("NATURAL JOIN").count(),
3709            2,
3710            "an unknown earlier source should prevent later inferred join keys"
3711        );
3712    }
3713
3714    #[test]
3715    fn test_qualify_columns_qualified_table_name() {
3716        let expr = parse("SELECT a FROM raw.t1");
3717
3718        let mut schema = MappingSchema::new();
3719        schema
3720            .add_table(
3721                "raw.t1",
3722                &[("a".to_string(), DataType::BigInt { length: None })],
3723                None,
3724            )
3725            .expect("schema setup");
3726
3727        let result =
3728            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3729        let sql = gen(&result);
3730
3731        assert!(
3732            sql.contains("t1.a"),
3733            "column should be qualified with table name: {sql}"
3734        );
3735
3736        // test that columns in agg functions also get qualified
3737        let expr = parse("SELECT MAX(a) FROM raw.t1");
3738        let result =
3739            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3740        let sql = gen(&result);
3741        assert!(
3742            sql.contains("t1.a"),
3743            "column in function should be qualified with table name: {sql}"
3744        );
3745
3746        // test that columns in scalar functions also get qualified
3747        let expr = parse("SELECT ABS(a) FROM raw.t1");
3748        let result =
3749            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3750        let sql = gen(&result);
3751        assert!(
3752            sql.contains("t1.a"),
3753            "column in function should be qualified with table name: {sql}"
3754        );
3755    }
3756
3757    #[test]
3758    fn test_qualify_columns_count_star() {
3759        // COUNT(*) uses Count { this: None } — verify qualify_columns handles it without panic
3760        let expr = parse("SELECT COUNT(*) FROM t1");
3761
3762        let mut schema = MappingSchema::new();
3763        schema
3764            .add_table(
3765                "t1",
3766                &[("id".to_string(), DataType::BigInt { length: None })],
3767                None,
3768            )
3769            .expect("schema setup");
3770
3771        let result =
3772            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3773        let sql = gen(&result);
3774
3775        assert!(
3776            sql.contains("COUNT(*)"),
3777            "COUNT(*) should be preserved: {sql}"
3778        );
3779    }
3780
3781    #[test]
3782    fn test_qualify_columns_correlated_scalar_subquery() {
3783        let expr =
3784            parse("SELECT id, (SELECT AVG(val) FROM t2 WHERE t2.id = t1.id) AS avg_val FROM t1");
3785
3786        let mut schema = MappingSchema::new();
3787        schema
3788            .add_table(
3789                "t1",
3790                &[("id".to_string(), DataType::BigInt { length: None })],
3791                None,
3792            )
3793            .expect("schema setup");
3794        schema
3795            .add_table(
3796                "t2",
3797                &[
3798                    ("id".to_string(), DataType::BigInt { length: None }),
3799                    ("val".to_string(), DataType::BigInt { length: None }),
3800                ],
3801                None,
3802            )
3803            .expect("schema setup");
3804
3805        let result =
3806            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3807        let sql = gen(&result);
3808
3809        assert!(
3810            sql.contains("t1.id"),
3811            "outer column should be qualified: {sql}"
3812        );
3813        assert!(
3814            sql.contains("t2.id"),
3815            "inner column should be qualified: {sql}"
3816        );
3817    }
3818
3819    #[test]
3820    fn test_qualify_columns_correlated_scalar_subquery_unqualified() {
3821        let expr =
3822            parse("SELECT t1_id, (SELECT AVG(val) FROM t2 WHERE t2_id = t1_id) AS avg_val FROM t1");
3823
3824        let mut schema = MappingSchema::new();
3825        schema
3826            .add_table(
3827                "t1",
3828                &[("t1_id".to_string(), DataType::BigInt { length: None })],
3829                None,
3830            )
3831            .expect("schema setup");
3832        schema
3833            .add_table(
3834                "t2",
3835                &[
3836                    ("t2_id".to_string(), DataType::BigInt { length: None }),
3837                    ("val".to_string(), DataType::BigInt { length: None }),
3838                ],
3839                None,
3840            )
3841            .expect("schema setup");
3842
3843        let result =
3844            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3845        let sql = gen(&result);
3846
3847        assert!(
3848            sql.contains("t1.t1_id"),
3849            "outer column should be qualified: {sql}"
3850        );
3851        assert!(
3852            sql.contains("t2.t2_id"),
3853            "inner column should be qualified: {sql}"
3854        );
3855        // Correlated reference t1_id in inner scope should be qualified as t1.t1_id
3856        assert!(
3857            sql.contains("= t1.t1_id"),
3858            "correlated column should be qualified: {sql}"
3859        );
3860    }
3861
3862    #[test]
3863    fn test_qualify_columns_correlated_exists_subquery() {
3864        let expr = parse(
3865            "SELECT o_orderpriority FROM orders \
3866             WHERE EXISTS (SELECT * FROM lineitem WHERE l_orderkey = o_orderkey)",
3867        );
3868
3869        let mut schema = MappingSchema::new();
3870        schema
3871            .add_table(
3872                "orders",
3873                &[
3874                    ("o_orderpriority".to_string(), DataType::Text),
3875                    ("o_orderkey".to_string(), DataType::BigInt { length: None }),
3876                ],
3877                None,
3878            )
3879            .expect("schema setup");
3880        schema
3881            .add_table(
3882                "lineitem",
3883                &[("l_orderkey".to_string(), DataType::BigInt { length: None })],
3884                None,
3885            )
3886            .expect("schema setup");
3887
3888        let result =
3889            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3890        let sql = gen(&result);
3891
3892        assert!(
3893            sql.contains("orders.o_orderpriority"),
3894            "outer column should be qualified: {sql}"
3895        );
3896        assert!(
3897            sql.contains("lineitem.l_orderkey"),
3898            "inner column should be qualified: {sql}"
3899        );
3900        assert!(
3901            sql.contains("orders.o_orderkey"),
3902            "correlated outer column should be qualified: {sql}"
3903        );
3904    }
3905
3906    #[test]
3907    fn test_qualify_columns_rejects_unknown_table() {
3908        let expr = parse("SELECT id FROM t1 WHERE nonexistent.col = 1");
3909
3910        let mut schema = MappingSchema::new();
3911        schema
3912            .add_table(
3913                "t1",
3914                &[("id".to_string(), DataType::BigInt { length: None })],
3915                None,
3916            )
3917            .expect("schema setup");
3918
3919        let result = qualify_columns(expr, &schema, &QualifyColumnsOptions::new());
3920        assert!(
3921            result.is_err(),
3922            "should reject reference to table not in scope or schema"
3923        );
3924    }
3925
3926    // ======================================================================
3927    // quote_identifiers tests
3928    // ======================================================================
3929
3930    #[test]
3931    fn test_needs_quoting_reserved_word() {
3932        let reserved = get_reserved_words(None);
3933        assert!(needs_quoting("select", &reserved));
3934        assert!(needs_quoting("SELECT", &reserved));
3935        assert!(needs_quoting("from", &reserved));
3936        assert!(needs_quoting("WHERE", &reserved));
3937        assert!(needs_quoting("join", &reserved));
3938        assert!(needs_quoting("table", &reserved));
3939    }
3940
3941    #[test]
3942    fn test_needs_quoting_normal_identifiers() {
3943        let reserved = get_reserved_words(None);
3944        assert!(!needs_quoting("foo", &reserved));
3945        assert!(!needs_quoting("my_column", &reserved));
3946        assert!(!needs_quoting("col1", &reserved));
3947        assert!(!needs_quoting("A", &reserved));
3948        assert!(!needs_quoting("_hidden", &reserved));
3949    }
3950
3951    #[test]
3952    fn test_needs_quoting_special_characters() {
3953        let reserved = get_reserved_words(None);
3954        assert!(needs_quoting("my column", &reserved)); // space
3955        assert!(needs_quoting("my-column", &reserved)); // hyphen
3956        assert!(needs_quoting("my.column", &reserved)); // dot
3957        assert!(needs_quoting("col@name", &reserved)); // at sign
3958        assert!(needs_quoting("col#name", &reserved)); // hash
3959    }
3960
3961    #[test]
3962    fn test_needs_quoting_starts_with_digit() {
3963        let reserved = get_reserved_words(None);
3964        assert!(needs_quoting("1col", &reserved));
3965        assert!(needs_quoting("123", &reserved));
3966        assert!(needs_quoting("0_start", &reserved));
3967    }
3968
3969    #[test]
3970    fn test_needs_quoting_empty() {
3971        let reserved = get_reserved_words(None);
3972        assert!(!needs_quoting("", &reserved));
3973    }
3974
3975    #[test]
3976    fn test_maybe_quote_sets_quoted_flag() {
3977        let reserved = get_reserved_words(None);
3978        let mut id = Identifier::new("select");
3979        assert!(!id.quoted);
3980        maybe_quote(&mut id, &reserved);
3981        assert!(id.quoted);
3982    }
3983
3984    #[test]
3985    fn test_maybe_quote_skips_already_quoted() {
3986        let reserved = get_reserved_words(None);
3987        let mut id = Identifier::quoted("myname");
3988        assert!(id.quoted);
3989        maybe_quote(&mut id, &reserved);
3990        assert!(id.quoted); // still quoted
3991        assert_eq!(id.name, "myname"); // name unchanged
3992    }
3993
3994    #[test]
3995    fn test_maybe_quote_skips_star() {
3996        let reserved = get_reserved_words(None);
3997        let mut id = Identifier::new("*");
3998        maybe_quote(&mut id, &reserved);
3999        assert!(!id.quoted); // star should not be quoted
4000    }
4001
4002    #[test]
4003    fn test_maybe_quote_skips_normal() {
4004        let reserved = get_reserved_words(None);
4005        let mut id = Identifier::new("normal_col");
4006        maybe_quote(&mut id, &reserved);
4007        assert!(!id.quoted);
4008    }
4009
4010    #[test]
4011    fn test_quote_identifiers_column_with_reserved_name() {
4012        // A column named "select" should be quoted
4013        let expr = Expression::boxed_column(Column {
4014            name: Identifier::new("select"),
4015            table: None,
4016            join_mark: false,
4017            trailing_comments: vec![],
4018            span: None,
4019            inferred_type: None,
4020        });
4021        let result = quote_identifiers(expr, None);
4022        if let Expression::Column(col) = &result {
4023            assert!(col.name.quoted, "Column named 'select' should be quoted");
4024        } else {
4025            panic!("Expected Column expression");
4026        }
4027    }
4028
4029    #[test]
4030    fn test_quote_identifiers_column_with_special_chars() {
4031        let expr = Expression::boxed_column(Column {
4032            name: Identifier::new("my column"),
4033            table: None,
4034            join_mark: false,
4035            trailing_comments: vec![],
4036            span: None,
4037            inferred_type: None,
4038        });
4039        let result = quote_identifiers(expr, None);
4040        if let Expression::Column(col) = &result {
4041            assert!(col.name.quoted, "Column with space should be quoted");
4042        } else {
4043            panic!("Expected Column expression");
4044        }
4045    }
4046
4047    #[test]
4048    fn test_quote_identifiers_preserves_normal_column() {
4049        let expr = Expression::boxed_column(Column {
4050            name: Identifier::new("normal_col"),
4051            table: Some(Identifier::new("my_table")),
4052            join_mark: false,
4053            trailing_comments: vec![],
4054            span: None,
4055            inferred_type: None,
4056        });
4057        let result = quote_identifiers(expr, None);
4058        if let Expression::Column(col) = &result {
4059            assert!(!col.name.quoted, "Normal column should not be quoted");
4060            assert!(
4061                !col.table.as_ref().unwrap().quoted,
4062                "Normal table should not be quoted"
4063            );
4064        } else {
4065            panic!("Expected Column expression");
4066        }
4067    }
4068
4069    #[test]
4070    fn test_quote_identifiers_table_ref_reserved() {
4071        let expr = Expression::Table(Box::new(TableRef::new("select")));
4072        let result = quote_identifiers(expr, None);
4073        if let Expression::Table(tr) = &result {
4074            assert!(tr.name.quoted, "Table named 'select' should be quoted");
4075        } else {
4076            panic!("Expected Table expression");
4077        }
4078    }
4079
4080    #[test]
4081    fn test_quote_identifiers_table_ref_schema_and_alias() {
4082        let mut tr = TableRef::new("my_table");
4083        tr.schema = Some(Identifier::new("from"));
4084        tr.alias = Some(Identifier::new("t"));
4085        let expr = Expression::Table(Box::new(tr));
4086        let result = quote_identifiers(expr, None);
4087        if let Expression::Table(tr) = &result {
4088            assert!(!tr.name.quoted, "Normal table name should not be quoted");
4089            assert!(
4090                tr.schema.as_ref().unwrap().quoted,
4091                "Schema named 'from' should be quoted"
4092            );
4093            assert!(
4094                !tr.alias.as_ref().unwrap().quoted,
4095                "Normal alias should not be quoted"
4096            );
4097        } else {
4098            panic!("Expected Table expression");
4099        }
4100    }
4101
4102    #[test]
4103    fn test_quote_identifiers_identifier_node() {
4104        let expr = Expression::Identifier(Identifier::new("order"));
4105        let result = quote_identifiers(expr, None);
4106        if let Expression::Identifier(id) = &result {
4107            assert!(id.quoted, "Identifier named 'order' should be quoted");
4108        } else {
4109            panic!("Expected Identifier expression");
4110        }
4111    }
4112
4113    #[test]
4114    fn test_quote_identifiers_alias() {
4115        let inner = Expression::boxed_column(Column {
4116            name: Identifier::new("val"),
4117            table: None,
4118            join_mark: false,
4119            trailing_comments: vec![],
4120            span: None,
4121            inferred_type: None,
4122        });
4123        let expr = Expression::Alias(Box::new(Alias {
4124            this: inner,
4125            alias: Identifier::new("select"),
4126            column_aliases: vec![Identifier::new("from")],
4127            alias_explicit_as: false,
4128            alias_keyword: None,
4129            pre_alias_comments: vec![],
4130            trailing_comments: vec![],
4131            inferred_type: None,
4132        }));
4133        let result = quote_identifiers(expr, None);
4134        if let Expression::Alias(alias) = &result {
4135            assert!(alias.alias.quoted, "Alias named 'select' should be quoted");
4136            assert!(
4137                alias.column_aliases[0].quoted,
4138                "Column alias named 'from' should be quoted"
4139            );
4140            // Inner column "val" should not be quoted
4141            if let Expression::Column(col) = &alias.this {
4142                assert!(!col.name.quoted);
4143            }
4144        } else {
4145            panic!("Expected Alias expression");
4146        }
4147    }
4148
4149    #[test]
4150    fn test_quote_identifiers_select_recursive() {
4151        // Parse a query and verify quote_identifiers walks through it
4152        let expr = parse("SELECT a, b FROM t WHERE c = 1");
4153        let result = quote_identifiers(expr, None);
4154        // "a", "b", "c", "t" are all normal identifiers, none should be quoted
4155        let sql = gen(&result);
4156        // The SQL should be unchanged since no reserved words are used
4157        assert!(sql.contains("a"));
4158        assert!(sql.contains("b"));
4159        assert!(sql.contains("t"));
4160    }
4161
4162    #[test]
4163    fn test_quote_identifiers_digit_start() {
4164        let expr = Expression::boxed_column(Column {
4165            name: Identifier::new("1col"),
4166            table: None,
4167            join_mark: false,
4168            trailing_comments: vec![],
4169            span: None,
4170            inferred_type: None,
4171        });
4172        let result = quote_identifiers(expr, None);
4173        if let Expression::Column(col) = &result {
4174            assert!(
4175                col.name.quoted,
4176                "Column starting with digit should be quoted"
4177            );
4178        } else {
4179            panic!("Expected Column expression");
4180        }
4181    }
4182
4183    #[test]
4184    fn test_quote_identifiers_with_mysql_dialect() {
4185        let reserved = get_reserved_words(Some(DialectType::MySQL));
4186        // "KILL" is reserved in MySQL
4187        assert!(needs_quoting("KILL", &reserved));
4188        // "FORCE" is reserved in MySQL
4189        assert!(needs_quoting("FORCE", &reserved));
4190    }
4191
4192    #[test]
4193    fn test_quote_identifiers_with_postgresql_dialect() {
4194        let reserved = get_reserved_words(Some(DialectType::PostgreSQL));
4195        // "ILIKE" is reserved in PostgreSQL
4196        assert!(needs_quoting("ILIKE", &reserved));
4197        // "VERBOSE" is reserved in PostgreSQL
4198        assert!(needs_quoting("VERBOSE", &reserved));
4199    }
4200
4201    #[test]
4202    fn test_quote_identifiers_with_bigquery_dialect() {
4203        let reserved = get_reserved_words(Some(DialectType::BigQuery));
4204        // "STRUCT" is reserved in BigQuery
4205        assert!(needs_quoting("STRUCT", &reserved));
4206        // "PROTO" is reserved in BigQuery
4207        assert!(needs_quoting("PROTO", &reserved));
4208    }
4209
4210    #[test]
4211    fn test_quote_identifiers_case_insensitive_reserved() {
4212        let reserved = get_reserved_words(None);
4213        assert!(needs_quoting("Select", &reserved));
4214        assert!(needs_quoting("sElEcT", &reserved));
4215        assert!(needs_quoting("FROM", &reserved));
4216        assert!(needs_quoting("from", &reserved));
4217    }
4218
4219    #[test]
4220    fn test_quote_identifiers_join_using() {
4221        // Build a join with USING identifiers that include reserved words
4222        let mut join = crate::expressions::Join {
4223            this: Expression::Table(Box::new(TableRef::new("other"))),
4224            on: None,
4225            using: vec![Identifier::new("key"), Identifier::new("value")],
4226            kind: crate::expressions::JoinKind::Inner,
4227            use_inner_keyword: false,
4228            use_outer_keyword: false,
4229            deferred_condition: false,
4230            join_hint: None,
4231            match_condition: None,
4232            pivots: vec![],
4233            comments: vec![],
4234            nesting_group: 0,
4235            directed: false,
4236        };
4237        let reserved = get_reserved_words(None);
4238        quote_join(&mut join, &reserved);
4239        // "key" is reserved, "value" is not
4240        assert!(
4241            join.using[0].quoted,
4242            "USING identifier 'key' should be quoted"
4243        );
4244        assert!(
4245            !join.using[1].quoted,
4246            "USING identifier 'value' should not be quoted"
4247        );
4248    }
4249
4250    #[test]
4251    fn test_quote_identifiers_cte() {
4252        // Build a CTE where alias is a reserved word
4253        let mut cte = crate::expressions::Cte {
4254            alias: Identifier::new("select"),
4255            this: Expression::boxed_column(Column {
4256                name: Identifier::new("x"),
4257                table: None,
4258                join_mark: false,
4259                trailing_comments: vec![],
4260                span: None,
4261                inferred_type: None,
4262            }),
4263            columns: vec![Identifier::new("from"), Identifier::new("normal")],
4264            materialized: None,
4265            key_expressions: vec![],
4266            alias_first: false,
4267            comments: Vec::new(),
4268        };
4269        let reserved = get_reserved_words(None);
4270        maybe_quote(&mut cte.alias, &reserved);
4271        for c in &mut cte.columns {
4272            maybe_quote(c, &reserved);
4273        }
4274        assert!(cte.alias.quoted, "CTE alias 'select' should be quoted");
4275        assert!(cte.columns[0].quoted, "CTE column 'from' should be quoted");
4276        assert!(
4277            !cte.columns[1].quoted,
4278            "CTE column 'normal' should not be quoted"
4279        );
4280    }
4281
4282    #[test]
4283    fn test_quote_identifiers_binary_ops_recurse() {
4284        // a_col + select_col should quote "select_col" but that's actually
4285        // just a regular name. Use actual reserved word as column name.
4286        let expr = Expression::Add(Box::new(crate::expressions::BinaryOp::new(
4287            Expression::boxed_column(Column {
4288                name: Identifier::new("select"),
4289                table: None,
4290                join_mark: false,
4291                trailing_comments: vec![],
4292                span: None,
4293                inferred_type: None,
4294            }),
4295            Expression::boxed_column(Column {
4296                name: Identifier::new("normal"),
4297                table: None,
4298                join_mark: false,
4299                trailing_comments: vec![],
4300                span: None,
4301                inferred_type: None,
4302            }),
4303        )));
4304        let result = quote_identifiers(expr, None);
4305        if let Expression::Add(bin) = &result {
4306            if let Expression::Column(left) = &bin.left {
4307                assert!(
4308                    left.name.quoted,
4309                    "'select' column should be quoted in binary op"
4310                );
4311            }
4312            if let Expression::Column(right) = &bin.right {
4313                assert!(!right.name.quoted, "'normal' column should not be quoted");
4314            }
4315        } else {
4316            panic!("Expected Add expression");
4317        }
4318    }
4319
4320    #[test]
4321    fn test_quote_identifiers_already_quoted_preserved() {
4322        // Already-quoted identifier should stay quoted even if it doesn't need it
4323        let expr = Expression::boxed_column(Column {
4324            name: Identifier::quoted("normal_name"),
4325            table: None,
4326            join_mark: false,
4327            trailing_comments: vec![],
4328            span: None,
4329            inferred_type: None,
4330        });
4331        let result = quote_identifiers(expr, None);
4332        if let Expression::Column(col) = &result {
4333            assert!(
4334                col.name.quoted,
4335                "Already-quoted identifier should remain quoted"
4336            );
4337        } else {
4338            panic!("Expected Column expression");
4339        }
4340    }
4341
4342    #[test]
4343    fn test_quote_identifiers_full_parsed_query() {
4344        // Test with a parsed query that uses reserved words as identifiers
4345        // We build the AST manually since the parser would fail on unquoted reserved words
4346        let mut select = crate::expressions::Select::new();
4347        select.expressions.push(Expression::boxed_column(Column {
4348            name: Identifier::new("order"),
4349            table: Some(Identifier::new("t")),
4350            join_mark: false,
4351            trailing_comments: vec![],
4352            span: None,
4353            inferred_type: None,
4354        }));
4355        select.from = Some(crate::expressions::From {
4356            expressions: vec![Expression::Table(Box::new(TableRef::new("t")))],
4357        });
4358        let expr = Expression::Select(Box::new(select));
4359
4360        let result = quote_identifiers(expr, None);
4361        if let Expression::Select(sel) = &result {
4362            if let Expression::Column(col) = &sel.expressions[0] {
4363                assert!(col.name.quoted, "Column named 'order' should be quoted");
4364                assert!(
4365                    !col.table.as_ref().unwrap().quoted,
4366                    "Table 't' should not be quoted"
4367                );
4368            } else {
4369                panic!("Expected Column in SELECT list");
4370            }
4371        } else {
4372            panic!("Expected Select expression");
4373        }
4374    }
4375
4376    #[test]
4377    fn test_get_reserved_words_all_dialects() {
4378        // Ensure get_reserved_words doesn't panic for any dialect
4379        let dialects = [
4380            None,
4381            Some(DialectType::Generic),
4382            Some(DialectType::MySQL),
4383            Some(DialectType::PostgreSQL),
4384            Some(DialectType::BigQuery),
4385            Some(DialectType::Snowflake),
4386            Some(DialectType::TSQL),
4387            Some(DialectType::ClickHouse),
4388            Some(DialectType::DuckDB),
4389            Some(DialectType::Hive),
4390            Some(DialectType::Spark),
4391            Some(DialectType::Trino),
4392            Some(DialectType::Oracle),
4393            Some(DialectType::Redshift),
4394        ];
4395        for dialect in &dialects {
4396            let words = get_reserved_words(*dialect);
4397            // All dialects should have basic SQL reserved words
4398            assert!(
4399                words.contains("SELECT"),
4400                "All dialects should have SELECT as reserved"
4401            );
4402            assert!(
4403                words.contains("FROM"),
4404                "All dialects should have FROM as reserved"
4405            );
4406        }
4407    }
4408}