Skip to main content

polyglot_sql/optimizer/
qualify_columns.rs

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