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