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