Skip to main content

polyglot_sql/
ast_transforms.rs

1//! AST transform helpers and convenience getters.
2//!
3//! This module provides functions for common AST mutations (adding WHERE clauses,
4//! setting LIMIT/OFFSET, renaming columns/tables) and read-only extraction helpers
5//! (getting column names, table names, functions, etc.).
6//!
7//! Mutation functions take an owned [`Expression`] and return a new [`Expression`].
8//! Read-only getters take `&Expression`.
9
10use std::collections::{HashMap, HashSet};
11
12use crate::builder::engine;
13use crate::expressions::*;
14use crate::traversal::{is_aggregate, ExpressionWalk};
15
16/// Apply a bottom-up transformation to every node in the tree.
17/// Wraps `crate::traversal::transform` with a simpler signature for this module.
18fn xform<F: Fn(Expression) -> Expression>(expr: Expression, fun: F) -> Expression {
19    crate::traversal::transform(expr, &|node| Ok(Some(fun(node))))
20        .unwrap_or_else(|_| Expression::Null(Null))
21}
22
23// ---------------------------------------------------------------------------
24// SELECT clause
25// ---------------------------------------------------------------------------
26
27/// Append columns to the SELECT list of a query.
28///
29/// If `expr` is a `Select`, the given `columns` are appended to its expression list.
30/// Non-SELECT expressions are returned unchanged.
31pub fn add_select_columns(expr: Expression, columns: Vec<Expression>) -> Expression {
32    let mut expression = expr;
33    let _ = engine::append_select(&mut expression, columns, true);
34    expression
35}
36
37/// Remove columns from the SELECT list where `predicate` returns `true`.
38pub fn remove_select_columns<F: Fn(&Expression) -> bool>(
39    expr: Expression,
40    predicate: F,
41) -> Expression {
42    if let Expression::Select(mut sel) = expr {
43        sel.expressions.retain(|e| !predicate(e));
44        Expression::Select(sel)
45    } else {
46        expr
47    }
48}
49
50/// Set or remove the DISTINCT flag on a SELECT.
51pub fn set_distinct(expr: Expression, distinct: bool) -> Expression {
52    let mut expression = expr;
53    let _ = engine::apply_distinct(&mut expression, distinct);
54    expression
55}
56
57// ---------------------------------------------------------------------------
58// WHERE clause
59// ---------------------------------------------------------------------------
60
61/// Add a condition to the WHERE clause.
62///
63/// If the SELECT already has a WHERE clause, the new condition is combined with the
64/// existing one using AND (default) or OR (when `use_or` is `true`).
65/// If there is no WHERE clause, one is created.
66pub fn add_where(expr: Expression, condition: Expression, use_or: bool) -> Expression {
67    if !use_or {
68        if !matches!(expr, Expression::Select(_)) {
69            return expr;
70        }
71        let mut expression = expr;
72        let _ = engine::apply_where(&mut expression, condition, true);
73        return expression;
74    }
75    if let Expression::Select(mut sel) = expr {
76        sel.where_clause = Some(match sel.where_clause.take() {
77            Some(existing) => {
78                let combined = if use_or {
79                    Expression::Or(Box::new(BinaryOp::new(existing.this, condition)))
80                } else {
81                    Expression::And(Box::new(BinaryOp::new(existing.this, condition)))
82                };
83                Where { this: combined }
84            }
85            None => Where { this: condition },
86        });
87        Expression::Select(sel)
88    } else {
89        expr
90    }
91}
92
93/// Remove the WHERE clause from a SELECT.
94pub fn remove_where(expr: Expression) -> Expression {
95    if let Expression::Select(mut sel) = expr {
96        sel.where_clause = None;
97        Expression::Select(sel)
98    } else {
99        expr
100    }
101}
102
103// ---------------------------------------------------------------------------
104// LIMIT / OFFSET / ORDER BY
105// ---------------------------------------------------------------------------
106
107/// Set the LIMIT on a SELECT or set operation.
108pub fn set_limit(expr: Expression, limit: usize) -> Expression {
109    set_limit_expr(expr, Expression::number(limit as i64))
110}
111
112/// Set the LIMIT on a SELECT or set operation using an expression.
113pub fn set_limit_expr(expr: Expression, limit: Expression) -> Expression {
114    let mut expression = expr;
115    let _ = engine::apply_limit(&mut expression, limit);
116    expression
117}
118
119/// Set the OFFSET on a SELECT or set operation.
120pub fn set_offset(expr: Expression, offset: usize) -> Expression {
121    set_offset_expr(expr, Expression::number(offset as i64))
122}
123
124/// Set the OFFSET on a SELECT or set operation using an expression.
125pub fn set_offset_expr(expr: Expression, offset: Expression) -> Expression {
126    let mut expression = expr;
127    let _ = engine::apply_offset(&mut expression, offset);
128    expression
129}
130
131/// Set the ORDER BY clause on a SELECT or set operation.
132///
133/// Bare expressions are normalized to ascending order expressions. Existing
134/// `Ordered` expressions preserve their direction and null-ordering metadata.
135pub fn set_order_by(expr: Expression, expressions: Vec<Expression>) -> Expression {
136    let mut expression = expr;
137    let values = expressions.into_iter().map(engine::ordered).collect();
138    let _ = engine::apply_order_by(&mut expression, values, false);
139    expression
140}
141
142/// Remove both LIMIT and OFFSET from a SELECT.
143pub fn remove_limit_offset(expr: Expression) -> Expression {
144    if let Expression::Select(mut sel) = expr {
145        sel.limit = None;
146        sel.offset = None;
147        Expression::Select(sel)
148    } else {
149        expr
150    }
151}
152
153// ---------------------------------------------------------------------------
154// Renaming
155// ---------------------------------------------------------------------------
156
157/// Rename columns throughout the expression tree using the provided mapping.
158///
159/// Column names present as keys in `mapping` are replaced with their corresponding
160/// values. The replacement is case-sensitive.
161pub fn rename_columns(expr: Expression, mapping: &HashMap<String, String>) -> Expression {
162    xform(expr, |node| match node {
163        Expression::Column(mut col) => {
164            if let Some(new_name) = mapping.get(&col.name.name) {
165                col.name.name = new_name.clone();
166            }
167            Expression::Column(col)
168        }
169        other => other,
170    })
171}
172
173/// Options for table renaming.
174#[derive(Debug, Clone)]
175pub struct RenameTablesOptions {
176    /// Whether renamed table references should receive aliases.
177    pub alias_renamed_tables: bool,
178    /// Whether existing aliases should be preserved when aliasing renamed tables.
179    pub preserve_existing_aliases: bool,
180}
181
182impl Default for RenameTablesOptions {
183    fn default() -> Self {
184        Self {
185            alias_renamed_tables: false,
186            preserve_existing_aliases: true,
187        }
188    }
189}
190
191impl RenameTablesOptions {
192    pub fn new() -> Self {
193        Self::default()
194    }
195
196    pub fn with_alias_renamed_tables(mut self, alias: bool) -> Self {
197        self.alias_renamed_tables = alias;
198        self
199    }
200
201    pub fn with_preserve_existing_aliases(mut self, preserve: bool) -> Self {
202        self.preserve_existing_aliases = preserve;
203        self
204    }
205}
206
207/// Rename tables throughout the expression tree using the provided mapping.
208pub fn rename_tables(expr: Expression, mapping: &HashMap<String, String>) -> Expression {
209    rename_tables_with_options(expr, mapping, &RenameTablesOptions::default())
210}
211
212/// Rename tables throughout the expression tree using the provided mapping and options.
213pub fn rename_tables_with_options(
214    expr: Expression,
215    mapping: &HashMap<String, String>,
216    options: &RenameTablesOptions,
217) -> Expression {
218    xform(expr, |node| match node {
219        Expression::Table(mut tbl) => {
220            if let Some(new_name) = mapping.get(&tbl.name.name) {
221                tbl.name.name = new_name.clone();
222                if options.alias_renamed_tables
223                    && (!options.preserve_existing_aliases || tbl.alias.is_none())
224                {
225                    tbl.alias = Some(Identifier::new(new_name));
226                    tbl.alias_explicit_as = true;
227                }
228            }
229            Expression::Table(tbl)
230        }
231        Expression::Column(mut col) => {
232            if let Some(ref mut table_id) = col.table {
233                if let Some(new_name) = mapping.get(&table_id.name) {
234                    table_id.name = new_name.clone();
235                }
236            }
237            Expression::Column(col)
238        }
239        other => other,
240    })
241}
242
243/// Qualify all unqualified column references with the given `table_name`.
244///
245/// Columns that already have a table qualifier are left unchanged.
246pub fn qualify_columns(expr: Expression, table_name: &str) -> Expression {
247    let table = table_name.to_string();
248    xform(expr, move |node| match node {
249        Expression::Column(mut col) => {
250            if col.table.is_none() {
251                col.table = Some(Identifier::new(&table));
252            }
253            Expression::Column(col)
254        }
255        other => other,
256    })
257}
258
259// ---------------------------------------------------------------------------
260// Generic replacement
261// ---------------------------------------------------------------------------
262
263/// Replace nodes matching `predicate` with `replacement` (cloned for each match).
264pub fn replace_nodes<F: Fn(&Expression) -> bool>(
265    expr: Expression,
266    predicate: F,
267    replacement: Expression,
268) -> Expression {
269    xform(expr, |node| {
270        if predicate(&node) {
271            replacement.clone()
272        } else {
273            node
274        }
275    })
276}
277
278/// Replace nodes matching `predicate` by applying `replacer` to the matched node.
279pub fn replace_by_type<F, R>(expr: Expression, predicate: F, replacer: R) -> Expression
280where
281    F: Fn(&Expression) -> bool,
282    R: Fn(Expression) -> Expression,
283{
284    xform(expr, |node| {
285        if predicate(&node) {
286            replacer(node)
287        } else {
288            node
289        }
290    })
291}
292
293/// Remove (replace with a `Null`) all nodes matching `predicate`.
294///
295/// This is most useful for removing clauses or sub-expressions from a tree.
296/// Note that removing structural elements (e.g. the FROM clause) may produce
297/// invalid SQL; use with care.
298pub fn remove_nodes<F: Fn(&Expression) -> bool>(expr: Expression, predicate: F) -> Expression {
299    xform(expr, |node| {
300        if predicate(&node) {
301            Expression::Null(Null)
302        } else {
303            node
304        }
305    })
306}
307
308// ---------------------------------------------------------------------------
309// Convenience getters
310// ---------------------------------------------------------------------------
311
312/// Collect all column names (as `String`) referenced in the expression tree.
313pub fn get_column_names(expr: &Expression) -> Vec<String> {
314    expr.find_all(|e| matches!(e, Expression::Column(_)))
315        .into_iter()
316        .filter_map(|e| {
317            if let Expression::Column(col) = e {
318                Some(col.name.name.clone())
319            } else {
320                None
321            }
322        })
323        .collect()
324}
325
326/// Collect projected output column names from a query expression.
327///
328/// This follows sqlglot-style query semantics:
329/// - For `SELECT`, returns names from the projection list.
330/// - For set operations (`UNION`/`INTERSECT`/`EXCEPT`), uses the left-most branch.
331/// - For `Subquery`, unwraps and evaluates the inner query.
332///
333/// Unlike [`get_column_names`], this does not return every referenced column in
334/// the AST and is suitable for result-schema style output names.
335pub fn get_output_column_names(expr: &Expression) -> Vec<String> {
336    output_column_names_from_query(expr)
337}
338
339/// Collect projected output column names using dialect-specific set-operation
340/// alignment rules.
341///
342/// This differs from [`get_output_column_names`] only for name-aligned set
343/// operations. When an output shape is not statically knowable (for example an
344/// unresolved wildcard), it preserves the existing leftmost-branch behavior.
345pub fn get_output_column_names_for_dialect(
346    expr: &Expression,
347    dialect: Option<crate::dialects::DialectType>,
348) -> Vec<String> {
349    crate::set_operation::query_output_identifiers(expr, dialect)
350        .map(|identifiers| {
351            identifiers
352                .into_iter()
353                .map(|identifier| identifier.name)
354                .collect()
355        })
356        .unwrap_or_else(|_| output_column_names_from_query(expr))
357}
358
359fn output_column_names_from_query(expr: &Expression) -> Vec<String> {
360    match expr {
361        Expression::Select(select) => select_output_column_names(select),
362        Expression::Union(union) => output_column_names_from_query(&union.left),
363        Expression::Intersect(intersect) => output_column_names_from_query(&intersect.left),
364        Expression::Except(except) => output_column_names_from_query(&except.left),
365        Expression::Subquery(subquery) => output_column_names_from_query(&subquery.this),
366        _ => Vec::new(),
367    }
368}
369
370fn select_output_column_names(select: &Select) -> Vec<String> {
371    let mut names = Vec::new();
372    for expr in &select.expressions {
373        if let Some(name) = expression_output_name(expr) {
374            names.push(name);
375        }
376    }
377    names
378}
379
380fn expression_output_name(expr: &Expression) -> Option<String> {
381    match expr {
382        Expression::Alias(alias) => Some(alias.alias.name.clone()),
383        Expression::Column(col) => Some(col.name.name.clone()),
384        Expression::Star(_) => Some("*".to_string()),
385        Expression::Identifier(id) => Some(id.name.clone()),
386        Expression::Aliases(aliases) => aliases.expressions.iter().find_map(|e| match e {
387            Expression::Identifier(id) => Some(id.name.clone()),
388            _ => None,
389        }),
390        _ => None,
391    }
392}
393
394/// Collect all table names (as `String`) referenced in the expression tree.
395pub fn get_table_names(expr: &Expression) -> Vec<String> {
396    fn collect_cte_aliases(with_clause: &With, aliases: &mut HashSet<String>) {
397        for cte in &with_clause.ctes {
398            aliases.insert(cte.alias.name.clone());
399        }
400    }
401
402    fn push_table_ref_name(
403        table: &TableRef,
404        cte_aliases: &HashSet<String>,
405        names: &mut Vec<String>,
406    ) {
407        let name = table.name.name.clone();
408        if !name.is_empty() && !cte_aliases.contains(&name) {
409            names.push(name);
410        }
411    }
412
413    let mut cte_aliases: HashSet<String> = HashSet::new();
414    for node in expr.dfs() {
415        match node {
416            Expression::Select(select) => {
417                if let Some(with) = &select.with {
418                    collect_cte_aliases(with, &mut cte_aliases);
419                }
420            }
421            Expression::Insert(insert) => {
422                if let Some(with) = &insert.with {
423                    collect_cte_aliases(with, &mut cte_aliases);
424                }
425            }
426            Expression::Update(update) => {
427                if let Some(with) = &update.with {
428                    collect_cte_aliases(with, &mut cte_aliases);
429                }
430            }
431            Expression::Delete(delete) => {
432                if let Some(with) = &delete.with {
433                    collect_cte_aliases(with, &mut cte_aliases);
434                }
435            }
436            Expression::Union(union) => {
437                if let Some(with) = &union.with {
438                    collect_cte_aliases(with, &mut cte_aliases);
439                }
440            }
441            Expression::Intersect(intersect) => {
442                if let Some(with) = &intersect.with {
443                    collect_cte_aliases(with, &mut cte_aliases);
444                }
445            }
446            Expression::Except(except) => {
447                if let Some(with) = &except.with {
448                    collect_cte_aliases(with, &mut cte_aliases);
449                }
450            }
451            Expression::CreateTable(create) => {
452                if let Some(with) = &create.with_cte {
453                    collect_cte_aliases(with, &mut cte_aliases);
454                }
455            }
456            Expression::Merge(merge) => {
457                if let Some(with_) = &merge.with_ {
458                    if let Expression::With(with_clause) = with_.as_ref() {
459                        collect_cte_aliases(with_clause, &mut cte_aliases);
460                    }
461                }
462            }
463            _ => {}
464        }
465    }
466
467    let mut names = Vec::new();
468    for node in expr.dfs() {
469        match node {
470            Expression::Table(tbl) => {
471                let name = tbl.name.name.clone();
472                if !name.is_empty() && !cte_aliases.contains(&name) {
473                    names.push(name);
474                }
475            }
476            Expression::Insert(insert) => {
477                push_table_ref_name(&insert.table, &cte_aliases, &mut names);
478            }
479            Expression::Update(update) => {
480                push_table_ref_name(&update.table, &cte_aliases, &mut names);
481                for table in &update.extra_tables {
482                    push_table_ref_name(table, &cte_aliases, &mut names);
483                }
484            }
485            Expression::Delete(delete) => {
486                push_table_ref_name(&delete.table, &cte_aliases, &mut names);
487                for table in &delete.using {
488                    push_table_ref_name(table, &cte_aliases, &mut names);
489                }
490                for table in &delete.tables {
491                    push_table_ref_name(table, &cte_aliases, &mut names);
492                }
493            }
494            Expression::CreateTable(create) => {
495                push_table_ref_name(&create.name, &cte_aliases, &mut names);
496                if let Some(as_select) = &create.as_select {
497                    names.extend(get_table_names(as_select));
498                }
499                if let Some(with) = &create.with_cte {
500                    for cte in &with.ctes {
501                        names.extend(get_table_names(&cte.this));
502                    }
503                }
504            }
505            Expression::Cache(cache) => {
506                let name = cache.table.name.clone();
507                if !name.is_empty() && !cte_aliases.contains(&name) {
508                    names.push(name);
509                }
510            }
511            Expression::Uncache(uncache) => {
512                let name = uncache.table.name.clone();
513                if !name.is_empty() && !cte_aliases.contains(&name) {
514                    names.push(name);
515                }
516            }
517            Expression::CreateSynonym(synonym) => {
518                push_table_ref_name(&synonym.name, &cte_aliases, &mut names);
519                push_table_ref_name(&synonym.target, &cte_aliases, &mut names);
520            }
521            _ => {}
522        }
523    }
524
525    names
526}
527
528/// Collect all identifier references in the expression tree.
529pub fn get_identifiers(expr: &Expression) -> Vec<&Expression> {
530    expr.find_all(|e| matches!(e, Expression::Identifier(_)))
531}
532
533/// Collect all function call nodes in the expression tree.
534pub fn get_functions(expr: &Expression) -> Vec<&Expression> {
535    expr.find_all(|e| {
536        matches!(
537            e,
538            Expression::Function(_) | Expression::AggregateFunction(_)
539        )
540    })
541}
542
543/// Collect all literal value nodes in the expression tree.
544pub fn get_literals(expr: &Expression) -> Vec<&Expression> {
545    expr.find_all(|e| {
546        matches!(
547            e,
548            Expression::Literal(_) | Expression::Boolean(_) | Expression::Null(_)
549        )
550    })
551}
552
553/// Collect all subquery nodes in the expression tree.
554pub fn get_subqueries(expr: &Expression) -> Vec<&Expression> {
555    expr.find_all(|e| matches!(e, Expression::Subquery(_)))
556}
557
558/// Collect all aggregate function nodes in the expression tree.
559///
560/// Includes typed aggregates (`Count`, `Sum`, `Avg`, `Min`, `Max`, etc.)
561/// and generic `AggregateFunction` nodes.
562pub fn get_aggregate_functions(expr: &Expression) -> Vec<&Expression> {
563    expr.find_all(is_aggregate)
564}
565
566/// Collect all window function nodes in the expression tree.
567pub fn get_window_functions(expr: &Expression) -> Vec<&Expression> {
568    expr.find_all(|e| matches!(e, Expression::WindowFunction(_)))
569}
570
571/// Count the total number of AST nodes in the expression tree.
572pub fn node_count(expr: &Expression) -> usize {
573    expr.dfs().count()
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use crate::parser::Parser;
580
581    fn parse_one(sql: &str) -> Expression {
582        let mut exprs = Parser::parse_sql(sql).unwrap();
583        exprs.remove(0)
584    }
585
586    #[test]
587    fn test_add_where() {
588        let expr = parse_one("SELECT a FROM t");
589        let cond = Expression::Eq(Box::new(BinaryOp::new(
590            Expression::column("b"),
591            Expression::number(1),
592        )));
593        let result = add_where(expr, cond, false);
594        let sql = result.sql();
595        assert!(sql.contains("WHERE"), "Expected WHERE in: {}", sql);
596        assert!(sql.contains("b = 1"), "Expected condition in: {}", sql);
597    }
598
599    #[test]
600    fn test_add_where_combines_with_and() {
601        let expr = parse_one("SELECT a FROM t WHERE x = 1");
602        let cond = Expression::Eq(Box::new(BinaryOp::new(
603            Expression::column("y"),
604            Expression::number(2),
605        )));
606        let result = add_where(expr, cond, false);
607        let sql = result.sql();
608        assert!(sql.contains("AND"), "Expected AND in: {}", sql);
609    }
610
611    #[test]
612    fn test_remove_where() {
613        let expr = parse_one("SELECT a FROM t WHERE x = 1");
614        let result = remove_where(expr);
615        let sql = result.sql();
616        assert!(!sql.contains("WHERE"), "Should not contain WHERE: {}", sql);
617    }
618
619    #[test]
620    fn test_set_limit() {
621        let expr = parse_one("SELECT a FROM t");
622        let result = set_limit(expr, 10);
623        let sql = result.sql();
624        assert!(sql.contains("LIMIT 10"), "Expected LIMIT in: {}", sql);
625    }
626
627    #[test]
628    fn test_set_limit_on_set_operation() {
629        let expr = parse_one("SELECT a FROM t UNION ALL SELECT a FROM u");
630        let result = set_limit(expr, 10);
631        let sql = result.sql();
632        assert_eq!(sql, "SELECT a FROM t UNION ALL SELECT a FROM u LIMIT 10");
633    }
634
635    #[test]
636    fn test_set_offset() {
637        let expr = parse_one("SELECT a FROM t");
638        let result = set_offset(expr, 5);
639        let sql = result.sql();
640        assert!(sql.contains("OFFSET 5"), "Expected OFFSET in: {}", sql);
641    }
642
643    #[test]
644    fn test_set_offset_on_set_operation() {
645        let expr = parse_one("SELECT a FROM t UNION ALL SELECT a FROM u");
646        let result = set_offset(expr, 5);
647        let sql = result.sql();
648        assert_eq!(sql, "SELECT a FROM t UNION ALL SELECT a FROM u OFFSET 5");
649    }
650
651    #[test]
652    fn test_set_order_by_on_set_operation() {
653        let expr = parse_one("SELECT a FROM t UNION ALL SELECT a FROM u");
654        let result = set_order_by(expr, vec![Expression::column("a")]);
655        let sql = result.sql();
656        assert_eq!(sql, "SELECT a FROM t UNION ALL SELECT a FROM u ORDER BY a");
657    }
658
659    #[test]
660    fn test_remove_limit_offset() {
661        let expr = parse_one("SELECT a FROM t LIMIT 10 OFFSET 5");
662        let result = remove_limit_offset(expr);
663        let sql = result.sql();
664        assert!(!sql.contains("LIMIT"), "Should not contain LIMIT: {}", sql);
665        assert!(
666            !sql.contains("OFFSET"),
667            "Should not contain OFFSET: {}",
668            sql
669        );
670    }
671
672    #[test]
673    fn test_get_column_names() {
674        let expr = parse_one("SELECT a, b, c FROM t");
675        let names = get_column_names(&expr);
676        assert!(names.contains(&"a".to_string()));
677        assert!(names.contains(&"b".to_string()));
678        assert!(names.contains(&"c".to_string()));
679    }
680
681    #[test]
682    fn test_get_output_column_names_select() {
683        let expr = parse_one("SELECT a, b AS c, 1 FROM t");
684        let names = get_output_column_names(&expr);
685        assert_eq!(names, vec!["a".to_string(), "c".to_string()]);
686    }
687
688    #[test]
689    fn test_get_output_column_names_union_left_projection() {
690        let expr =
691            parse_one("SELECT id, name FROM customers UNION ALL SELECT id, name FROM employees");
692        let names = get_output_column_names(&expr);
693        assert_eq!(names, vec!["id".to_string(), "name".to_string()]);
694    }
695
696    #[test]
697    fn test_get_output_column_names_union_uses_left_aliases() {
698        let expr = parse_one("SELECT id AS c1, name AS c2 FROM t1 UNION SELECT x, y FROM t2");
699        let names = get_output_column_names(&expr);
700        assert_eq!(names, vec!["c1".to_string(), "c2".to_string()]);
701    }
702
703    #[test]
704    fn test_get_output_column_names_uses_dialect_by_name_layout() {
705        for dialect in [
706            crate::dialects::DialectType::DuckDB,
707            crate::dialects::DialectType::Snowflake,
708        ] {
709            let expr = crate::parse_one(
710                "SELECT 1 AS left_value UNION ALL BY NAME SELECT 2 AS right_value",
711                dialect,
712            )
713            .expect("parse");
714            assert_eq!(
715                get_output_column_names_for_dialect(&expr, Some(dialect)),
716                vec!["left_value", "right_value"]
717            );
718        }
719
720        let expr = crate::parse_one(
721            "SELECT 1 AS a, 2 AS b UNION ALL BY NAME SELECT 3 AS b, 4 AS a",
722            crate::dialects::DialectType::BigQuery,
723        )
724        .expect("parse");
725        assert_eq!(
726            get_output_column_names_for_dialect(
727                &expr,
728                Some(crate::dialects::DialectType::BigQuery),
729            ),
730            vec!["a", "b"]
731        );
732    }
733
734    #[test]
735    fn test_get_column_names_union_still_returns_all_references() {
736        let expr =
737            parse_one("SELECT id, name FROM customers UNION ALL SELECT id, name FROM employees");
738        let names = get_column_names(&expr);
739        assert_eq!(
740            names,
741            vec![
742                "id".to_string(),
743                "name".to_string(),
744                "id".to_string(),
745                "name".to_string()
746            ]
747        );
748    }
749
750    #[test]
751    fn test_get_table_names() {
752        let expr = parse_one("SELECT a FROM users");
753        let names = get_table_names(&expr);
754        assert_eq!(names, vec!["users".to_string()]);
755    }
756
757    #[test]
758    fn test_get_table_names_excludes_cte_aliases() {
759        let expr = parse_one(
760            "WITH cte AS (SELECT * FROM users) SELECT * FROM cte JOIN orders o ON cte.id = o.id",
761        );
762        let names = get_table_names(&expr);
763        assert!(names.iter().any(|n| n == "users"));
764        assert!(names.iter().any(|n| n == "orders"));
765        assert!(!names.iter().any(|n| n == "cte"));
766    }
767
768    #[test]
769    fn test_get_table_names_includes_dml_targets() {
770        let insert_expr = parse_one("INSERT INTO users (id) VALUES (1)");
771        let insert_names = get_table_names(&insert_expr);
772        assert!(insert_names.iter().any(|n| n == "users"));
773
774        let update_expr =
775            parse_one("UPDATE users SET name = 'x' FROM accounts WHERE users.id = accounts.id");
776        let update_names = get_table_names(&update_expr);
777        assert!(update_names.iter().any(|n| n == "users"));
778        assert!(update_names.iter().any(|n| n == "accounts"));
779
780        let delete_expr =
781            parse_one("DELETE FROM users USING accounts WHERE users.id = accounts.id");
782        let delete_names = get_table_names(&delete_expr);
783        assert!(delete_names.iter().any(|n| n == "users"));
784        assert!(delete_names.iter().any(|n| n == "accounts"));
785
786        let create_expr = parse_one("CREATE TABLE out_table AS SELECT 1 AS id FROM src");
787        let create_names = get_table_names(&create_expr);
788        assert!(create_names.iter().any(|n| n == "out_table"));
789        assert!(create_names.iter().any(|n| n == "src"));
790    }
791
792    #[test]
793    fn test_node_count() {
794        let expr = parse_one("SELECT a FROM t");
795        let count = node_count(&expr);
796        assert!(count > 0, "Expected non-zero node count");
797    }
798
799    #[test]
800    fn test_rename_columns() {
801        let expr = parse_one("SELECT old_name FROM t");
802        let mut mapping = HashMap::new();
803        mapping.insert("old_name".to_string(), "new_name".to_string());
804        let result = rename_columns(expr, &mapping);
805        let sql = result.sql();
806        assert!(sql.contains("new_name"), "Expected new_name in: {}", sql);
807        assert!(
808            !sql.contains("old_name"),
809            "Should not contain old_name: {}",
810            sql
811        );
812    }
813
814    #[test]
815    fn test_rename_tables() {
816        let expr = parse_one("SELECT a FROM old_table");
817        let mut mapping = HashMap::new();
818        mapping.insert("old_table".to_string(), "new_table".to_string());
819        let result = rename_tables(expr, &mapping);
820        let sql = result.sql();
821        assert!(sql.contains("new_table"), "Expected new_table in: {}", sql);
822    }
823
824    #[test]
825    fn test_rename_tables_with_alias_renamed_tables() {
826        let expr = parse_one("SELECT a FROM old_table");
827        let mut mapping = HashMap::new();
828        mapping.insert("old_table".to_string(), "new_table".to_string());
829        let options = RenameTablesOptions::new().with_alias_renamed_tables(true);
830        let result = rename_tables_with_options(expr, &mapping, &options);
831        let sql = result.sql();
832
833        assert_eq!(sql, "SELECT a FROM new_table AS new_table");
834    }
835
836    #[test]
837    fn test_rename_tables_with_alias_preserves_existing_alias() {
838        let expr = parse_one("SELECT a FROM old_table AS t");
839        let mut mapping = HashMap::new();
840        mapping.insert("old_table".to_string(), "new_table".to_string());
841        let options = RenameTablesOptions::new().with_alias_renamed_tables(true);
842        let result = rename_tables_with_options(expr, &mapping, &options);
843        let sql = result.sql();
844
845        assert_eq!(sql, "SELECT a FROM new_table AS t");
846    }
847
848    #[test]
849    fn test_set_distinct() {
850        let expr = parse_one("SELECT a FROM t");
851        let result = set_distinct(expr, true);
852        let sql = result.sql();
853        assert!(sql.contains("DISTINCT"), "Expected DISTINCT in: {}", sql);
854    }
855
856    #[test]
857    fn test_add_select_columns() {
858        let expr = parse_one("SELECT a FROM t");
859        let result = add_select_columns(expr, vec![Expression::column("b")]);
860        let sql = result.sql();
861        assert!(
862            sql.contains("a, b") || sql.contains("a,b"),
863            "Expected a, b in: {}",
864            sql
865        );
866    }
867
868    #[test]
869    fn test_qualify_columns() {
870        let expr = parse_one("SELECT a, b FROM t");
871        let result = qualify_columns(expr, "t");
872        let sql = result.sql();
873        assert!(sql.contains("t.a"), "Expected t.a in: {}", sql);
874        assert!(sql.contains("t.b"), "Expected t.b in: {}", sql);
875    }
876
877    #[test]
878    fn test_get_functions() {
879        let expr = parse_one("SELECT COUNT(*), UPPER(name) FROM t");
880        let funcs = get_functions(&expr);
881        // UPPER is a typed function (Expression::Upper), not Expression::Function
882        // COUNT is Expression::Count, not Expression::AggregateFunction
883        // So get_functions (which checks Function | AggregateFunction) may return 0
884        // That's OK — we have separate get_aggregate_functions for typed aggs
885        let _ = funcs.len();
886    }
887
888    #[test]
889    fn test_get_aggregate_functions() {
890        let expr = crate::parse_one(
891            "SELECT COUNT_IF(numeric_value > 0), MEDIAN(numeric_value), FIRST(numeric_value) FROM source_table",
892            crate::dialects::DialectType::DuckDB,
893        )
894        .unwrap();
895        let aggs = get_aggregate_functions(&expr);
896        let aggregate_types: Vec<_> = aggs.iter().map(|agg| agg.variant_name()).collect();
897
898        assert_eq!(aggregate_types, vec!["count_if", "median", "first"]);
899    }
900}