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) => {
1060                has_star = true;
1061                if let Some(table) = &star.table {
1062                    let table_name = &table.name;
1063                    if !ordered_sources.contains(table_name) {
1064                        return Err(QualifyColumnsError::UnknownTable(table_name.clone()));
1065                    }
1066                    if let Ok(columns) = resolver.get_source_columns(table_name) {
1067                        if columns.contains(&"*".to_string()) || columns.is_empty() {
1068                            return Ok(());
1069                        }
1070                        for col_name in &columns {
1071                            let normalized = normalize_column_name(col_name, resolver.dialect);
1072                            if coalesced_columns.contains(&normalized) {
1073                                continue;
1074                            }
1075                            if let Some(tables) = column_tables.get(&normalized) {
1076                                if tables.contains(table_name) {
1077                                    coalesced_columns.insert(normalized);
1078                                    let coalesce = make_coalesce(col_name, tables);
1079                                    new_selections.push(Expression::Alias(Box::new(Alias {
1080                                        this: coalesce,
1081                                        alias: Identifier::new(col_name),
1082                                        column_aliases: vec![],
1083                                        alias_explicit_as: false,
1084                                        alias_keyword: None,
1085                                        pre_alias_comments: vec![],
1086                                        trailing_comments: vec![],
1087                                        inferred_type: None,
1088                                    })));
1089                                    continue;
1090                                }
1091                            }
1092                            new_selections
1093                                .push(create_qualified_column(col_name, Some(table_name)));
1094                        }
1095                    }
1096                } else {
1097                    for source_name in &ordered_sources {
1098                        if let Ok(columns) = resolver.get_source_columns(source_name) {
1099                            if columns.contains(&"*".to_string()) || columns.is_empty() {
1100                                return Ok(());
1101                            }
1102                            for col_name in &columns {
1103                                let normalized = normalize_column_name(col_name, resolver.dialect);
1104                                if coalesced_columns.contains(&normalized) {
1105                                    // Already emitted as COALESCE, skip
1106                                    continue;
1107                                }
1108                                if let Some(tables) = column_tables.get(&normalized) {
1109                                    if tables.contains(source_name) {
1110                                        // Emit COALESCE and mark as coalesced
1111                                        coalesced_columns.insert(normalized);
1112                                        let coalesce = make_coalesce(col_name, tables);
1113                                        new_selections.push(Expression::Alias(Box::new(Alias {
1114                                            this: coalesce,
1115                                            alias: Identifier::new(col_name),
1116                                            column_aliases: vec![],
1117                                            alias_explicit_as: false,
1118                                            alias_keyword: None,
1119                                            pre_alias_comments: vec![],
1120                                            trailing_comments: vec![],
1121                                            inferred_type: None,
1122                                        })));
1123                                        continue;
1124                                    }
1125                                }
1126                                new_selections
1127                                    .push(create_qualified_column(col_name, Some(source_name)));
1128                            }
1129                        }
1130                    }
1131                }
1132            }
1133            Expression::Column(col) if is_star_column(col) => {
1134                has_star = true;
1135                if let Some(table) = &col.table {
1136                    let table_name = &table.name;
1137                    if !ordered_sources.contains(table_name) {
1138                        return Err(QualifyColumnsError::UnknownTable(table_name.clone()));
1139                    }
1140                    if let Ok(columns) = resolver.get_source_columns(table_name) {
1141                        if columns.contains(&"*".to_string()) || columns.is_empty() {
1142                            return Ok(());
1143                        }
1144                        for col_name in &columns {
1145                            let normalized = normalize_column_name(col_name, resolver.dialect);
1146                            if coalesced_columns.contains(&normalized) {
1147                                continue;
1148                            }
1149                            if let Some(tables) = column_tables.get(&normalized) {
1150                                if tables.contains(table_name) {
1151                                    coalesced_columns.insert(normalized);
1152                                    let coalesce = make_coalesce(col_name, tables);
1153                                    new_selections.push(Expression::Alias(Box::new(Alias {
1154                                        this: coalesce,
1155                                        alias: Identifier::new(col_name),
1156                                        column_aliases: vec![],
1157                                        alias_explicit_as: false,
1158                                        alias_keyword: None,
1159                                        pre_alias_comments: vec![],
1160                                        trailing_comments: vec![],
1161                                        inferred_type: None,
1162                                    })));
1163                                    continue;
1164                                }
1165                            }
1166                            new_selections
1167                                .push(create_qualified_column(col_name, Some(table_name)));
1168                        }
1169                    }
1170                }
1171            }
1172            _ => new_selections.push(expr.clone()),
1173        }
1174    }
1175
1176    if has_star {
1177        select.expressions = new_selections;
1178    }
1179
1180    Ok(())
1181}
1182
1183/// Ensure all output columns in a SELECT are aliased.
1184///
1185/// For example:
1186/// `SELECT a + b FROM t`
1187/// becomes:
1188/// `SELECT a + b AS _col_0 FROM t`
1189pub fn qualify_outputs(scope: &Scope) -> QualifyColumnsResult<()> {
1190    if let Expression::Select(mut select) = scope.expression.clone() {
1191        qualify_outputs_select(&mut select)?;
1192    }
1193    Ok(())
1194}
1195
1196fn qualify_outputs_select(select: &mut Select) -> QualifyColumnsResult<()> {
1197    let mut new_selections: Vec<Expression> = Vec::new();
1198
1199    for (i, expr) in select.expressions.iter().enumerate() {
1200        match expr {
1201            Expression::Alias(_) => new_selections.push(expr.clone()),
1202            Expression::Column(col) => {
1203                new_selections.push(create_alias(expr.clone(), &col.name.name));
1204            }
1205            Expression::Star(_) => new_selections.push(expr.clone()),
1206            _ => {
1207                let alias_name = get_output_name(expr).unwrap_or_else(|| format!("_col_{}", i));
1208                new_selections.push(create_alias(expr.clone(), &alias_name));
1209            }
1210        }
1211    }
1212
1213    select.expressions = new_selections;
1214    Ok(())
1215}
1216
1217fn qualify_columns_in_expression(
1218    expr: &mut Expression,
1219    scope: &Scope,
1220    resolver: &mut Resolver,
1221    allow_partial: bool,
1222) -> QualifyColumnsResult<()> {
1223    let first_error: RefCell<Option<QualifyColumnsError>> = RefCell::new(None);
1224    let resolver_cell: RefCell<&mut Resolver> = RefCell::new(resolver);
1225
1226    let transformed = transform_in_scope(expr.clone(), |node| {
1227        if first_error.borrow().is_some() {
1228            return Ok(node);
1229        }
1230
1231        match node {
1232            Expression::Column(mut col) => {
1233                if let Err(err) = qualify_single_column(
1234                    &mut col,
1235                    scope,
1236                    &mut resolver_cell.borrow_mut(),
1237                    allow_partial,
1238                ) {
1239                    *first_error.borrow_mut() = Some(err);
1240                }
1241                Ok(Expression::Column(col))
1242            }
1243            _ => Ok(node),
1244        }
1245    })
1246    .map_err(|err: crate::Error| QualifyColumnsError::CannotAutoJoin(err.to_string()))?;
1247
1248    if let Some(err) = first_error.into_inner() {
1249        return Err(err);
1250    }
1251
1252    *expr = transformed;
1253    Ok(())
1254}
1255
1256fn qualify_single_column(
1257    col: &mut Column,
1258    scope: &Scope,
1259    resolver: &mut Resolver,
1260    allow_partial: bool,
1261) -> QualifyColumnsResult<()> {
1262    if is_star_column(col) {
1263        return Ok(());
1264    }
1265
1266    if let Some(table) = &col.table {
1267        let table_name = &table.name;
1268        let source_name = scope.sources.keys().find(|source| {
1269            normalize_name(source, resolver.dialect, true, true)
1270                == normalize_name(table_name, resolver.dialect, true, true)
1271        });
1272        let Some(source_name) = source_name else {
1273            if resolver.outer_source_exists(table_name) {
1274                return Ok(());
1275            }
1276            return Err(QualifyColumnsError::UnknownTable(table_name.clone()));
1277        };
1278
1279        if let Ok(source_columns) = resolver.get_source_columns(source_name) {
1280            let normalized_column_name = normalize_column_name(&col.name.name, resolver.dialect);
1281            if !allow_partial
1282                && !source_columns.is_empty()
1283                && !source_columns.iter().any(|column| {
1284                    normalize_column_name(column, resolver.dialect) == normalized_column_name
1285                })
1286                && !source_columns.contains(&"*".to_string())
1287            {
1288                return Err(QualifyColumnsError::UnknownColumn(col.name.name.clone()));
1289            }
1290        }
1291        return Ok(());
1292    }
1293
1294    if resolver.is_ambiguous(&col.name.name) {
1295        if allow_partial {
1296            return Ok(());
1297        }
1298        return Err(QualifyColumnsError::AmbiguousColumn(col.name.name.clone()));
1299    }
1300    if let Some(table_name) = resolver.get_table(&col.name.name) {
1301        col.table = Some(Identifier::new(table_name));
1302        return Ok(());
1303    }
1304
1305    if let Some(outer_table) = resolver.find_column_in_outer_scopes(&col.name.name) {
1306        col.table = Some(Identifier::new(outer_table));
1307        return Ok(());
1308    }
1309
1310    if !allow_partial {
1311        return Err(QualifyColumnsError::UnknownColumn(col.name.name.clone()));
1312    }
1313
1314    Ok(())
1315}
1316
1317fn normalize_column_name(name: &str, dialect: Option<DialectType>) -> String {
1318    normalize_name(name, dialect, false, true)
1319}
1320
1321fn replace_alias_refs_in_expression(
1322    expr: &mut Expression,
1323    alias_to_expression: &HashMap<String, (Expression, usize)>,
1324    literal_index: bool,
1325) {
1326    let transformed: crate::Result<_> = transform_in_scope(expr.clone(), |node| match node {
1327        Expression::Column(col) if col.table.is_none() => {
1328            if let Some((alias_expr, index)) = alias_to_expression.get(&col.name.name) {
1329                if literal_index && matches!(alias_expr, Expression::Literal(_)) {
1330                    return Ok(Expression::number(*index as i64));
1331                }
1332                return Ok(Expression::Paren(Box::new(Paren {
1333                    this: alias_expr.clone(),
1334                    trailing_comments: vec![],
1335                })));
1336            }
1337            Ok(Expression::Column(col))
1338        }
1339        other => Ok(other),
1340    });
1341
1342    if let Ok(next) = transformed {
1343        *expr = next;
1344    }
1345}
1346
1347fn positional_reference(expr: &Expression) -> Option<usize> {
1348    match expr {
1349        Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
1350            let Literal::Number(value) = lit.as_ref() else {
1351                unreachable!()
1352            };
1353            value.parse::<usize>().ok()
1354        }
1355        _ => None,
1356    }
1357}
1358
1359fn select_expression_at_position(
1360    projections: &[Expression],
1361    index: usize,
1362) -> QualifyColumnsResult<Expression> {
1363    if index == 0 || index > projections.len() {
1364        return Err(QualifyColumnsError::UnknownOutputColumn(index.to_string()));
1365    }
1366
1367    let projection = projections[index - 1].clone();
1368    Ok(match projection {
1369        Expression::Alias(alias) => alias.this.clone(),
1370        other => other,
1371    })
1372}
1373
1374/// Returns the set of SQL reserved words for a given dialect.
1375/// If no dialect is provided, returns a comprehensive default set.
1376fn get_reserved_words(dialect: Option<DialectType>) -> HashSet<&'static str> {
1377    // Core SQL reserved words that are common across all dialects
1378    let mut words: HashSet<&'static str> = [
1379        // SQL standard reserved words
1380        "ADD",
1381        "ALL",
1382        "ALTER",
1383        "AND",
1384        "ANY",
1385        "AS",
1386        "ASC",
1387        "BETWEEN",
1388        "BY",
1389        "CASE",
1390        "CAST",
1391        "CHECK",
1392        "COLUMN",
1393        "CONSTRAINT",
1394        "CREATE",
1395        "CROSS",
1396        "CURRENT",
1397        "CURRENT_DATE",
1398        "CURRENT_TIME",
1399        "CURRENT_TIMESTAMP",
1400        "CURRENT_USER",
1401        "DATABASE",
1402        "DEFAULT",
1403        "DELETE",
1404        "DESC",
1405        "DISTINCT",
1406        "DROP",
1407        "ELSE",
1408        "END",
1409        "ESCAPE",
1410        "EXCEPT",
1411        "EXISTS",
1412        "FALSE",
1413        "FETCH",
1414        "FOR",
1415        "FOREIGN",
1416        "FROM",
1417        "FULL",
1418        "GRANT",
1419        "GROUP",
1420        "HAVING",
1421        "IF",
1422        "IN",
1423        "INDEX",
1424        "INNER",
1425        "INSERT",
1426        "INTERSECT",
1427        "INTO",
1428        "IS",
1429        "JOIN",
1430        "KEY",
1431        "LEFT",
1432        "LIKE",
1433        "LIMIT",
1434        "NATURAL",
1435        "NOT",
1436        "NULL",
1437        "OFFSET",
1438        "ON",
1439        "OR",
1440        "ORDER",
1441        "OUTER",
1442        "PRIMARY",
1443        "REFERENCES",
1444        "REPLACE",
1445        "RETURNING",
1446        "RIGHT",
1447        "ROLLBACK",
1448        "ROW",
1449        "ROWS",
1450        "SELECT",
1451        "SESSION_USER",
1452        "SET",
1453        "SOME",
1454        "TABLE",
1455        "THEN",
1456        "TO",
1457        "TRUE",
1458        "TRUNCATE",
1459        "UNION",
1460        "UNIQUE",
1461        "UPDATE",
1462        "USING",
1463        "VALUES",
1464        "VIEW",
1465        "WHEN",
1466        "WHERE",
1467        "WINDOW",
1468        "WITH",
1469    ]
1470    .iter()
1471    .copied()
1472    .collect();
1473
1474    // Add dialect-specific reserved words
1475    match dialect {
1476        Some(DialectType::MySQL) => {
1477            words.extend(
1478                [
1479                    "ANALYZE",
1480                    "BOTH",
1481                    "CHANGE",
1482                    "CONDITION",
1483                    "DATABASES",
1484                    "DAY_HOUR",
1485                    "DAY_MICROSECOND",
1486                    "DAY_MINUTE",
1487                    "DAY_SECOND",
1488                    "DELAYED",
1489                    "DETERMINISTIC",
1490                    "DIV",
1491                    "DUAL",
1492                    "EACH",
1493                    "ELSEIF",
1494                    "ENCLOSED",
1495                    "EXPLAIN",
1496                    "FLOAT4",
1497                    "FLOAT8",
1498                    "FORCE",
1499                    "HOUR_MICROSECOND",
1500                    "HOUR_MINUTE",
1501                    "HOUR_SECOND",
1502                    "IGNORE",
1503                    "INFILE",
1504                    "INT1",
1505                    "INT2",
1506                    "INT3",
1507                    "INT4",
1508                    "INT8",
1509                    "ITERATE",
1510                    "KEYS",
1511                    "KILL",
1512                    "LEADING",
1513                    "LEAVE",
1514                    "LINES",
1515                    "LOAD",
1516                    "LOCK",
1517                    "LONG",
1518                    "LONGBLOB",
1519                    "LONGTEXT",
1520                    "LOOP",
1521                    "LOW_PRIORITY",
1522                    "MATCH",
1523                    "MEDIUMBLOB",
1524                    "MEDIUMINT",
1525                    "MEDIUMTEXT",
1526                    "MINUTE_MICROSECOND",
1527                    "MINUTE_SECOND",
1528                    "MOD",
1529                    "MODIFIES",
1530                    "NO_WRITE_TO_BINLOG",
1531                    "OPTIMIZE",
1532                    "OPTIONALLY",
1533                    "OUT",
1534                    "OUTFILE",
1535                    "PURGE",
1536                    "READS",
1537                    "REGEXP",
1538                    "RELEASE",
1539                    "RENAME",
1540                    "REPEAT",
1541                    "REQUIRE",
1542                    "RESIGNAL",
1543                    "RETURN",
1544                    "REVOKE",
1545                    "RLIKE",
1546                    "SCHEMA",
1547                    "SCHEMAS",
1548                    "SECOND_MICROSECOND",
1549                    "SENSITIVE",
1550                    "SEPARATOR",
1551                    "SHOW",
1552                    "SIGNAL",
1553                    "SPATIAL",
1554                    "SQL",
1555                    "SQLEXCEPTION",
1556                    "SQLSTATE",
1557                    "SQLWARNING",
1558                    "SQL_BIG_RESULT",
1559                    "SQL_CALC_FOUND_ROWS",
1560                    "SQL_SMALL_RESULT",
1561                    "SSL",
1562                    "STARTING",
1563                    "STRAIGHT_JOIN",
1564                    "TERMINATED",
1565                    "TINYBLOB",
1566                    "TINYINT",
1567                    "TINYTEXT",
1568                    "TRAILING",
1569                    "TRIGGER",
1570                    "UNDO",
1571                    "UNLOCK",
1572                    "UNSIGNED",
1573                    "USAGE",
1574                    "UTC_DATE",
1575                    "UTC_TIME",
1576                    "UTC_TIMESTAMP",
1577                    "VARBINARY",
1578                    "VARCHARACTER",
1579                    "WHILE",
1580                    "WRITE",
1581                    "XOR",
1582                    "YEAR_MONTH",
1583                    "ZEROFILL",
1584                ]
1585                .iter()
1586                .copied(),
1587            );
1588        }
1589        Some(DialectType::PostgreSQL) | Some(DialectType::CockroachDB) => {
1590            words.extend(
1591                [
1592                    "ANALYSE",
1593                    "ANALYZE",
1594                    "ARRAY",
1595                    "AUTHORIZATION",
1596                    "BINARY",
1597                    "BOTH",
1598                    "COLLATE",
1599                    "CONCURRENTLY",
1600                    "DO",
1601                    "FREEZE",
1602                    "ILIKE",
1603                    "INITIALLY",
1604                    "ISNULL",
1605                    "LATERAL",
1606                    "LEADING",
1607                    "LOCALTIME",
1608                    "LOCALTIMESTAMP",
1609                    "NOTNULL",
1610                    "ONLY",
1611                    "OVERLAPS",
1612                    "PLACING",
1613                    "SIMILAR",
1614                    "SYMMETRIC",
1615                    "TABLESAMPLE",
1616                    "TRAILING",
1617                    "VARIADIC",
1618                    "VERBOSE",
1619                ]
1620                .iter()
1621                .copied(),
1622            );
1623        }
1624        Some(DialectType::BigQuery) => {
1625            words.extend(
1626                [
1627                    "ASSERT_ROWS_MODIFIED",
1628                    "COLLATE",
1629                    "CONTAINS",
1630                    "CUBE",
1631                    "DEFINE",
1632                    "ENUM",
1633                    "EXTRACT",
1634                    "FOLLOWING",
1635                    "GROUPING",
1636                    "GROUPS",
1637                    "HASH",
1638                    "IGNORE",
1639                    "LATERAL",
1640                    "LOOKUP",
1641                    "MERGE",
1642                    "NEW",
1643                    "NO",
1644                    "NULLS",
1645                    "OF",
1646                    "OVER",
1647                    "PARTITION",
1648                    "PRECEDING",
1649                    "PROTO",
1650                    "RANGE",
1651                    "RECURSIVE",
1652                    "RESPECT",
1653                    "ROLLUP",
1654                    "STRUCT",
1655                    "TABLESAMPLE",
1656                    "TREAT",
1657                    "UNBOUNDED",
1658                    "WITHIN",
1659                ]
1660                .iter()
1661                .copied(),
1662            );
1663        }
1664        Some(DialectType::Snowflake) => {
1665            words.extend(
1666                [
1667                    "ACCOUNT",
1668                    "BOTH",
1669                    "CONNECT",
1670                    "FOLLOWING",
1671                    "ILIKE",
1672                    "INCREMENT",
1673                    "ISSUE",
1674                    "LATERAL",
1675                    "LEADING",
1676                    "LOCALTIME",
1677                    "LOCALTIMESTAMP",
1678                    "MINUS",
1679                    "QUALIFY",
1680                    "REGEXP",
1681                    "RLIKE",
1682                    "SOME",
1683                    "START",
1684                    "TABLESAMPLE",
1685                    "TOP",
1686                    "TRAILING",
1687                    "TRY_CAST",
1688                ]
1689                .iter()
1690                .copied(),
1691            );
1692        }
1693        Some(DialectType::TSQL) | Some(DialectType::Fabric) => {
1694            words.extend(
1695                [
1696                    "BACKUP",
1697                    "BREAK",
1698                    "BROWSE",
1699                    "BULK",
1700                    "CASCADE",
1701                    "CHECKPOINT",
1702                    "CLOSE",
1703                    "CLUSTERED",
1704                    "COALESCE",
1705                    "COMPUTE",
1706                    "CONTAINS",
1707                    "CONTAINSTABLE",
1708                    "CONTINUE",
1709                    "CONVERT",
1710                    "DBCC",
1711                    "DEALLOCATE",
1712                    "DENY",
1713                    "DISK",
1714                    "DISTRIBUTED",
1715                    "DUMP",
1716                    "ERRLVL",
1717                    "EXEC",
1718                    "EXECUTE",
1719                    "EXIT",
1720                    "EXTERNAL",
1721                    "FILE",
1722                    "FILLFACTOR",
1723                    "FREETEXT",
1724                    "FREETEXTTABLE",
1725                    "FUNCTION",
1726                    "GOTO",
1727                    "HOLDLOCK",
1728                    "IDENTITY",
1729                    "IDENTITYCOL",
1730                    "IDENTITY_INSERT",
1731                    "KILL",
1732                    "LINENO",
1733                    "MERGE",
1734                    "NONCLUSTERED",
1735                    "NULLIF",
1736                    "OF",
1737                    "OFF",
1738                    "OFFSETS",
1739                    "OPEN",
1740                    "OPENDATASOURCE",
1741                    "OPENQUERY",
1742                    "OPENROWSET",
1743                    "OPENXML",
1744                    "OVER",
1745                    "PERCENT",
1746                    "PIVOT",
1747                    "PLAN",
1748                    "PRINT",
1749                    "PROC",
1750                    "PROCEDURE",
1751                    "PUBLIC",
1752                    "RAISERROR",
1753                    "READ",
1754                    "READTEXT",
1755                    "RECONFIGURE",
1756                    "REPLICATION",
1757                    "RESTORE",
1758                    "RESTRICT",
1759                    "REVERT",
1760                    "ROWCOUNT",
1761                    "ROWGUIDCOL",
1762                    "RULE",
1763                    "SAVE",
1764                    "SECURITYAUDIT",
1765                    "SEMANTICKEYPHRASETABLE",
1766                    "SEMANTICSIMILARITYDETAILSTABLE",
1767                    "SEMANTICSIMILARITYTABLE",
1768                    "SETUSER",
1769                    "SHUTDOWN",
1770                    "STATISTICS",
1771                    "SYSTEM_USER",
1772                    "TEXTSIZE",
1773                    "TOP",
1774                    "TRAN",
1775                    "TRANSACTION",
1776                    "TRIGGER",
1777                    "TSEQUAL",
1778                    "UNPIVOT",
1779                    "UPDATETEXT",
1780                    "WAITFOR",
1781                    "WRITETEXT",
1782                ]
1783                .iter()
1784                .copied(),
1785            );
1786        }
1787        Some(DialectType::ClickHouse) => {
1788            words.extend(
1789                [
1790                    "ANTI",
1791                    "ARRAY",
1792                    "ASOF",
1793                    "FINAL",
1794                    "FORMAT",
1795                    "GLOBAL",
1796                    "INF",
1797                    "KILL",
1798                    "MATERIALIZED",
1799                    "NAN",
1800                    "PREWHERE",
1801                    "SAMPLE",
1802                    "SEMI",
1803                    "SETTINGS",
1804                    "TOP",
1805                ]
1806                .iter()
1807                .copied(),
1808            );
1809        }
1810        Some(DialectType::DuckDB) => {
1811            words.extend(
1812                [
1813                    "ANALYSE",
1814                    "ANALYZE",
1815                    "ARRAY",
1816                    "BOTH",
1817                    "LATERAL",
1818                    "LEADING",
1819                    "LOCALTIME",
1820                    "LOCALTIMESTAMP",
1821                    "PLACING",
1822                    "QUALIFY",
1823                    "SIMILAR",
1824                    "TABLESAMPLE",
1825                    "TRAILING",
1826                ]
1827                .iter()
1828                .copied(),
1829            );
1830        }
1831        Some(DialectType::Hive) | Some(DialectType::Spark) | Some(DialectType::Databricks) => {
1832            words.extend(
1833                [
1834                    "BOTH",
1835                    "CLUSTER",
1836                    "DISTRIBUTE",
1837                    "EXCHANGE",
1838                    "EXTENDED",
1839                    "FUNCTION",
1840                    "LATERAL",
1841                    "LEADING",
1842                    "MACRO",
1843                    "OVER",
1844                    "PARTITION",
1845                    "PERCENT",
1846                    "RANGE",
1847                    "READS",
1848                    "REDUCE",
1849                    "REGEXP",
1850                    "REVOKE",
1851                    "RLIKE",
1852                    "ROLLUP",
1853                    "SEMI",
1854                    "SORT",
1855                    "TABLESAMPLE",
1856                    "TRAILING",
1857                    "TRANSFORM",
1858                    "UNBOUNDED",
1859                    "UNIQUEJOIN",
1860                ]
1861                .iter()
1862                .copied(),
1863            );
1864        }
1865        Some(DialectType::Trino) | Some(DialectType::Presto) | Some(DialectType::Athena) => {
1866            words.extend(
1867                [
1868                    "CUBE",
1869                    "DEALLOCATE",
1870                    "DESCRIBE",
1871                    "EXECUTE",
1872                    "EXTRACT",
1873                    "GROUPING",
1874                    "LATERAL",
1875                    "LOCALTIME",
1876                    "LOCALTIMESTAMP",
1877                    "NORMALIZE",
1878                    "PREPARE",
1879                    "ROLLUP",
1880                    "SOME",
1881                    "TABLESAMPLE",
1882                    "UESCAPE",
1883                    "UNNEST",
1884                ]
1885                .iter()
1886                .copied(),
1887            );
1888        }
1889        Some(DialectType::Oracle) => {
1890            words.extend(
1891                [
1892                    "ACCESS",
1893                    "AUDIT",
1894                    "CLUSTER",
1895                    "COMMENT",
1896                    "COMPRESS",
1897                    "CONNECT",
1898                    "EXCLUSIVE",
1899                    "FILE",
1900                    "IDENTIFIED",
1901                    "IMMEDIATE",
1902                    "INCREMENT",
1903                    "INITIAL",
1904                    "LEVEL",
1905                    "LOCK",
1906                    "LONG",
1907                    "MAXEXTENTS",
1908                    "MINUS",
1909                    "MODE",
1910                    "NOAUDIT",
1911                    "NOCOMPRESS",
1912                    "NOWAIT",
1913                    "NUMBER",
1914                    "OF",
1915                    "OFFLINE",
1916                    "ONLINE",
1917                    "PCTFREE",
1918                    "PRIOR",
1919                    "RAW",
1920                    "RENAME",
1921                    "RESOURCE",
1922                    "REVOKE",
1923                    "SHARE",
1924                    "SIZE",
1925                    "START",
1926                    "SUCCESSFUL",
1927                    "SYNONYM",
1928                    "SYSDATE",
1929                    "TRIGGER",
1930                    "UID",
1931                    "VALIDATE",
1932                    "VARCHAR2",
1933                    "WHENEVER",
1934                ]
1935                .iter()
1936                .copied(),
1937            );
1938        }
1939        Some(DialectType::Redshift) => {
1940            words.extend(
1941                [
1942                    "AZ64",
1943                    "BZIP2",
1944                    "DELTA",
1945                    "DELTA32K",
1946                    "DISTSTYLE",
1947                    "ENCODE",
1948                    "GZIP",
1949                    "ILIKE",
1950                    "LIMIT",
1951                    "LUNS",
1952                    "LZO",
1953                    "LZOP",
1954                    "MOSTLY13",
1955                    "MOSTLY32",
1956                    "MOSTLY8",
1957                    "RAW",
1958                    "SIMILAR",
1959                    "SNAPSHOT",
1960                    "SORTKEY",
1961                    "SYSDATE",
1962                    "TOP",
1963                    "ZSTD",
1964                ]
1965                .iter()
1966                .copied(),
1967            );
1968        }
1969        _ => {
1970            // For Generic or unknown dialects, add a broad set of commonly reserved words
1971            words.extend(
1972                [
1973                    "ANALYZE",
1974                    "ARRAY",
1975                    "BOTH",
1976                    "CUBE",
1977                    "GROUPING",
1978                    "LATERAL",
1979                    "LEADING",
1980                    "LOCALTIME",
1981                    "LOCALTIMESTAMP",
1982                    "OVER",
1983                    "PARTITION",
1984                    "QUALIFY",
1985                    "RANGE",
1986                    "ROLLUP",
1987                    "SIMILAR",
1988                    "SOME",
1989                    "TABLESAMPLE",
1990                    "TRAILING",
1991                ]
1992                .iter()
1993                .copied(),
1994            );
1995        }
1996    }
1997
1998    words
1999}
2000
2001/// Check whether an identifier name needs quoting.
2002///
2003/// An identifier needs quoting if:
2004/// - It is empty
2005/// - It starts with a digit
2006/// - It contains characters other than `[a-zA-Z0-9_]`
2007/// - It is a SQL reserved word (case-insensitive)
2008fn needs_quoting(name: &str, reserved_words: &HashSet<&str>) -> bool {
2009    if name.is_empty() {
2010        return false;
2011    }
2012
2013    // Starts with a digit
2014    if name.as_bytes()[0].is_ascii_digit() {
2015        return true;
2016    }
2017
2018    // Contains non-identifier characters
2019    if !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
2020        return true;
2021    }
2022
2023    // Is a reserved word (case-insensitive check)
2024    let upper = name.to_uppercase();
2025    reserved_words.contains(upper.as_str())
2026}
2027
2028/// Conditionally set `quoted = true` on an identifier if it needs quoting.
2029fn maybe_quote(id: &mut Identifier, reserved_words: &HashSet<&str>) {
2030    // Don't re-quote something already quoted, and don't quote empty identifiers
2031    // or wildcard identifiers
2032    if id.quoted || id.name.is_empty() || id.name == "*" {
2033        return;
2034    }
2035    if needs_quoting(&id.name, reserved_words) {
2036        id.quoted = true;
2037    }
2038}
2039
2040/// Recursively walk an expression and quote identifiers that need quoting.
2041fn quote_identifiers_recursive(expr: &mut Expression, reserved_words: &HashSet<&str>) {
2042    match expr {
2043        // ── Leaf nodes with Identifier ────────────────────────────
2044        Expression::Identifier(id) => {
2045            maybe_quote(id, reserved_words);
2046        }
2047
2048        Expression::Column(col) => {
2049            maybe_quote(&mut col.name, reserved_words);
2050            if let Some(ref mut table) = col.table {
2051                maybe_quote(table, reserved_words);
2052            }
2053        }
2054
2055        Expression::Table(table_ref) => {
2056            maybe_quote(&mut table_ref.name, reserved_words);
2057            if let Some(ref mut schema) = table_ref.schema {
2058                maybe_quote(schema, reserved_words);
2059            }
2060            if let Some(ref mut catalog) = table_ref.catalog {
2061                maybe_quote(catalog, reserved_words);
2062            }
2063            if let Some(ref mut alias) = table_ref.alias {
2064                maybe_quote(alias, reserved_words);
2065            }
2066            for ca in &mut table_ref.column_aliases {
2067                maybe_quote(ca, reserved_words);
2068            }
2069            for p in &mut table_ref.partitions {
2070                maybe_quote(p, reserved_words);
2071            }
2072            // Recurse into hints and other child expressions
2073            for h in &mut table_ref.hints {
2074                quote_identifiers_recursive(h, reserved_words);
2075            }
2076            if let Some(ref mut ver) = table_ref.version {
2077                quote_identifiers_recursive(&mut ver.this, reserved_words);
2078                if let Some(ref mut e) = ver.expression {
2079                    quote_identifiers_recursive(e, reserved_words);
2080                }
2081            }
2082        }
2083
2084        Expression::Star(star) => {
2085            if let Some(ref mut table) = star.table {
2086                maybe_quote(table, reserved_words);
2087            }
2088            if let Some(ref mut except_ids) = star.except {
2089                for id in except_ids {
2090                    maybe_quote(id, reserved_words);
2091                }
2092            }
2093            if let Some(ref mut replace_aliases) = star.replace {
2094                for alias in replace_aliases {
2095                    maybe_quote(&mut alias.alias, reserved_words);
2096                    quote_identifiers_recursive(&mut alias.this, reserved_words);
2097                }
2098            }
2099            if let Some(ref mut rename_pairs) = star.rename {
2100                for (from, to) in rename_pairs {
2101                    maybe_quote(from, reserved_words);
2102                    maybe_quote(to, reserved_words);
2103                }
2104            }
2105        }
2106
2107        // ── Alias ─────────────────────────────────────────────────
2108        Expression::Alias(alias) => {
2109            maybe_quote(&mut alias.alias, reserved_words);
2110            for ca in &mut alias.column_aliases {
2111                maybe_quote(ca, reserved_words);
2112            }
2113            quote_identifiers_recursive(&mut alias.this, reserved_words);
2114        }
2115
2116        // ── SELECT ────────────────────────────────────────────────
2117        Expression::Select(select) => {
2118            for e in &mut select.expressions {
2119                quote_identifiers_recursive(e, reserved_words);
2120            }
2121            if let Some(ref mut from) = select.from {
2122                for e in &mut from.expressions {
2123                    quote_identifiers_recursive(e, reserved_words);
2124                }
2125            }
2126            for join in &mut select.joins {
2127                quote_join(join, reserved_words);
2128            }
2129            for lv in &mut select.lateral_views {
2130                quote_lateral_view(lv, reserved_words);
2131            }
2132            if let Some(ref mut prewhere) = select.prewhere {
2133                quote_identifiers_recursive(prewhere, reserved_words);
2134            }
2135            if let Some(ref mut wh) = select.where_clause {
2136                quote_identifiers_recursive(&mut wh.this, reserved_words);
2137            }
2138            if let Some(ref mut gb) = select.group_by {
2139                for e in &mut gb.expressions {
2140                    quote_identifiers_recursive(e, reserved_words);
2141                }
2142            }
2143            if let Some(ref mut hv) = select.having {
2144                quote_identifiers_recursive(&mut hv.this, reserved_words);
2145            }
2146            if let Some(ref mut q) = select.qualify {
2147                quote_identifiers_recursive(&mut q.this, reserved_words);
2148            }
2149            if let Some(ref mut ob) = select.order_by {
2150                for o in &mut ob.expressions {
2151                    quote_identifiers_recursive(&mut o.this, reserved_words);
2152                }
2153            }
2154            if let Some(ref mut lim) = select.limit {
2155                quote_identifiers_recursive(&mut lim.this, reserved_words);
2156            }
2157            if let Some(ref mut off) = select.offset {
2158                quote_identifiers_recursive(&mut off.this, reserved_words);
2159            }
2160            if let Some(ref mut with) = select.with {
2161                quote_with(with, reserved_words);
2162            }
2163            if let Some(ref mut windows) = select.windows {
2164                for nw in windows {
2165                    maybe_quote(&mut nw.name, reserved_words);
2166                    quote_over(&mut nw.spec, reserved_words);
2167                }
2168            }
2169            if let Some(ref mut distinct_on) = select.distinct_on {
2170                for e in distinct_on {
2171                    quote_identifiers_recursive(e, reserved_words);
2172                }
2173            }
2174            if let Some(ref mut limit_by) = select.limit_by {
2175                for e in limit_by {
2176                    quote_identifiers_recursive(e, reserved_words);
2177                }
2178            }
2179            if let Some(ref mut settings) = select.settings {
2180                for e in settings {
2181                    quote_identifiers_recursive(e, reserved_words);
2182                }
2183            }
2184            if let Some(ref mut format) = select.format {
2185                quote_identifiers_recursive(format, reserved_words);
2186            }
2187        }
2188
2189        // ── Set operations ────────────────────────────────────────
2190        Expression::Union(u) => {
2191            quote_identifiers_recursive(&mut u.left, reserved_words);
2192            quote_identifiers_recursive(&mut u.right, reserved_words);
2193            if let Some(ref mut with) = u.with {
2194                quote_with(with, reserved_words);
2195            }
2196            if let Some(ref mut ob) = u.order_by {
2197                for o in &mut ob.expressions {
2198                    quote_identifiers_recursive(&mut o.this, reserved_words);
2199                }
2200            }
2201            if let Some(ref mut lim) = u.limit {
2202                quote_identifiers_recursive(lim, reserved_words);
2203            }
2204            if let Some(ref mut off) = u.offset {
2205                quote_identifiers_recursive(off, reserved_words);
2206            }
2207        }
2208        Expression::Intersect(i) => {
2209            quote_identifiers_recursive(&mut i.left, reserved_words);
2210            quote_identifiers_recursive(&mut i.right, reserved_words);
2211            if let Some(ref mut with) = i.with {
2212                quote_with(with, reserved_words);
2213            }
2214            if let Some(ref mut ob) = i.order_by {
2215                for o in &mut ob.expressions {
2216                    quote_identifiers_recursive(&mut o.this, reserved_words);
2217                }
2218            }
2219            if let Some(limit) = &mut i.limit {
2220                quote_identifiers_recursive(limit, reserved_words);
2221            }
2222            if let Some(offset) = &mut i.offset {
2223                quote_identifiers_recursive(offset, reserved_words);
2224            }
2225        }
2226        Expression::Except(e) => {
2227            quote_identifiers_recursive(&mut e.left, reserved_words);
2228            quote_identifiers_recursive(&mut e.right, reserved_words);
2229            if let Some(ref mut with) = e.with {
2230                quote_with(with, reserved_words);
2231            }
2232            if let Some(ref mut ob) = e.order_by {
2233                for o in &mut ob.expressions {
2234                    quote_identifiers_recursive(&mut o.this, reserved_words);
2235                }
2236            }
2237            if let Some(limit) = &mut e.limit {
2238                quote_identifiers_recursive(limit, reserved_words);
2239            }
2240            if let Some(offset) = &mut e.offset {
2241                quote_identifiers_recursive(offset, reserved_words);
2242            }
2243        }
2244
2245        // ── Subquery ──────────────────────────────────────────────
2246        Expression::Subquery(sq) => {
2247            quote_identifiers_recursive(&mut sq.this, reserved_words);
2248            if let Some(ref mut alias) = sq.alias {
2249                maybe_quote(alias, reserved_words);
2250            }
2251            for ca in &mut sq.column_aliases {
2252                maybe_quote(ca, reserved_words);
2253            }
2254            if let Some(ref mut ob) = sq.order_by {
2255                for o in &mut ob.expressions {
2256                    quote_identifiers_recursive(&mut o.this, reserved_words);
2257                }
2258            }
2259        }
2260
2261        // ── DML ───────────────────────────────────────────────────
2262        Expression::Insert(ins) => {
2263            quote_table_ref(&mut ins.table, reserved_words);
2264            for c in &mut ins.columns {
2265                maybe_quote(c, reserved_words);
2266            }
2267            for row in &mut ins.values {
2268                for e in row {
2269                    quote_identifiers_recursive(e, reserved_words);
2270                }
2271            }
2272            if let Some(ref mut q) = ins.query {
2273                quote_identifiers_recursive(q, reserved_words);
2274            }
2275            for (id, val) in &mut ins.partition {
2276                maybe_quote(id, reserved_words);
2277                if let Some(ref mut v) = val {
2278                    quote_identifiers_recursive(v, reserved_words);
2279                }
2280            }
2281            for e in &mut ins.returning {
2282                quote_identifiers_recursive(e, reserved_words);
2283            }
2284            if let Some(ref mut on_conflict) = ins.on_conflict {
2285                quote_identifiers_recursive(on_conflict, reserved_words);
2286            }
2287            if let Some(ref mut with) = ins.with {
2288                quote_with(with, reserved_words);
2289            }
2290            if let Some(ref mut alias) = ins.alias {
2291                maybe_quote(alias, reserved_words);
2292            }
2293            if let Some(ref mut src_alias) = ins.source_alias {
2294                maybe_quote(src_alias, reserved_words);
2295            }
2296        }
2297
2298        Expression::Update(upd) => {
2299            quote_table_ref(&mut upd.table, reserved_words);
2300            for tr in &mut upd.extra_tables {
2301                quote_table_ref(tr, reserved_words);
2302            }
2303            for join in &mut upd.table_joins {
2304                quote_join(join, reserved_words);
2305            }
2306            for (id, val) in &mut upd.set {
2307                maybe_quote(id, reserved_words);
2308                quote_identifiers_recursive(val, reserved_words);
2309            }
2310            if let Some(ref mut from) = upd.from_clause {
2311                for e in &mut from.expressions {
2312                    quote_identifiers_recursive(e, reserved_words);
2313                }
2314            }
2315            for join in &mut upd.from_joins {
2316                quote_join(join, reserved_words);
2317            }
2318            if let Some(ref mut wh) = upd.where_clause {
2319                quote_identifiers_recursive(&mut wh.this, reserved_words);
2320            }
2321            for e in &mut upd.returning {
2322                quote_identifiers_recursive(e, reserved_words);
2323            }
2324            if let Some(ref mut with) = upd.with {
2325                quote_with(with, reserved_words);
2326            }
2327        }
2328
2329        Expression::Delete(del) => {
2330            quote_table_ref(&mut del.table, reserved_words);
2331            if let Some(ref mut alias) = del.alias {
2332                maybe_quote(alias, reserved_words);
2333            }
2334            for tr in &mut del.using {
2335                quote_table_ref(tr, reserved_words);
2336            }
2337            if let Some(ref mut wh) = del.where_clause {
2338                quote_identifiers_recursive(&mut wh.this, reserved_words);
2339            }
2340            if let Some(ref mut with) = del.with {
2341                quote_with(with, reserved_words);
2342            }
2343        }
2344
2345        // ── Binary operations ─────────────────────────────────────
2346        Expression::And(bin)
2347        | Expression::Or(bin)
2348        | Expression::Eq(bin)
2349        | Expression::Neq(bin)
2350        | Expression::Lt(bin)
2351        | Expression::Lte(bin)
2352        | Expression::Gt(bin)
2353        | Expression::Gte(bin)
2354        | Expression::Add(bin)
2355        | Expression::Sub(bin)
2356        | Expression::Mul(bin)
2357        | Expression::Div(bin)
2358        | Expression::Mod(bin)
2359        | Expression::BitwiseAnd(bin)
2360        | Expression::BitwiseOr(bin)
2361        | Expression::BitwiseXor(bin)
2362        | Expression::Concat(bin)
2363        | Expression::Adjacent(bin)
2364        | Expression::TsMatch(bin)
2365        | Expression::PropertyEQ(bin)
2366        | Expression::ArrayContainsAll(bin)
2367        | Expression::ArrayContainedBy(bin)
2368        | Expression::ArrayOverlaps(bin)
2369        | Expression::JSONBContainsAllTopKeys(bin)
2370        | Expression::JSONBContainsAnyTopKeys(bin)
2371        | Expression::JSONBDeleteAtPath(bin)
2372        | Expression::ExtendsLeft(bin)
2373        | Expression::ExtendsRight(bin)
2374        | Expression::Is(bin)
2375        | Expression::NullSafeEq(bin)
2376        | Expression::NullSafeNeq(bin)
2377        | Expression::Glob(bin)
2378        | Expression::Match(bin)
2379        | Expression::MemberOf(bin)
2380        | Expression::BitwiseLeftShift(bin)
2381        | Expression::BitwiseRightShift(bin) => {
2382            quote_identifiers_recursive(&mut bin.left, reserved_words);
2383            quote_identifiers_recursive(&mut bin.right, reserved_words);
2384        }
2385
2386        // ── Like operations ───────────────────────────────────────
2387        Expression::Like(like) | Expression::ILike(like) => {
2388            quote_identifiers_recursive(&mut like.left, reserved_words);
2389            quote_identifiers_recursive(&mut like.right, reserved_words);
2390            if let Some(ref mut esc) = like.escape {
2391                quote_identifiers_recursive(esc, reserved_words);
2392            }
2393        }
2394
2395        // ── Unary operations ──────────────────────────────────────
2396        Expression::Not(un) | Expression::Neg(un) | Expression::BitwiseNot(un) => {
2397            quote_identifiers_recursive(&mut un.this, reserved_words);
2398        }
2399
2400        // ── Predicates ────────────────────────────────────────────
2401        Expression::In(in_expr) => {
2402            quote_identifiers_recursive(&mut in_expr.this, reserved_words);
2403            for e in &mut in_expr.expressions {
2404                quote_identifiers_recursive(e, reserved_words);
2405            }
2406            if let Some(ref mut q) = in_expr.query {
2407                quote_identifiers_recursive(q, reserved_words);
2408            }
2409            if let Some(ref mut un) = in_expr.unnest {
2410                quote_identifiers_recursive(un, reserved_words);
2411            }
2412        }
2413
2414        Expression::Between(bw) => {
2415            quote_identifiers_recursive(&mut bw.this, reserved_words);
2416            quote_identifiers_recursive(&mut bw.low, reserved_words);
2417            quote_identifiers_recursive(&mut bw.high, reserved_words);
2418        }
2419
2420        Expression::IsNull(is_null) => {
2421            quote_identifiers_recursive(&mut is_null.this, reserved_words);
2422        }
2423
2424        Expression::IsTrue(is_tf) | Expression::IsFalse(is_tf) => {
2425            quote_identifiers_recursive(&mut is_tf.this, reserved_words);
2426        }
2427
2428        Expression::Exists(ex) => {
2429            quote_identifiers_recursive(&mut ex.this, reserved_words);
2430        }
2431
2432        // ── Functions ─────────────────────────────────────────────
2433        Expression::Function(func) => {
2434            for arg in &mut func.args {
2435                quote_identifiers_recursive(arg, reserved_words);
2436            }
2437        }
2438
2439        Expression::AggregateFunction(agg) => {
2440            for arg in &mut agg.args {
2441                quote_identifiers_recursive(arg, reserved_words);
2442            }
2443            if let Some(ref mut filter) = agg.filter {
2444                quote_identifiers_recursive(filter, reserved_words);
2445            }
2446            for o in &mut agg.order_by {
2447                quote_identifiers_recursive(&mut o.this, reserved_words);
2448            }
2449        }
2450
2451        Expression::WindowFunction(wf) => {
2452            quote_identifiers_recursive(&mut wf.this, reserved_words);
2453            quote_over(&mut wf.over, reserved_words);
2454        }
2455
2456        // ── CASE ──────────────────────────────────────────────────
2457        Expression::Case(case) => {
2458            if let Some(ref mut operand) = case.operand {
2459                quote_identifiers_recursive(operand, reserved_words);
2460            }
2461            for (when, then) in &mut case.whens {
2462                quote_identifiers_recursive(when, reserved_words);
2463                quote_identifiers_recursive(then, reserved_words);
2464            }
2465            if let Some(ref mut else_) = case.else_ {
2466                quote_identifiers_recursive(else_, reserved_words);
2467            }
2468        }
2469
2470        // ── CAST / TryCast / SafeCast ─────────────────────────────
2471        Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => {
2472            quote_identifiers_recursive(&mut cast.this, reserved_words);
2473            if let Some(ref mut fmt) = cast.format {
2474                quote_identifiers_recursive(fmt, reserved_words);
2475            }
2476        }
2477
2478        // ── Paren / Annotated ─────────────────────────────────────
2479        Expression::Paren(paren) => {
2480            quote_identifiers_recursive(&mut paren.this, reserved_words);
2481        }
2482
2483        Expression::Annotated(ann) => {
2484            quote_identifiers_recursive(&mut ann.this, reserved_words);
2485        }
2486
2487        // ── WITH clause (standalone) ──────────────────────────────
2488        Expression::With(with) => {
2489            quote_with(with, reserved_words);
2490        }
2491
2492        Expression::Cte(cte) => {
2493            maybe_quote(&mut cte.alias, reserved_words);
2494            for c in &mut cte.columns {
2495                maybe_quote(c, reserved_words);
2496            }
2497            quote_identifiers_recursive(&mut cte.this, reserved_words);
2498        }
2499
2500        // ── Clauses (standalone) ──────────────────────────────────
2501        Expression::From(from) => {
2502            for e in &mut from.expressions {
2503                quote_identifiers_recursive(e, reserved_words);
2504            }
2505        }
2506
2507        Expression::Join(join) => {
2508            quote_join(join, reserved_words);
2509        }
2510
2511        Expression::JoinedTable(jt) => {
2512            quote_identifiers_recursive(&mut jt.left, reserved_words);
2513            for join in &mut jt.joins {
2514                quote_join(join, reserved_words);
2515            }
2516            if let Some(ref mut alias) = jt.alias {
2517                maybe_quote(alias, reserved_words);
2518            }
2519        }
2520
2521        Expression::Where(wh) => {
2522            quote_identifiers_recursive(&mut wh.this, reserved_words);
2523        }
2524
2525        Expression::GroupBy(gb) => {
2526            for e in &mut gb.expressions {
2527                quote_identifiers_recursive(e, reserved_words);
2528            }
2529        }
2530
2531        Expression::Having(hv) => {
2532            quote_identifiers_recursive(&mut hv.this, reserved_words);
2533        }
2534
2535        Expression::OrderBy(ob) => {
2536            for o in &mut ob.expressions {
2537                quote_identifiers_recursive(&mut o.this, reserved_words);
2538            }
2539        }
2540
2541        Expression::Ordered(ord) => {
2542            quote_identifiers_recursive(&mut ord.this, reserved_words);
2543        }
2544
2545        Expression::Limit(lim) => {
2546            quote_identifiers_recursive(&mut lim.this, reserved_words);
2547        }
2548
2549        Expression::Offset(off) => {
2550            quote_identifiers_recursive(&mut off.this, reserved_words);
2551        }
2552        Expression::Fetch(fetch) => {
2553            if let Some(count) = &mut fetch.count {
2554                quote_identifiers_recursive(count, reserved_words);
2555            }
2556        }
2557
2558        Expression::Qualify(q) => {
2559            quote_identifiers_recursive(&mut q.this, reserved_words);
2560        }
2561
2562        Expression::Window(ws) => {
2563            for e in &mut ws.partition_by {
2564                quote_identifiers_recursive(e, reserved_words);
2565            }
2566            for o in &mut ws.order_by {
2567                quote_identifiers_recursive(&mut o.this, reserved_words);
2568            }
2569        }
2570
2571        Expression::Over(over) => {
2572            quote_over(over, reserved_words);
2573        }
2574
2575        Expression::WithinGroup(wg) => {
2576            quote_identifiers_recursive(&mut wg.this, reserved_words);
2577            for o in &mut wg.order_by {
2578                quote_identifiers_recursive(&mut o.this, reserved_words);
2579            }
2580        }
2581
2582        // ── Pivot / Unpivot ───────────────────────────────────────
2583        Expression::Pivot(piv) => {
2584            quote_identifiers_recursive(&mut piv.this, reserved_words);
2585            for e in &mut piv.expressions {
2586                quote_identifiers_recursive(e, reserved_words);
2587            }
2588            for f in &mut piv.fields {
2589                quote_identifiers_recursive(f, reserved_words);
2590            }
2591            if let Some(ref mut alias) = piv.alias {
2592                maybe_quote(alias, reserved_words);
2593            }
2594        }
2595
2596        Expression::Unpivot(unpiv) => {
2597            quote_identifiers_recursive(&mut unpiv.this, reserved_words);
2598            maybe_quote(&mut unpiv.value_column, reserved_words);
2599            maybe_quote(&mut unpiv.name_column, reserved_words);
2600            for e in &mut unpiv.columns {
2601                quote_identifiers_recursive(e, reserved_words);
2602            }
2603            if let Some(ref mut alias) = unpiv.alias {
2604                maybe_quote(alias, reserved_words);
2605            }
2606        }
2607
2608        // ── Values ────────────────────────────────────────────────
2609        Expression::Values(vals) => {
2610            for tuple in &mut vals.expressions {
2611                for e in &mut tuple.expressions {
2612                    quote_identifiers_recursive(e, reserved_words);
2613                }
2614            }
2615            if let Some(ref mut alias) = vals.alias {
2616                maybe_quote(alias, reserved_words);
2617            }
2618            for ca in &mut vals.column_aliases {
2619                maybe_quote(ca, reserved_words);
2620            }
2621        }
2622
2623        // ── Array / Struct / Tuple ────────────────────────────────
2624        Expression::Array(arr) => {
2625            for e in &mut arr.expressions {
2626                quote_identifiers_recursive(e, reserved_words);
2627            }
2628        }
2629
2630        Expression::Struct(st) => {
2631            for (_name, e) in &mut st.fields {
2632                quote_identifiers_recursive(e, reserved_words);
2633            }
2634        }
2635
2636        Expression::Tuple(tup) => {
2637            for e in &mut tup.expressions {
2638                quote_identifiers_recursive(e, reserved_words);
2639            }
2640        }
2641
2642        // ── Subscript / Dot / Method ──────────────────────────────
2643        Expression::Subscript(sub) => {
2644            quote_identifiers_recursive(&mut sub.this, reserved_words);
2645            quote_identifiers_recursive(&mut sub.index, reserved_words);
2646        }
2647
2648        Expression::Dot(dot) => {
2649            quote_identifiers_recursive(&mut dot.this, reserved_words);
2650            maybe_quote(&mut dot.field, reserved_words);
2651        }
2652
2653        Expression::ScopeResolution(sr) => {
2654            if let Some(ref mut this) = sr.this {
2655                quote_identifiers_recursive(this, reserved_words);
2656            }
2657            quote_identifiers_recursive(&mut sr.expression, reserved_words);
2658        }
2659
2660        // ── Lateral ───────────────────────────────────────────────
2661        Expression::Lateral(lat) => {
2662            quote_identifiers_recursive(&mut lat.this, reserved_words);
2663            // lat.alias is Option<String>, not Identifier, so we skip it
2664        }
2665
2666        // ── DPipe (|| concatenation) ──────────────────────────────
2667        Expression::DPipe(dpipe) => {
2668            quote_identifiers_recursive(&mut dpipe.this, reserved_words);
2669            quote_identifiers_recursive(&mut dpipe.expression, reserved_words);
2670        }
2671
2672        // ── Merge ─────────────────────────────────────────────────
2673        Expression::Merge(merge) => {
2674            quote_identifiers_recursive(&mut merge.this, reserved_words);
2675            quote_identifiers_recursive(&mut merge.using, reserved_words);
2676            if let Some(ref mut on) = merge.on {
2677                quote_identifiers_recursive(on, reserved_words);
2678            }
2679            if let Some(ref mut whens) = merge.whens {
2680                quote_identifiers_recursive(whens, reserved_words);
2681            }
2682            if let Some(ref mut with) = merge.with_ {
2683                quote_identifiers_recursive(with, reserved_words);
2684            }
2685            if let Some(ref mut ret) = merge.returning {
2686                quote_identifiers_recursive(ret, reserved_words);
2687            }
2688        }
2689
2690        // ── LateralView (standalone) ──────────────────────────────
2691        Expression::LateralView(lv) => {
2692            quote_lateral_view(lv, reserved_words);
2693        }
2694
2695        // ── Anonymous (generic function) ──────────────────────────
2696        Expression::Anonymous(anon) => {
2697            quote_identifiers_recursive(&mut anon.this, reserved_words);
2698            for e in &mut anon.expressions {
2699                quote_identifiers_recursive(e, reserved_words);
2700            }
2701        }
2702
2703        // ── Filter (e.g., FILTER(WHERE ...)) ──────────────────────
2704        Expression::Filter(filter) => {
2705            quote_identifiers_recursive(&mut filter.this, reserved_words);
2706            quote_identifiers_recursive(&mut filter.expression, reserved_words);
2707        }
2708
2709        // ── Returning ─────────────────────────────────────────────
2710        Expression::Returning(ret) => {
2711            for e in &mut ret.expressions {
2712                quote_identifiers_recursive(e, reserved_words);
2713            }
2714        }
2715
2716        // ── BracedWildcard ────────────────────────────────────────
2717        Expression::BracedWildcard(inner) => {
2718            quote_identifiers_recursive(inner, reserved_words);
2719        }
2720
2721        // ── ReturnStmt ────────────────────────────────────────────
2722        Expression::ReturnStmt(inner) => {
2723            quote_identifiers_recursive(inner, reserved_words);
2724        }
2725
2726        // ── Leaf nodes that never contain identifiers ─────────────
2727        Expression::Literal(_)
2728        | Expression::Boolean(_)
2729        | Expression::Null(_)
2730        | Expression::DataType(_)
2731        | Expression::Raw(_)
2732        | Expression::Placeholder(_)
2733        | Expression::CurrentDate(_)
2734        | Expression::CurrentTime(_)
2735        | Expression::CurrentTimestamp(_)
2736        | Expression::CurrentTimestampLTZ(_)
2737        | Expression::SessionUser(_)
2738        | Expression::RowNumber(_)
2739        | Expression::Rank(_)
2740        | Expression::DenseRank(_)
2741        | Expression::PercentRank(_)
2742        | Expression::CumeDist(_)
2743        | Expression::Random(_)
2744        | Expression::Pi(_)
2745        | Expression::JSONPathRoot(_) => {
2746            // Nothing to do – these are leaves or do not contain identifiers
2747        }
2748
2749        // ── Catch-all: many expression variants follow common patterns.
2750        // Rather than listing every single variant, we leave them unchanged.
2751        // The key identifier-bearing variants are covered above.
2752        _ => {}
2753    }
2754}
2755
2756/// Helper: quote identifiers in a Join.
2757fn quote_join(join: &mut Join, reserved_words: &HashSet<&str>) {
2758    quote_identifiers_recursive(&mut join.this, reserved_words);
2759    if let Some(ref mut on) = join.on {
2760        quote_identifiers_recursive(on, reserved_words);
2761    }
2762    for id in &mut join.using {
2763        maybe_quote(id, reserved_words);
2764    }
2765    if let Some(ref mut mc) = join.match_condition {
2766        quote_identifiers_recursive(mc, reserved_words);
2767    }
2768    for piv in &mut join.pivots {
2769        quote_identifiers_recursive(piv, reserved_words);
2770    }
2771}
2772
2773/// Helper: quote identifiers in a WITH clause.
2774fn quote_with(with: &mut With, reserved_words: &HashSet<&str>) {
2775    for cte in &mut with.ctes {
2776        maybe_quote(&mut cte.alias, reserved_words);
2777        for c in &mut cte.columns {
2778            maybe_quote(c, reserved_words);
2779        }
2780        for k in &mut cte.key_expressions {
2781            maybe_quote(k, reserved_words);
2782        }
2783        quote_identifiers_recursive(&mut cte.this, reserved_words);
2784    }
2785}
2786
2787/// Helper: quote identifiers in an Over clause.
2788fn quote_over(over: &mut Over, reserved_words: &HashSet<&str>) {
2789    if let Some(ref mut wn) = over.window_name {
2790        maybe_quote(wn, reserved_words);
2791    }
2792    for e in &mut over.partition_by {
2793        quote_identifiers_recursive(e, reserved_words);
2794    }
2795    for o in &mut over.order_by {
2796        quote_identifiers_recursive(&mut o.this, reserved_words);
2797    }
2798    if let Some(ref mut alias) = over.alias {
2799        maybe_quote(alias, reserved_words);
2800    }
2801}
2802
2803/// Helper: quote identifiers in a TableRef (used by DML statements).
2804fn quote_table_ref(table_ref: &mut TableRef, reserved_words: &HashSet<&str>) {
2805    maybe_quote(&mut table_ref.name, reserved_words);
2806    if let Some(ref mut schema) = table_ref.schema {
2807        maybe_quote(schema, reserved_words);
2808    }
2809    if let Some(ref mut catalog) = table_ref.catalog {
2810        maybe_quote(catalog, reserved_words);
2811    }
2812    if let Some(ref mut alias) = table_ref.alias {
2813        maybe_quote(alias, reserved_words);
2814    }
2815    for ca in &mut table_ref.column_aliases {
2816        maybe_quote(ca, reserved_words);
2817    }
2818    for p in &mut table_ref.partitions {
2819        maybe_quote(p, reserved_words);
2820    }
2821    for h in &mut table_ref.hints {
2822        quote_identifiers_recursive(h, reserved_words);
2823    }
2824}
2825
2826/// Helper: quote identifiers in a LateralView.
2827fn quote_lateral_view(lv: &mut LateralView, reserved_words: &HashSet<&str>) {
2828    quote_identifiers_recursive(&mut lv.this, reserved_words);
2829    if let Some(ref mut ta) = lv.table_alias {
2830        maybe_quote(ta, reserved_words);
2831    }
2832    for ca in &mut lv.column_aliases {
2833        maybe_quote(ca, reserved_words);
2834    }
2835}
2836
2837/// Quote identifiers that need quoting based on dialect rules.
2838///
2839/// Walks the entire AST recursively and sets `quoted = true` on any
2840/// `Identifier` that:
2841/// - contains special characters (anything not `[a-zA-Z0-9_]`)
2842/// - starts with a digit
2843/// - is a SQL reserved word for the given dialect
2844///
2845/// The function takes ownership of the expression, mutates a clone,
2846/// and returns the modified version.
2847pub fn quote_identifiers(expression: Expression, dialect: Option<DialectType>) -> Expression {
2848    let reserved_words = get_reserved_words(dialect);
2849    let mut result = expression;
2850    quote_identifiers_recursive(&mut result, &reserved_words);
2851    result
2852}
2853
2854/// Pushdown CTE alias columns into the projection.
2855///
2856/// This is useful for dialects like Snowflake where CTE alias columns
2857/// can be referenced in HAVING.
2858pub fn pushdown_cte_alias_columns(_scope: &Scope) {
2859    // Kept for API compatibility. The mutating implementation is applied within
2860    // `qualify_columns` where AST ownership is available.
2861}
2862
2863fn pushdown_cte_alias_columns_with(with: &mut With) {
2864    for cte in &mut with.ctes {
2865        if cte.columns.is_empty() {
2866            continue;
2867        }
2868
2869        if let Expression::Select(select) = &mut cte.this {
2870            let mut next_expressions = Vec::with_capacity(select.expressions.len());
2871
2872            for (i, projection) in select.expressions.iter().enumerate() {
2873                let Some(alias_name) = cte.columns.get(i) else {
2874                    next_expressions.push(projection.clone());
2875                    continue;
2876                };
2877
2878                match projection {
2879                    Expression::Alias(existing) => {
2880                        let mut aliased = existing.clone();
2881                        aliased.alias = alias_name.clone();
2882                        next_expressions.push(Expression::Alias(aliased));
2883                    }
2884                    _ => {
2885                        next_expressions.push(create_alias(projection.clone(), &alias_name.name));
2886                    }
2887                }
2888            }
2889
2890            select.expressions = next_expressions;
2891        }
2892    }
2893}
2894
2895// ============================================================================
2896// Helper functions
2897// ============================================================================
2898
2899/// Get all column references in a scope
2900fn get_scope_columns(scope: &Scope) -> Vec<ColumnRef> {
2901    let mut columns = Vec::new();
2902    collect_columns(&scope.expression, &mut columns);
2903    columns
2904}
2905
2906/// Column reference for tracking
2907#[derive(Debug, Clone)]
2908struct ColumnRef {
2909    table: Option<String>,
2910    name: String,
2911}
2912
2913/// Recursively collect column references from an expression
2914fn collect_columns(expr: &Expression, columns: &mut Vec<ColumnRef>) {
2915    match expr {
2916        Expression::Column(col) => {
2917            columns.push(ColumnRef {
2918                table: col.table.as_ref().map(|t| t.name.clone()),
2919                name: col.name.name.clone(),
2920            });
2921        }
2922        Expression::Select(select) => {
2923            for e in &select.expressions {
2924                collect_columns(e, columns);
2925            }
2926            if let Some(from) = &select.from {
2927                for e in &from.expressions {
2928                    collect_columns(e, columns);
2929                }
2930            }
2931            if let Some(where_clause) = &select.where_clause {
2932                collect_columns(&where_clause.this, columns);
2933            }
2934            if let Some(group_by) = &select.group_by {
2935                for e in &group_by.expressions {
2936                    collect_columns(e, columns);
2937                }
2938            }
2939            if let Some(having) = &select.having {
2940                collect_columns(&having.this, columns);
2941            }
2942            if let Some(order_by) = &select.order_by {
2943                for o in &order_by.expressions {
2944                    collect_columns(&o.this, columns);
2945                }
2946            }
2947            for join in &select.joins {
2948                collect_columns(&join.this, columns);
2949                if let Some(on) = &join.on {
2950                    collect_columns(on, columns);
2951                }
2952            }
2953        }
2954        Expression::Alias(alias) => {
2955            collect_columns(&alias.this, columns);
2956        }
2957        Expression::Function(func) => {
2958            for arg in &func.args {
2959                collect_columns(arg, columns);
2960            }
2961        }
2962        Expression::AggregateFunction(agg) => {
2963            for arg in &agg.args {
2964                collect_columns(arg, columns);
2965            }
2966        }
2967        Expression::And(bin)
2968        | Expression::Or(bin)
2969        | Expression::Eq(bin)
2970        | Expression::Neq(bin)
2971        | Expression::Lt(bin)
2972        | Expression::Lte(bin)
2973        | Expression::Gt(bin)
2974        | Expression::Gte(bin)
2975        | Expression::Add(bin)
2976        | Expression::Sub(bin)
2977        | Expression::Mul(bin)
2978        | Expression::Div(bin) => {
2979            collect_columns(&bin.left, columns);
2980            collect_columns(&bin.right, columns);
2981        }
2982        Expression::Not(unary) | Expression::Neg(unary) => {
2983            collect_columns(&unary.this, columns);
2984        }
2985        Expression::Paren(paren) => {
2986            collect_columns(&paren.this, columns);
2987        }
2988        Expression::Case(case) => {
2989            if let Some(operand) = &case.operand {
2990                collect_columns(operand, columns);
2991            }
2992            for (when, then) in &case.whens {
2993                collect_columns(when, columns);
2994                collect_columns(then, columns);
2995            }
2996            if let Some(else_) = &case.else_ {
2997                collect_columns(else_, columns);
2998            }
2999        }
3000        Expression::Cast(cast) => {
3001            collect_columns(&cast.this, columns);
3002        }
3003        Expression::In(in_expr) => {
3004            collect_columns(&in_expr.this, columns);
3005            for e in &in_expr.expressions {
3006                collect_columns(e, columns);
3007            }
3008            if let Some(query) = &in_expr.query {
3009                collect_columns(query, columns);
3010            }
3011        }
3012        Expression::Between(between) => {
3013            collect_columns(&between.this, columns);
3014            collect_columns(&between.low, columns);
3015            collect_columns(&between.high, columns);
3016        }
3017        Expression::Subquery(subquery) => {
3018            collect_columns(&subquery.this, columns);
3019        }
3020        _ => {}
3021    }
3022}
3023
3024/// Get unqualified columns in a scope
3025fn get_unqualified_columns(scope: &Scope) -> Vec<ColumnRef> {
3026    get_scope_columns(scope)
3027        .into_iter()
3028        .filter(|c| c.table.is_none())
3029        .collect()
3030}
3031
3032/// Get external columns (columns not resolvable in current scope)
3033fn get_external_columns(scope: &Scope) -> Vec<ColumnRef> {
3034    let source_names: HashSet<_> = scope.sources.keys().cloned().collect();
3035
3036    get_scope_columns(scope)
3037        .into_iter()
3038        .filter(|c| {
3039            if let Some(table) = &c.table {
3040                !source_names.contains(table)
3041            } else {
3042                false
3043            }
3044        })
3045        .collect()
3046}
3047
3048/// Check if a scope represents a correlated subquery
3049fn is_correlated_subquery(scope: &Scope) -> bool {
3050    scope.can_be_correlated && !get_external_columns(scope).is_empty()
3051}
3052
3053/// Check if a column represents a star (e.g., table.*)
3054fn is_star_column(col: &Column) -> bool {
3055    col.name.name == "*"
3056}
3057
3058/// Create a qualified column expression
3059fn create_qualified_column(name: &str, table: Option<&str>) -> Expression {
3060    Expression::boxed_column(Column {
3061        name: Identifier::new(name),
3062        table: table.map(Identifier::new),
3063        join_mark: false,
3064        trailing_comments: vec![],
3065        span: None,
3066        inferred_type: None,
3067    })
3068}
3069
3070/// Create an alias expression
3071fn create_alias(expr: Expression, alias_name: &str) -> Expression {
3072    Expression::Alias(Box::new(Alias {
3073        this: expr,
3074        alias: Identifier::new(alias_name),
3075        column_aliases: vec![],
3076        alias_explicit_as: false,
3077        alias_keyword: None,
3078        pre_alias_comments: vec![],
3079        trailing_comments: vec![],
3080        inferred_type: None,
3081    }))
3082}
3083
3084/// Get the output name for an expression
3085fn get_output_name(expr: &Expression) -> Option<String> {
3086    match expr {
3087        Expression::Column(col) => Some(col.name.name.clone()),
3088        Expression::Alias(alias) => Some(alias.alias.name.clone()),
3089        Expression::Identifier(id) => Some(id.name.clone()),
3090        _ => None,
3091    }
3092}
3093
3094#[cfg(test)]
3095mod tests {
3096    use super::*;
3097    use crate::expressions::DataType;
3098    use crate::generator::Generator;
3099    use crate::parser::Parser;
3100    use crate::scope::build_scope;
3101    use crate::{MappingSchema, Schema};
3102
3103    fn gen(expr: &Expression) -> String {
3104        Generator::new().generate(expr).unwrap()
3105    }
3106
3107    fn parse(sql: &str) -> Expression {
3108        Parser::parse_sql(sql).expect("Failed to parse")[0].clone()
3109    }
3110
3111    #[test]
3112    fn test_qualify_columns_options() {
3113        let options = QualifyColumnsOptions::new()
3114            .with_expand_alias_refs(true)
3115            .with_expand_stars(false)
3116            .with_dialect(DialectType::PostgreSQL)
3117            .with_allow_partial(true);
3118
3119        assert!(options.expand_alias_refs);
3120        assert!(!options.expand_stars);
3121        assert_eq!(options.dialect, Some(DialectType::PostgreSQL));
3122        assert!(options.allow_partial_qualification);
3123    }
3124
3125    #[test]
3126    fn test_qualify_cte_chain_uses_lexically_selected_sources() {
3127        let mut schema = MappingSchema::new();
3128        schema
3129            .add_table("raw_orders", &[("amount".into(), DataType::Text)], None)
3130            .unwrap();
3131        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";
3132        let qualified =
3133            qualify_columns(parse(sql), &schema, &QualifyColumnsOptions::new()).unwrap();
3134        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");
3135    }
3136
3137    #[test]
3138    fn test_qualify_does_not_capture_unrelated_schema_tables() {
3139        let mut schema = MappingSchema::new();
3140        schema
3141            .add_table("unrelated", &[("n".into(), DataType::Text)], None)
3142            .unwrap();
3143        for sql in ["SELECT n FROM missing", "SELECT unrelated.n FROM missing"] {
3144            assert!(
3145                qualify_columns(parse(sql), &schema, &QualifyColumnsOptions::new()).is_err(),
3146                "{sql}"
3147            );
3148        }
3149        let sql = "SELECT n FROM missing";
3150        let qualified = qualify_columns(
3151            parse(sql),
3152            &schema,
3153            &QualifyColumnsOptions::new().with_allow_partial(true),
3154        )
3155        .unwrap();
3156        assert_eq!(gen(&qualified), "SELECT n AS n FROM missing");
3157        assert!(matches!(
3158            qualify_columns(
3159                parse(
3160                    "WITH a AS (SELECT 1 AS n), b AS (SELECT 2 AS n) SELECT n FROM a CROSS JOIN b"
3161                ),
3162                &schema,
3163                &QualifyColumnsOptions::new()
3164            ),
3165            Err(QualifyColumnsError::AmbiguousColumn(_))
3166        ));
3167    }
3168
3169    #[test]
3170    fn test_qualify_correlated_cte_alias_and_nested_shadowing() {
3171        let schema = MappingSchema::new();
3172        for (sql, expected) in [
3173            ("WITH a AS (SELECT 1 AS n) SELECT (SELECT n) AS result FROM a AS outer_a", "(SELECT outer_a.n AS n)"),
3174            ("WITH a AS (SELECT 1 AS n) SELECT (SELECT (SELECT n)) AS result FROM a AS outer_a", "(SELECT outer_a.n AS n)"),
3175            ("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"),
3176            ("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"),
3177            ("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"),
3178            ("WITH a AS (SELECT 1 AS n) SELECT b.n FROM a, LATERAL (SELECT a.n) AS b", "SELECT a.n AS n"),
3179        ] {
3180            let qualified = qualify_columns(parse(sql), &schema, &QualifyColumnsOptions::new())
3181                .unwrap_or_else(|error| panic!("{sql}: {error}"));
3182            assert!(gen(&qualified).contains(expected), "{}", gen(&qualified));
3183        }
3184    }
3185
3186    #[test]
3187    fn test_get_scope_columns() {
3188        let expr = parse("SELECT a, b FROM t WHERE c = 1");
3189        let scope = build_scope(&expr);
3190        let columns = get_scope_columns(&scope);
3191
3192        assert!(columns.iter().any(|c| c.name == "a"));
3193        assert!(columns.iter().any(|c| c.name == "b"));
3194        assert!(columns.iter().any(|c| c.name == "c"));
3195    }
3196
3197    #[test]
3198    fn test_get_unqualified_columns() {
3199        let expr = parse("SELECT t.a, b FROM t");
3200        let scope = build_scope(&expr);
3201        let unqualified = get_unqualified_columns(&scope);
3202
3203        // Only 'b' should be unqualified
3204        assert!(unqualified.iter().any(|c| c.name == "b"));
3205        assert!(!unqualified.iter().any(|c| c.name == "a"));
3206    }
3207
3208    #[test]
3209    fn test_is_star_column() {
3210        let col = Column {
3211            name: Identifier::new("*"),
3212            table: Some(Identifier::new("t")),
3213            join_mark: false,
3214            trailing_comments: vec![],
3215            span: None,
3216            inferred_type: None,
3217        };
3218        assert!(is_star_column(&col));
3219
3220        let col2 = Column {
3221            name: Identifier::new("id"),
3222            table: None,
3223            join_mark: false,
3224            trailing_comments: vec![],
3225            span: None,
3226            inferred_type: None,
3227        };
3228        assert!(!is_star_column(&col2));
3229    }
3230
3231    #[test]
3232    fn test_create_qualified_column() {
3233        let expr = create_qualified_column("id", Some("users"));
3234        let sql = gen(&expr);
3235        assert!(sql.contains("users"));
3236        assert!(sql.contains("id"));
3237    }
3238
3239    #[test]
3240    fn test_create_alias() {
3241        let col = Expression::boxed_column(Column {
3242            name: Identifier::new("value"),
3243            table: None,
3244            join_mark: false,
3245            trailing_comments: vec![],
3246            span: None,
3247            inferred_type: None,
3248        });
3249        let aliased = create_alias(col, "total");
3250        let sql = gen(&aliased);
3251        assert!(sql.contains("AS") || sql.contains("total"));
3252    }
3253
3254    #[test]
3255    fn test_qualify_columns_normalizes_struct_field_access_issue_408() {
3256        let struct_type = DataType::Struct {
3257            fields: vec![crate::expressions::StructField::new(
3258                "field_value".into(),
3259                DataType::Text,
3260            )],
3261            nested: true,
3262        };
3263        let mut schema = MappingSchema::with_dialect(DialectType::DuckDB);
3264        schema
3265            .add_table(
3266                "source_table",
3267                &[
3268                    ("composite_value".into(), struct_type.clone()),
3269                    (
3270                        "nested_items".into(),
3271                        DataType::Array {
3272                            element_type: Box::new(struct_type),
3273                            dimension: None,
3274                        },
3275                    ),
3276                ],
3277                None,
3278            )
3279            .expect("schema setup");
3280
3281        let cases = [
3282            (
3283                "SELECT composite_value.field_value AS output_value FROM source_table",
3284                "source_table",
3285                "composite_value",
3286            ),
3287            (
3288                "SELECT item.field_value AS output_value FROM source_table s \
3289                 CROSS JOIN UNNEST(s.nested_items) AS expanded(item)",
3290                "expanded",
3291                "item",
3292            ),
3293        ];
3294
3295        for (sql, expected_table, expected_column) in cases {
3296            let qualified = qualify_columns(
3297                parse(sql),
3298                &schema,
3299                &QualifyColumnsOptions::new().with_dialect(DialectType::DuckDB),
3300            )
3301            .unwrap_or_else(|error| panic!("qualification failed for {sql:?}: {error}"));
3302            let Expression::Select(select) = qualified else {
3303                panic!("expected SELECT");
3304            };
3305            let Expression::Alias(alias) = &select.expressions[0] else {
3306                panic!("expected aliased projection");
3307            };
3308            let Expression::Dot(dot) = &alias.this else {
3309                panic!("expected normalized Dot, got {:?}", alias.this);
3310            };
3311            let Expression::Column(base) = &dot.this else {
3312                panic!("expected column-backed Dot");
3313            };
3314
3315            assert_eq!(
3316                base.table.as_ref().map(|table| table.name.as_str()),
3317                Some(expected_table)
3318            );
3319            assert_eq!(base.name.name, expected_column);
3320            assert_eq!(dot.field.name, "field_value");
3321        }
3322    }
3323
3324    #[test]
3325    fn test_validate_qualify_columns_success() {
3326        // All columns qualified
3327        let expr = parse("SELECT t.a, t.b FROM t");
3328        let result = validate_qualify_columns(&expr);
3329        // This may or may not error depending on scope analysis
3330        // The test verifies the function runs without panic
3331        let _ = result;
3332    }
3333
3334    #[test]
3335    fn test_collect_columns_nested() {
3336        let expr = parse("SELECT a + b, c FROM t WHERE d > 0 GROUP BY e HAVING f = 1");
3337        let mut columns = Vec::new();
3338        collect_columns(&expr, &mut columns);
3339
3340        let names: Vec<_> = columns.iter().map(|c| c.name.as_str()).collect();
3341        assert!(names.contains(&"a"));
3342        assert!(names.contains(&"b"));
3343        assert!(names.contains(&"c"));
3344        assert!(names.contains(&"d"));
3345        assert!(names.contains(&"e"));
3346        assert!(names.contains(&"f"));
3347    }
3348
3349    #[test]
3350    fn test_collect_columns_in_case() {
3351        let expr = parse("SELECT CASE WHEN a = 1 THEN b ELSE c END FROM t");
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    }
3360
3361    #[test]
3362    fn test_collect_columns_in_subquery() {
3363        let expr = parse("SELECT a FROM t WHERE b IN (SELECT c FROM s)");
3364        let mut columns = Vec::new();
3365        collect_columns(&expr, &mut columns);
3366
3367        let names: Vec<_> = columns.iter().map(|c| c.name.as_str()).collect();
3368        assert!(names.contains(&"a"));
3369        assert!(names.contains(&"b"));
3370        assert!(names.contains(&"c"));
3371    }
3372
3373    #[test]
3374    fn test_qualify_outputs_basic() {
3375        let expr = parse("SELECT a, b + c FROM t");
3376        let scope = build_scope(&expr);
3377        let result = qualify_outputs(&scope);
3378        assert!(result.is_ok());
3379    }
3380
3381    #[test]
3382    fn test_qualify_columns_expands_star_with_schema() {
3383        let expr = parse("SELECT * FROM users");
3384
3385        let mut schema = MappingSchema::new();
3386        schema
3387            .add_table(
3388                "users",
3389                &[
3390                    (
3391                        "id".to_string(),
3392                        DataType::Int {
3393                            length: None,
3394                            integer_spelling: false,
3395                        },
3396                    ),
3397                    ("name".to_string(), DataType::Text),
3398                    ("email".to_string(), DataType::Text),
3399                ],
3400                None,
3401            )
3402            .expect("schema setup");
3403
3404        let result =
3405            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3406        let sql = gen(&result);
3407
3408        assert!(!sql.contains("SELECT *"));
3409        assert!(sql.contains("users.id"));
3410        assert!(sql.contains("users.name"));
3411        assert!(sql.contains("users.email"));
3412    }
3413
3414    #[test]
3415    fn test_qualify_columns_expands_group_by_positions() {
3416        let expr = parse("SELECT a, b FROM t GROUP BY 1, 2");
3417
3418        let mut schema = MappingSchema::new();
3419        schema
3420            .add_table(
3421                "t",
3422                &[
3423                    (
3424                        "a".to_string(),
3425                        DataType::Int {
3426                            length: None,
3427                            integer_spelling: false,
3428                        },
3429                    ),
3430                    (
3431                        "b".to_string(),
3432                        DataType::Int {
3433                            length: None,
3434                            integer_spelling: false,
3435                        },
3436                    ),
3437                ],
3438                None,
3439            )
3440            .expect("schema setup");
3441
3442        let result =
3443            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3444        let sql = gen(&result);
3445
3446        assert!(!sql.contains("GROUP BY 1"));
3447        assert!(!sql.contains("GROUP BY 2"));
3448        assert!(sql.contains("GROUP BY"));
3449        assert!(sql.contains("t.a"));
3450        assert!(sql.contains("t.b"));
3451    }
3452
3453    // ======================================================================
3454    // USING expansion tests
3455    // ======================================================================
3456
3457    #[test]
3458    fn test_expand_using_simple() {
3459        // Already-qualified column: USING→ON rewrite but no COALESCE needed
3460        let expr = parse("SELECT x.b FROM x JOIN y USING (b)");
3461
3462        let mut schema = MappingSchema::new();
3463        schema
3464            .add_table(
3465                "x",
3466                &[
3467                    ("a".to_string(), DataType::BigInt { length: None }),
3468                    ("b".to_string(), DataType::BigInt { length: None }),
3469                ],
3470                None,
3471            )
3472            .expect("schema setup");
3473        schema
3474            .add_table(
3475                "y",
3476                &[
3477                    ("b".to_string(), DataType::BigInt { length: None }),
3478                    ("c".to_string(), DataType::BigInt { length: None }),
3479                ],
3480                None,
3481            )
3482            .expect("schema setup");
3483
3484        let result =
3485            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3486        let sql = gen(&result);
3487
3488        // USING should be replaced with ON
3489        assert!(
3490            !sql.contains("USING"),
3491            "USING should be replaced with ON: {sql}"
3492        );
3493        assert!(
3494            sql.contains("ON x.b = y.b"),
3495            "ON condition should be x.b = y.b: {sql}"
3496        );
3497        // x.b in SELECT should remain as-is (already qualified)
3498        assert!(sql.contains("SELECT x.b"), "SELECT should keep x.b: {sql}");
3499    }
3500
3501    #[test]
3502    fn test_expand_using_unqualified_coalesce() {
3503        // Unqualified USING column in SELECT should become COALESCE
3504        let expr = parse("SELECT b FROM x JOIN y USING(b)");
3505
3506        let mut schema = MappingSchema::new();
3507        schema
3508            .add_table(
3509                "x",
3510                &[
3511                    ("a".to_string(), DataType::BigInt { length: None }),
3512                    ("b".to_string(), DataType::BigInt { length: None }),
3513                ],
3514                None,
3515            )
3516            .expect("schema setup");
3517        schema
3518            .add_table(
3519                "y",
3520                &[
3521                    ("b".to_string(), DataType::BigInt { length: None }),
3522                    ("c".to_string(), DataType::BigInt { length: None }),
3523                ],
3524                None,
3525            )
3526            .expect("schema setup");
3527
3528        let result =
3529            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3530        let sql = gen(&result);
3531
3532        assert!(
3533            sql.contains("COALESCE(x.b, y.b)"),
3534            "Unqualified USING column should become COALESCE: {sql}"
3535        );
3536        assert!(
3537            sql.contains("AS b"),
3538            "COALESCE should be aliased as 'b': {sql}"
3539        );
3540        assert!(
3541            sql.contains("ON x.b = y.b"),
3542            "ON condition should be generated: {sql}"
3543        );
3544    }
3545
3546    #[test]
3547    fn test_expand_using_with_where() {
3548        // USING column in WHERE should become COALESCE
3549        let expr = parse("SELECT b FROM x JOIN y USING(b) WHERE b = 1");
3550
3551        let mut schema = MappingSchema::new();
3552        schema
3553            .add_table(
3554                "x",
3555                &[("b".to_string(), DataType::BigInt { length: None })],
3556                None,
3557            )
3558            .expect("schema setup");
3559        schema
3560            .add_table(
3561                "y",
3562                &[("b".to_string(), DataType::BigInt { length: None })],
3563                None,
3564            )
3565            .expect("schema setup");
3566
3567        let result =
3568            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3569        let sql = gen(&result);
3570
3571        assert!(
3572            sql.contains("WHERE COALESCE(x.b, y.b)"),
3573            "WHERE should use COALESCE for USING column: {sql}"
3574        );
3575    }
3576
3577    #[test]
3578    fn test_expand_using_multi_join() {
3579        // Three-way join with same USING column
3580        let expr = parse("SELECT b FROM x JOIN y USING(b) JOIN z USING(b)");
3581
3582        let mut schema = MappingSchema::new();
3583        for table in &["x", "y", "z"] {
3584            schema
3585                .add_table(
3586                    table,
3587                    &[("b".to_string(), DataType::BigInt { length: None })],
3588                    None,
3589                )
3590                .expect("schema setup");
3591        }
3592
3593        let result =
3594            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3595        let sql = gen(&result);
3596
3597        // SELECT should have 3-table COALESCE
3598        assert!(
3599            sql.contains("COALESCE(x.b, y.b, z.b)"),
3600            "Should have 3-table COALESCE: {sql}"
3601        );
3602        // First join: simple ON
3603        assert!(
3604            sql.contains("ON x.b = y.b"),
3605            "First join ON condition: {sql}"
3606        );
3607    }
3608
3609    #[test]
3610    fn test_expand_using_multi_column() {
3611        // Two USING columns
3612        let expr = parse("SELECT b, c FROM y JOIN z USING(b, c)");
3613
3614        let mut schema = MappingSchema::new();
3615        schema
3616            .add_table(
3617                "y",
3618                &[
3619                    ("b".to_string(), DataType::BigInt { length: None }),
3620                    ("c".to_string(), DataType::BigInt { length: None }),
3621                ],
3622                None,
3623            )
3624            .expect("schema setup");
3625        schema
3626            .add_table(
3627                "z",
3628                &[
3629                    ("b".to_string(), DataType::BigInt { length: None }),
3630                    ("c".to_string(), DataType::BigInt { length: None }),
3631                ],
3632                None,
3633            )
3634            .expect("schema setup");
3635
3636        let result =
3637            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3638        let sql = gen(&result);
3639
3640        assert!(
3641            sql.contains("COALESCE(y.b, z.b)"),
3642            "column 'b' should get COALESCE: {sql}"
3643        );
3644        assert!(
3645            sql.contains("COALESCE(y.c, z.c)"),
3646            "column 'c' should get COALESCE: {sql}"
3647        );
3648        // ON should have both conditions ANDed
3649        assert!(
3650            sql.contains("y.b = z.b") && sql.contains("y.c = z.c"),
3651            "ON should have both equality conditions: {sql}"
3652        );
3653    }
3654
3655    #[test]
3656    fn test_expand_using_star() {
3657        // SELECT * should deduplicate USING columns
3658        let expr = parse("SELECT * FROM x JOIN y USING(b)");
3659
3660        let mut schema = MappingSchema::new();
3661        schema
3662            .add_table(
3663                "x",
3664                &[
3665                    ("a".to_string(), DataType::BigInt { length: None }),
3666                    ("b".to_string(), DataType::BigInt { length: None }),
3667                ],
3668                None,
3669            )
3670            .expect("schema setup");
3671        schema
3672            .add_table(
3673                "y",
3674                &[
3675                    ("b".to_string(), DataType::BigInt { length: None }),
3676                    ("c".to_string(), DataType::BigInt { length: None }),
3677                ],
3678                None,
3679            )
3680            .expect("schema setup");
3681
3682        let result =
3683            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3684        let sql = gen(&result);
3685
3686        // b should appear once as COALESCE
3687        assert!(
3688            sql.contains("COALESCE(x.b, y.b) AS b"),
3689            "USING column should be COALESCE in star expansion: {sql}"
3690        );
3691        // a and c should be normal qualified columns
3692        assert!(sql.contains("x.a"), "non-USING column a from x: {sql}");
3693        assert!(sql.contains("y.c"), "non-USING column c from y: {sql}");
3694        // b should only appear once (not duplicated from both tables)
3695        let coalesce_count = sql.matches("COALESCE").count();
3696        assert_eq!(
3697            coalesce_count, 1,
3698            "b should appear only once as COALESCE: {sql}"
3699        );
3700    }
3701
3702    #[test]
3703    fn test_expand_using_table_star() {
3704        // table.* with USING column
3705        let expr = parse("SELECT x.* FROM x JOIN y USING(b)");
3706
3707        let mut schema = MappingSchema::new();
3708        schema
3709            .add_table(
3710                "x",
3711                &[
3712                    ("a".to_string(), DataType::BigInt { length: None }),
3713                    ("b".to_string(), DataType::BigInt { length: None }),
3714                ],
3715                None,
3716            )
3717            .expect("schema setup");
3718        schema
3719            .add_table(
3720                "y",
3721                &[
3722                    ("b".to_string(), DataType::BigInt { length: None }),
3723                    ("c".to_string(), DataType::BigInt { length: None }),
3724                ],
3725                None,
3726            )
3727            .expect("schema setup");
3728
3729        let result =
3730            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3731        let sql = gen(&result);
3732
3733        // b should become COALESCE (since x participates in USING for b)
3734        assert!(
3735            sql.contains("COALESCE(x.b, y.b)"),
3736            "USING column from x.* should become COALESCE: {sql}"
3737        );
3738        assert!(sql.contains("x.a"), "non-USING column a: {sql}");
3739    }
3740
3741    #[test]
3742    fn test_expand_natural_join_with_derived_table() {
3743        let expr = parse(
3744            "SELECT shared_key AS output_key FROM source_table \
3745             NATURAL JOIN (SELECT shared_key FROM source_table) AS derived",
3746        );
3747
3748        let mut schema = MappingSchema::new();
3749        schema
3750            .add_table(
3751                "source_table",
3752                &[(
3753                    "shared_key".to_string(),
3754                    DataType::VarChar {
3755                        length: None,
3756                        parenthesized_length: false,
3757                    },
3758                )],
3759                None,
3760            )
3761            .expect("schema setup");
3762
3763        let result = qualify_columns(
3764            expr,
3765            &schema,
3766            &QualifyColumnsOptions::new().with_dialect(DialectType::DuckDB),
3767        )
3768        .expect("qualify");
3769        let sql = gen(&result);
3770
3771        assert!(!sql.contains("NATURAL"), "NATURAL should expand: {sql}");
3772        assert!(
3773            sql.contains("ON source_table.shared_key = derived.shared_key"),
3774            "common column should become an equality condition: {sql}"
3775        );
3776        assert!(
3777            sql.contains("COALESCE(source_table.shared_key, derived.shared_key) AS output_key"),
3778            "merged projection should preserve both sources: {sql}"
3779        );
3780    }
3781
3782    #[test]
3783    fn test_expand_natural_join_star_and_chained_sources() {
3784        let expr = parse("SELECT * FROM x NATURAL JOIN y NATURAL JOIN z");
3785
3786        let mut schema = MappingSchema::new();
3787        schema
3788            .add_table(
3789                "x",
3790                &[
3791                    ("a".to_string(), DataType::BigInt { length: None }),
3792                    ("shared".to_string(), DataType::BigInt { length: None }),
3793                ],
3794                None,
3795            )
3796            .expect("schema setup");
3797        schema
3798            .add_table(
3799                "y",
3800                &[
3801                    ("shared".to_string(), DataType::BigInt { length: None }),
3802                    ("b".to_string(), DataType::BigInt { length: None }),
3803                ],
3804                None,
3805            )
3806            .expect("schema setup");
3807        schema
3808            .add_table(
3809                "z",
3810                &[
3811                    ("shared".to_string(), DataType::BigInt { length: None }),
3812                    ("c".to_string(), DataType::BigInt { length: None }),
3813                ],
3814                None,
3815            )
3816            .expect("schema setup");
3817
3818        let result =
3819            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3820        let sql = gen(&result);
3821
3822        assert!(!sql.contains("NATURAL"), "both joins should expand: {sql}");
3823        assert!(sql.contains("ON x.shared = y.shared"), "first join: {sql}");
3824        assert!(sql.contains("ON x.shared = z.shared"), "second join: {sql}");
3825        assert_eq!(
3826            sql.matches("COALESCE(x.shared, y.shared, z.shared) AS shared")
3827                .count(),
3828            1,
3829            "merged star column should be emitted once: {sql}"
3830        );
3831        assert!(sql.contains("x.a"), "left-only column should remain: {sql}");
3832        assert!(
3833            sql.contains("y.b"),
3834            "middle-only column should remain: {sql}"
3835        );
3836        assert!(
3837            sql.contains("z.c"),
3838            "right-only column should remain: {sql}"
3839        );
3840    }
3841
3842    #[test]
3843    fn test_expand_natural_outer_join_kinds() {
3844        let mut schema = MappingSchema::new();
3845        for table in ["x", "y"] {
3846            schema
3847                .add_table(
3848                    table,
3849                    &[("shared".to_string(), DataType::BigInt { length: None })],
3850                    None,
3851                )
3852                .expect("schema setup");
3853        }
3854
3855        for (input_kind, output_kind) in [
3856            ("NATURAL LEFT JOIN", "LEFT JOIN"),
3857            ("NATURAL RIGHT JOIN", "RIGHT JOIN"),
3858            ("NATURAL FULL JOIN", "FULL JOIN"),
3859        ] {
3860            let expr = parse(&format!("SELECT shared FROM x {input_kind} y"));
3861            let result =
3862                qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3863            let sql = gen(&result);
3864
3865            assert!(
3866                !sql.contains("NATURAL"),
3867                "{input_kind} should expand: {sql}"
3868            );
3869            assert!(sql.contains(output_kind), "join kind should remain: {sql}");
3870            assert!(
3871                sql.contains("ON x.shared = y.shared"),
3872                "join condition should be derived: {sql}"
3873            );
3874        }
3875    }
3876
3877    #[test]
3878    fn test_preserve_natural_join_without_known_common_columns() {
3879        let mut no_common_schema = MappingSchema::new();
3880        no_common_schema
3881            .add_table(
3882                "x",
3883                &[("a".to_string(), DataType::BigInt { length: None })],
3884                None,
3885            )
3886            .expect("schema setup");
3887        no_common_schema
3888            .add_table(
3889                "y",
3890                &[("b".to_string(), DataType::BigInt { length: None })],
3891                None,
3892            )
3893            .expect("schema setup");
3894
3895        let no_common = qualify_columns(
3896            parse("SELECT * FROM x NATURAL JOIN y"),
3897            &no_common_schema,
3898            &QualifyColumnsOptions::new(),
3899        )
3900        .expect("qualify");
3901        assert!(
3902            gen(&no_common).contains("NATURAL JOIN"),
3903            "join without common columns should remain NATURAL"
3904        );
3905
3906        let mut partial_schema = MappingSchema::new();
3907        partial_schema
3908            .add_table(
3909                "x",
3910                &[("a".to_string(), DataType::BigInt { length: None })],
3911                None,
3912            )
3913            .expect("schema setup");
3914        let unknown_right = qualify_columns(
3915            parse("SELECT * FROM x NATURAL JOIN unknown_table"),
3916            &partial_schema,
3917            &QualifyColumnsOptions::new().with_allow_partial(true),
3918        )
3919        .expect("partial qualification");
3920        assert!(
3921            gen(&unknown_right).contains("NATURAL JOIN"),
3922            "join with an unknown schema should remain NATURAL"
3923        );
3924
3925        let unknown_left = qualify_columns(
3926            parse("SELECT * FROM unknown_table NATURAL JOIN x"),
3927            &partial_schema,
3928            &QualifyColumnsOptions::new().with_allow_partial(true),
3929        )
3930        .expect("partial qualification");
3931        assert!(
3932            gen(&unknown_left).contains("NATURAL JOIN"),
3933            "join with an unknown left schema should remain NATURAL"
3934        );
3935
3936        let partially_known_chain = qualify_columns(
3937            parse("SELECT * FROM x NATURAL JOIN unknown_table NATURAL JOIN x AS x2"),
3938            &partial_schema,
3939            &QualifyColumnsOptions::new().with_allow_partial(true),
3940        )
3941        .expect("partial qualification");
3942        assert_eq!(
3943            gen(&partially_known_chain).matches("NATURAL JOIN").count(),
3944            2,
3945            "an unknown earlier source should prevent later inferred join keys"
3946        );
3947    }
3948
3949    #[test]
3950    fn test_qualify_columns_qualified_table_name() {
3951        let expr = parse("SELECT a FROM raw.t1");
3952
3953        let mut schema = MappingSchema::new();
3954        schema
3955            .add_table(
3956                "raw.t1",
3957                &[("a".to_string(), DataType::BigInt { length: None })],
3958                None,
3959            )
3960            .expect("schema setup");
3961
3962        let result =
3963            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3964        let sql = gen(&result);
3965
3966        assert!(
3967            sql.contains("t1.a"),
3968            "column should be qualified with table name: {sql}"
3969        );
3970
3971        // test that columns in agg functions also get qualified
3972        let expr = parse("SELECT MAX(a) FROM raw.t1");
3973        let result =
3974            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3975        let sql = gen(&result);
3976        assert!(
3977            sql.contains("t1.a"),
3978            "column in function should be qualified with table name: {sql}"
3979        );
3980
3981        // test that columns in scalar functions also get qualified
3982        let expr = parse("SELECT ABS(a) FROM raw.t1");
3983        let result =
3984            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
3985        let sql = gen(&result);
3986        assert!(
3987            sql.contains("t1.a"),
3988            "column in function should be qualified with table name: {sql}"
3989        );
3990    }
3991
3992    #[test]
3993    fn test_qualify_columns_count_star() {
3994        // COUNT(*) uses Count { this: None } — verify qualify_columns handles it without panic
3995        let expr = parse("SELECT COUNT(*) FROM t1");
3996
3997        let mut schema = MappingSchema::new();
3998        schema
3999            .add_table(
4000                "t1",
4001                &[("id".to_string(), DataType::BigInt { length: None })],
4002                None,
4003            )
4004            .expect("schema setup");
4005
4006        let result =
4007            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
4008        let sql = gen(&result);
4009
4010        assert!(
4011            sql.contains("COUNT(*)"),
4012            "COUNT(*) should be preserved: {sql}"
4013        );
4014    }
4015
4016    #[test]
4017    fn test_qualify_columns_correlated_scalar_subquery() {
4018        let expr =
4019            parse("SELECT id, (SELECT AVG(val) FROM t2 WHERE t2.id = t1.id) AS avg_val FROM t1");
4020
4021        let mut schema = MappingSchema::new();
4022        schema
4023            .add_table(
4024                "t1",
4025                &[("id".to_string(), DataType::BigInt { length: None })],
4026                None,
4027            )
4028            .expect("schema setup");
4029        schema
4030            .add_table(
4031                "t2",
4032                &[
4033                    ("id".to_string(), DataType::BigInt { length: None }),
4034                    ("val".to_string(), DataType::BigInt { length: None }),
4035                ],
4036                None,
4037            )
4038            .expect("schema setup");
4039
4040        let result =
4041            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
4042        let sql = gen(&result);
4043
4044        assert!(
4045            sql.contains("t1.id"),
4046            "outer column should be qualified: {sql}"
4047        );
4048        assert!(
4049            sql.contains("t2.id"),
4050            "inner column should be qualified: {sql}"
4051        );
4052    }
4053
4054    #[test]
4055    fn test_qualify_columns_correlated_scalar_subquery_unqualified() {
4056        let expr =
4057            parse("SELECT t1_id, (SELECT AVG(val) FROM t2 WHERE t2_id = t1_id) AS avg_val FROM t1");
4058
4059        let mut schema = MappingSchema::new();
4060        schema
4061            .add_table(
4062                "t1",
4063                &[("t1_id".to_string(), DataType::BigInt { length: None })],
4064                None,
4065            )
4066            .expect("schema setup");
4067        schema
4068            .add_table(
4069                "t2",
4070                &[
4071                    ("t2_id".to_string(), DataType::BigInt { length: None }),
4072                    ("val".to_string(), DataType::BigInt { length: None }),
4073                ],
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("t1.t1_id"),
4084            "outer column should be qualified: {sql}"
4085        );
4086        assert!(
4087            sql.contains("t2.t2_id"),
4088            "inner column should be qualified: {sql}"
4089        );
4090        // Correlated reference t1_id in inner scope should be qualified as t1.t1_id
4091        assert!(
4092            sql.contains("= t1.t1_id"),
4093            "correlated column should be qualified: {sql}"
4094        );
4095    }
4096
4097    #[test]
4098    fn test_qualify_columns_correlated_exists_subquery() {
4099        let expr = parse(
4100            "SELECT o_orderpriority FROM orders \
4101             WHERE EXISTS (SELECT * FROM lineitem WHERE l_orderkey = o_orderkey)",
4102        );
4103
4104        let mut schema = MappingSchema::new();
4105        schema
4106            .add_table(
4107                "orders",
4108                &[
4109                    ("o_orderpriority".to_string(), DataType::Text),
4110                    ("o_orderkey".to_string(), DataType::BigInt { length: None }),
4111                ],
4112                None,
4113            )
4114            .expect("schema setup");
4115        schema
4116            .add_table(
4117                "lineitem",
4118                &[("l_orderkey".to_string(), DataType::BigInt { length: None })],
4119                None,
4120            )
4121            .expect("schema setup");
4122
4123        let result =
4124            qualify_columns(expr, &schema, &QualifyColumnsOptions::new()).expect("qualify");
4125        let sql = gen(&result);
4126
4127        assert!(
4128            sql.contains("orders.o_orderpriority"),
4129            "outer column should be qualified: {sql}"
4130        );
4131        assert!(
4132            sql.contains("lineitem.l_orderkey"),
4133            "inner column should be qualified: {sql}"
4134        );
4135        assert!(
4136            sql.contains("orders.o_orderkey"),
4137            "correlated outer column should be qualified: {sql}"
4138        );
4139    }
4140
4141    #[test]
4142    fn test_qualify_columns_rejects_unknown_table() {
4143        let expr = parse("SELECT id FROM t1 WHERE nonexistent.col = 1");
4144
4145        let mut schema = MappingSchema::new();
4146        schema
4147            .add_table(
4148                "t1",
4149                &[("id".to_string(), DataType::BigInt { length: None })],
4150                None,
4151            )
4152            .expect("schema setup");
4153
4154        let result = qualify_columns(expr, &schema, &QualifyColumnsOptions::new());
4155        assert!(
4156            result.is_err(),
4157            "should reject reference to table not in scope or schema"
4158        );
4159    }
4160
4161    // ======================================================================
4162    // quote_identifiers tests
4163    // ======================================================================
4164
4165    #[test]
4166    fn test_needs_quoting_reserved_word() {
4167        let reserved = get_reserved_words(None);
4168        assert!(needs_quoting("select", &reserved));
4169        assert!(needs_quoting("SELECT", &reserved));
4170        assert!(needs_quoting("from", &reserved));
4171        assert!(needs_quoting("WHERE", &reserved));
4172        assert!(needs_quoting("join", &reserved));
4173        assert!(needs_quoting("table", &reserved));
4174    }
4175
4176    #[test]
4177    fn test_needs_quoting_normal_identifiers() {
4178        let reserved = get_reserved_words(None);
4179        assert!(!needs_quoting("foo", &reserved));
4180        assert!(!needs_quoting("my_column", &reserved));
4181        assert!(!needs_quoting("col1", &reserved));
4182        assert!(!needs_quoting("A", &reserved));
4183        assert!(!needs_quoting("_hidden", &reserved));
4184    }
4185
4186    #[test]
4187    fn test_needs_quoting_special_characters() {
4188        let reserved = get_reserved_words(None);
4189        assert!(needs_quoting("my column", &reserved)); // space
4190        assert!(needs_quoting("my-column", &reserved)); // hyphen
4191        assert!(needs_quoting("my.column", &reserved)); // dot
4192        assert!(needs_quoting("col@name", &reserved)); // at sign
4193        assert!(needs_quoting("col#name", &reserved)); // hash
4194    }
4195
4196    #[test]
4197    fn test_needs_quoting_starts_with_digit() {
4198        let reserved = get_reserved_words(None);
4199        assert!(needs_quoting("1col", &reserved));
4200        assert!(needs_quoting("123", &reserved));
4201        assert!(needs_quoting("0_start", &reserved));
4202    }
4203
4204    #[test]
4205    fn test_needs_quoting_empty() {
4206        let reserved = get_reserved_words(None);
4207        assert!(!needs_quoting("", &reserved));
4208    }
4209
4210    #[test]
4211    fn test_maybe_quote_sets_quoted_flag() {
4212        let reserved = get_reserved_words(None);
4213        let mut id = Identifier::new("select");
4214        assert!(!id.quoted);
4215        maybe_quote(&mut id, &reserved);
4216        assert!(id.quoted);
4217    }
4218
4219    #[test]
4220    fn test_maybe_quote_skips_already_quoted() {
4221        let reserved = get_reserved_words(None);
4222        let mut id = Identifier::quoted("myname");
4223        assert!(id.quoted);
4224        maybe_quote(&mut id, &reserved);
4225        assert!(id.quoted); // still quoted
4226        assert_eq!(id.name, "myname"); // name unchanged
4227    }
4228
4229    #[test]
4230    fn test_maybe_quote_skips_star() {
4231        let reserved = get_reserved_words(None);
4232        let mut id = Identifier::new("*");
4233        maybe_quote(&mut id, &reserved);
4234        assert!(!id.quoted); // star should not be quoted
4235    }
4236
4237    #[test]
4238    fn test_maybe_quote_skips_normal() {
4239        let reserved = get_reserved_words(None);
4240        let mut id = Identifier::new("normal_col");
4241        maybe_quote(&mut id, &reserved);
4242        assert!(!id.quoted);
4243    }
4244
4245    #[test]
4246    fn test_quote_identifiers_column_with_reserved_name() {
4247        // A column named "select" should be quoted
4248        let expr = Expression::boxed_column(Column {
4249            name: Identifier::new("select"),
4250            table: None,
4251            join_mark: false,
4252            trailing_comments: vec![],
4253            span: None,
4254            inferred_type: None,
4255        });
4256        let result = quote_identifiers(expr, None);
4257        if let Expression::Column(col) = &result {
4258            assert!(col.name.quoted, "Column named 'select' should be quoted");
4259        } else {
4260            panic!("Expected Column expression");
4261        }
4262    }
4263
4264    #[test]
4265    fn test_quote_identifiers_column_with_special_chars() {
4266        let expr = Expression::boxed_column(Column {
4267            name: Identifier::new("my column"),
4268            table: None,
4269            join_mark: false,
4270            trailing_comments: vec![],
4271            span: None,
4272            inferred_type: None,
4273        });
4274        let result = quote_identifiers(expr, None);
4275        if let Expression::Column(col) = &result {
4276            assert!(col.name.quoted, "Column with space should be quoted");
4277        } else {
4278            panic!("Expected Column expression");
4279        }
4280    }
4281
4282    #[test]
4283    fn test_quote_identifiers_preserves_normal_column() {
4284        let expr = Expression::boxed_column(Column {
4285            name: Identifier::new("normal_col"),
4286            table: Some(Identifier::new("my_table")),
4287            join_mark: false,
4288            trailing_comments: vec![],
4289            span: None,
4290            inferred_type: None,
4291        });
4292        let result = quote_identifiers(expr, None);
4293        if let Expression::Column(col) = &result {
4294            assert!(!col.name.quoted, "Normal column should not be quoted");
4295            assert!(
4296                !col.table.as_ref().unwrap().quoted,
4297                "Normal table should not be quoted"
4298            );
4299        } else {
4300            panic!("Expected Column expression");
4301        }
4302    }
4303
4304    #[test]
4305    fn test_quote_identifiers_table_ref_reserved() {
4306        let expr = Expression::Table(Box::new(TableRef::new("select")));
4307        let result = quote_identifiers(expr, None);
4308        if let Expression::Table(tr) = &result {
4309            assert!(tr.name.quoted, "Table named 'select' should be quoted");
4310        } else {
4311            panic!("Expected Table expression");
4312        }
4313    }
4314
4315    #[test]
4316    fn test_quote_identifiers_table_ref_schema_and_alias() {
4317        let mut tr = TableRef::new("my_table");
4318        tr.schema = Some(Identifier::new("from"));
4319        tr.alias = Some(Identifier::new("t"));
4320        let expr = Expression::Table(Box::new(tr));
4321        let result = quote_identifiers(expr, None);
4322        if let Expression::Table(tr) = &result {
4323            assert!(!tr.name.quoted, "Normal table name should not be quoted");
4324            assert!(
4325                tr.schema.as_ref().unwrap().quoted,
4326                "Schema named 'from' should be quoted"
4327            );
4328            assert!(
4329                !tr.alias.as_ref().unwrap().quoted,
4330                "Normal alias should not be quoted"
4331            );
4332        } else {
4333            panic!("Expected Table expression");
4334        }
4335    }
4336
4337    #[test]
4338    fn test_quote_identifiers_identifier_node() {
4339        let expr = Expression::Identifier(Identifier::new("order"));
4340        let result = quote_identifiers(expr, None);
4341        if let Expression::Identifier(id) = &result {
4342            assert!(id.quoted, "Identifier named 'order' should be quoted");
4343        } else {
4344            panic!("Expected Identifier expression");
4345        }
4346    }
4347
4348    #[test]
4349    fn test_quote_identifiers_alias() {
4350        let inner = Expression::boxed_column(Column {
4351            name: Identifier::new("val"),
4352            table: None,
4353            join_mark: false,
4354            trailing_comments: vec![],
4355            span: None,
4356            inferred_type: None,
4357        });
4358        let expr = Expression::Alias(Box::new(Alias {
4359            this: inner,
4360            alias: Identifier::new("select"),
4361            column_aliases: vec![Identifier::new("from")],
4362            alias_explicit_as: false,
4363            alias_keyword: None,
4364            pre_alias_comments: vec![],
4365            trailing_comments: vec![],
4366            inferred_type: None,
4367        }));
4368        let result = quote_identifiers(expr, None);
4369        if let Expression::Alias(alias) = &result {
4370            assert!(alias.alias.quoted, "Alias named 'select' should be quoted");
4371            assert!(
4372                alias.column_aliases[0].quoted,
4373                "Column alias named 'from' should be quoted"
4374            );
4375            // Inner column "val" should not be quoted
4376            if let Expression::Column(col) = &alias.this {
4377                assert!(!col.name.quoted);
4378            }
4379        } else {
4380            panic!("Expected Alias expression");
4381        }
4382    }
4383
4384    #[test]
4385    fn test_quote_identifiers_select_recursive() {
4386        // Parse a query and verify quote_identifiers walks through it
4387        let expr = parse("SELECT a, b FROM t WHERE c = 1");
4388        let result = quote_identifiers(expr, None);
4389        // "a", "b", "c", "t" are all normal identifiers, none should be quoted
4390        let sql = gen(&result);
4391        // The SQL should be unchanged since no reserved words are used
4392        assert!(sql.contains("a"));
4393        assert!(sql.contains("b"));
4394        assert!(sql.contains("t"));
4395    }
4396
4397    #[test]
4398    fn test_quote_identifiers_digit_start() {
4399        let expr = Expression::boxed_column(Column {
4400            name: Identifier::new("1col"),
4401            table: None,
4402            join_mark: false,
4403            trailing_comments: vec![],
4404            span: None,
4405            inferred_type: None,
4406        });
4407        let result = quote_identifiers(expr, None);
4408        if let Expression::Column(col) = &result {
4409            assert!(
4410                col.name.quoted,
4411                "Column starting with digit should be quoted"
4412            );
4413        } else {
4414            panic!("Expected Column expression");
4415        }
4416    }
4417
4418    #[test]
4419    fn test_quote_identifiers_with_mysql_dialect() {
4420        let reserved = get_reserved_words(Some(DialectType::MySQL));
4421        // "KILL" is reserved in MySQL
4422        assert!(needs_quoting("KILL", &reserved));
4423        // "FORCE" is reserved in MySQL
4424        assert!(needs_quoting("FORCE", &reserved));
4425    }
4426
4427    #[test]
4428    fn test_quote_identifiers_with_postgresql_dialect() {
4429        let reserved = get_reserved_words(Some(DialectType::PostgreSQL));
4430        // "ILIKE" is reserved in PostgreSQL
4431        assert!(needs_quoting("ILIKE", &reserved));
4432        // "VERBOSE" is reserved in PostgreSQL
4433        assert!(needs_quoting("VERBOSE", &reserved));
4434    }
4435
4436    #[test]
4437    fn test_quote_identifiers_with_bigquery_dialect() {
4438        let reserved = get_reserved_words(Some(DialectType::BigQuery));
4439        // "STRUCT" is reserved in BigQuery
4440        assert!(needs_quoting("STRUCT", &reserved));
4441        // "PROTO" is reserved in BigQuery
4442        assert!(needs_quoting("PROTO", &reserved));
4443    }
4444
4445    #[test]
4446    fn test_quote_identifiers_case_insensitive_reserved() {
4447        let reserved = get_reserved_words(None);
4448        assert!(needs_quoting("Select", &reserved));
4449        assert!(needs_quoting("sElEcT", &reserved));
4450        assert!(needs_quoting("FROM", &reserved));
4451        assert!(needs_quoting("from", &reserved));
4452    }
4453
4454    #[test]
4455    fn test_quote_identifiers_join_using() {
4456        // Build a join with USING identifiers that include reserved words
4457        let mut join = crate::expressions::Join {
4458            this: Expression::Table(Box::new(TableRef::new("other"))),
4459            on: None,
4460            using: vec![Identifier::new("key"), Identifier::new("value")],
4461            kind: crate::expressions::JoinKind::Inner,
4462            use_inner_keyword: false,
4463            use_outer_keyword: false,
4464            deferred_condition: false,
4465            join_hint: None,
4466            match_condition: None,
4467            pivots: vec![],
4468            comments: vec![],
4469            nesting_group: 0,
4470            directed: false,
4471        };
4472        let reserved = get_reserved_words(None);
4473        quote_join(&mut join, &reserved);
4474        // "key" is reserved, "value" is not
4475        assert!(
4476            join.using[0].quoted,
4477            "USING identifier 'key' should be quoted"
4478        );
4479        assert!(
4480            !join.using[1].quoted,
4481            "USING identifier 'value' should not be quoted"
4482        );
4483    }
4484
4485    #[test]
4486    fn test_quote_identifiers_cte() {
4487        // Build a CTE where alias is a reserved word
4488        let mut cte = crate::expressions::Cte {
4489            alias: Identifier::new("select"),
4490            this: Expression::boxed_column(Column {
4491                name: Identifier::new("x"),
4492                table: None,
4493                join_mark: false,
4494                trailing_comments: vec![],
4495                span: None,
4496                inferred_type: None,
4497            }),
4498            columns: vec![Identifier::new("from"), Identifier::new("normal")],
4499            materialized: None,
4500            key_expressions: vec![],
4501            alias_first: false,
4502            comments: Vec::new(),
4503        };
4504        let reserved = get_reserved_words(None);
4505        maybe_quote(&mut cte.alias, &reserved);
4506        for c in &mut cte.columns {
4507            maybe_quote(c, &reserved);
4508        }
4509        assert!(cte.alias.quoted, "CTE alias 'select' should be quoted");
4510        assert!(cte.columns[0].quoted, "CTE column 'from' should be quoted");
4511        assert!(
4512            !cte.columns[1].quoted,
4513            "CTE column 'normal' should not be quoted"
4514        );
4515    }
4516
4517    #[test]
4518    fn test_quote_identifiers_binary_ops_recurse() {
4519        // a_col + select_col should quote "select_col" but that's actually
4520        // just a regular name. Use actual reserved word as column name.
4521        let expr = Expression::Add(Box::new(crate::expressions::BinaryOp::new(
4522            Expression::boxed_column(Column {
4523                name: Identifier::new("select"),
4524                table: None,
4525                join_mark: false,
4526                trailing_comments: vec![],
4527                span: None,
4528                inferred_type: None,
4529            }),
4530            Expression::boxed_column(Column {
4531                name: Identifier::new("normal"),
4532                table: None,
4533                join_mark: false,
4534                trailing_comments: vec![],
4535                span: None,
4536                inferred_type: None,
4537            }),
4538        )));
4539        let result = quote_identifiers(expr, None);
4540        if let Expression::Add(bin) = &result {
4541            if let Expression::Column(left) = &bin.left {
4542                assert!(
4543                    left.name.quoted,
4544                    "'select' column should be quoted in binary op"
4545                );
4546            }
4547            if let Expression::Column(right) = &bin.right {
4548                assert!(!right.name.quoted, "'normal' column should not be quoted");
4549            }
4550        } else {
4551            panic!("Expected Add expression");
4552        }
4553    }
4554
4555    #[test]
4556    fn test_quote_identifiers_already_quoted_preserved() {
4557        // Already-quoted identifier should stay quoted even if it doesn't need it
4558        let expr = Expression::boxed_column(Column {
4559            name: Identifier::quoted("normal_name"),
4560            table: None,
4561            join_mark: false,
4562            trailing_comments: vec![],
4563            span: None,
4564            inferred_type: None,
4565        });
4566        let result = quote_identifiers(expr, None);
4567        if let Expression::Column(col) = &result {
4568            assert!(
4569                col.name.quoted,
4570                "Already-quoted identifier should remain quoted"
4571            );
4572        } else {
4573            panic!("Expected Column expression");
4574        }
4575    }
4576
4577    #[test]
4578    fn test_quote_identifiers_full_parsed_query() {
4579        // Test with a parsed query that uses reserved words as identifiers
4580        // We build the AST manually since the parser would fail on unquoted reserved words
4581        let mut select = crate::expressions::Select::new();
4582        select.expressions.push(Expression::boxed_column(Column {
4583            name: Identifier::new("order"),
4584            table: Some(Identifier::new("t")),
4585            join_mark: false,
4586            trailing_comments: vec![],
4587            span: None,
4588            inferred_type: None,
4589        }));
4590        select.from = Some(crate::expressions::From {
4591            expressions: vec![Expression::Table(Box::new(TableRef::new("t")))],
4592        });
4593        let expr = Expression::Select(Box::new(select));
4594
4595        let result = quote_identifiers(expr, None);
4596        if let Expression::Select(sel) = &result {
4597            if let Expression::Column(col) = &sel.expressions[0] {
4598                assert!(col.name.quoted, "Column named 'order' should be quoted");
4599                assert!(
4600                    !col.table.as_ref().unwrap().quoted,
4601                    "Table 't' should not be quoted"
4602                );
4603            } else {
4604                panic!("Expected Column in SELECT list");
4605            }
4606        } else {
4607            panic!("Expected Select expression");
4608        }
4609    }
4610
4611    #[test]
4612    fn test_get_reserved_words_all_dialects() {
4613        // Ensure get_reserved_words doesn't panic for any dialect
4614        let dialects = [
4615            None,
4616            Some(DialectType::Generic),
4617            Some(DialectType::MySQL),
4618            Some(DialectType::PostgreSQL),
4619            Some(DialectType::BigQuery),
4620            Some(DialectType::Snowflake),
4621            Some(DialectType::TSQL),
4622            Some(DialectType::ClickHouse),
4623            Some(DialectType::DuckDB),
4624            Some(DialectType::Hive),
4625            Some(DialectType::Spark),
4626            Some(DialectType::Trino),
4627            Some(DialectType::Oracle),
4628            Some(DialectType::Redshift),
4629        ];
4630        for dialect in &dialects {
4631            let words = get_reserved_words(*dialect);
4632            // All dialects should have basic SQL reserved words
4633            assert!(
4634                words.contains("SELECT"),
4635                "All dialects should have SELECT as reserved"
4636            );
4637            assert!(
4638                words.contains("FROM"),
4639                "All dialects should have FROM as reserved"
4640            );
4641        }
4642    }
4643}