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::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
339fn output_column_names_from_query(expr: &Expression) -> Vec<String> {
340    match expr {
341        Expression::Select(select) => select_output_column_names(select),
342        Expression::Union(union) => output_column_names_from_query(&union.left),
343        Expression::Intersect(intersect) => output_column_names_from_query(&intersect.left),
344        Expression::Except(except) => output_column_names_from_query(&except.left),
345        Expression::Subquery(subquery) => output_column_names_from_query(&subquery.this),
346        _ => Vec::new(),
347    }
348}
349
350fn select_output_column_names(select: &Select) -> Vec<String> {
351    let mut names = Vec::new();
352    for expr in &select.expressions {
353        if let Some(name) = expression_output_name(expr) {
354            names.push(name);
355        }
356    }
357    names
358}
359
360fn expression_output_name(expr: &Expression) -> Option<String> {
361    match expr {
362        Expression::Alias(alias) => Some(alias.alias.name.clone()),
363        Expression::Column(col) => Some(col.name.name.clone()),
364        Expression::Star(_) => Some("*".to_string()),
365        Expression::Identifier(id) => Some(id.name.clone()),
366        Expression::Aliases(aliases) => aliases.expressions.iter().find_map(|e| match e {
367            Expression::Identifier(id) => Some(id.name.clone()),
368            _ => None,
369        }),
370        _ => None,
371    }
372}
373
374/// Collect all table names (as `String`) referenced in the expression tree.
375pub fn get_table_names(expr: &Expression) -> Vec<String> {
376    fn collect_cte_aliases(with_clause: &With, aliases: &mut HashSet<String>) {
377        for cte in &with_clause.ctes {
378            aliases.insert(cte.alias.name.clone());
379        }
380    }
381
382    fn push_table_ref_name(
383        table: &TableRef,
384        cte_aliases: &HashSet<String>,
385        names: &mut Vec<String>,
386    ) {
387        let name = table.name.name.clone();
388        if !name.is_empty() && !cte_aliases.contains(&name) {
389            names.push(name);
390        }
391    }
392
393    let mut cte_aliases: HashSet<String> = HashSet::new();
394    for node in expr.dfs() {
395        match node {
396            Expression::Select(select) => {
397                if let Some(with) = &select.with {
398                    collect_cte_aliases(with, &mut cte_aliases);
399                }
400            }
401            Expression::Insert(insert) => {
402                if let Some(with) = &insert.with {
403                    collect_cte_aliases(with, &mut cte_aliases);
404                }
405            }
406            Expression::Update(update) => {
407                if let Some(with) = &update.with {
408                    collect_cte_aliases(with, &mut cte_aliases);
409                }
410            }
411            Expression::Delete(delete) => {
412                if let Some(with) = &delete.with {
413                    collect_cte_aliases(with, &mut cte_aliases);
414                }
415            }
416            Expression::Union(union) => {
417                if let Some(with) = &union.with {
418                    collect_cte_aliases(with, &mut cte_aliases);
419                }
420            }
421            Expression::Intersect(intersect) => {
422                if let Some(with) = &intersect.with {
423                    collect_cte_aliases(with, &mut cte_aliases);
424                }
425            }
426            Expression::Except(except) => {
427                if let Some(with) = &except.with {
428                    collect_cte_aliases(with, &mut cte_aliases);
429                }
430            }
431            Expression::CreateTable(create) => {
432                if let Some(with) = &create.with_cte {
433                    collect_cte_aliases(with, &mut cte_aliases);
434                }
435            }
436            Expression::Merge(merge) => {
437                if let Some(with_) = &merge.with_ {
438                    if let Expression::With(with_clause) = with_.as_ref() {
439                        collect_cte_aliases(with_clause, &mut cte_aliases);
440                    }
441                }
442            }
443            _ => {}
444        }
445    }
446
447    let mut names = Vec::new();
448    for node in expr.dfs() {
449        match node {
450            Expression::Table(tbl) => {
451                let name = tbl.name.name.clone();
452                if !name.is_empty() && !cte_aliases.contains(&name) {
453                    names.push(name);
454                }
455            }
456            Expression::Insert(insert) => {
457                push_table_ref_name(&insert.table, &cte_aliases, &mut names);
458            }
459            Expression::Update(update) => {
460                push_table_ref_name(&update.table, &cte_aliases, &mut names);
461                for table in &update.extra_tables {
462                    push_table_ref_name(table, &cte_aliases, &mut names);
463                }
464            }
465            Expression::Delete(delete) => {
466                push_table_ref_name(&delete.table, &cte_aliases, &mut names);
467                for table in &delete.using {
468                    push_table_ref_name(table, &cte_aliases, &mut names);
469                }
470                for table in &delete.tables {
471                    push_table_ref_name(table, &cte_aliases, &mut names);
472                }
473            }
474            Expression::CreateTable(create) => {
475                push_table_ref_name(&create.name, &cte_aliases, &mut names);
476                if let Some(as_select) = &create.as_select {
477                    names.extend(get_table_names(as_select));
478                }
479                if let Some(with) = &create.with_cte {
480                    for cte in &with.ctes {
481                        names.extend(get_table_names(&cte.this));
482                    }
483                }
484            }
485            Expression::Cache(cache) => {
486                let name = cache.table.name.clone();
487                if !name.is_empty() && !cte_aliases.contains(&name) {
488                    names.push(name);
489                }
490            }
491            Expression::Uncache(uncache) => {
492                let name = uncache.table.name.clone();
493                if !name.is_empty() && !cte_aliases.contains(&name) {
494                    names.push(name);
495                }
496            }
497            Expression::CreateSynonym(synonym) => {
498                push_table_ref_name(&synonym.name, &cte_aliases, &mut names);
499                push_table_ref_name(&synonym.target, &cte_aliases, &mut names);
500            }
501            _ => {}
502        }
503    }
504
505    names
506}
507
508/// Collect all identifier references in the expression tree.
509pub fn get_identifiers(expr: &Expression) -> Vec<&Expression> {
510    expr.find_all(|e| matches!(e, Expression::Identifier(_)))
511}
512
513/// Collect all function call nodes in the expression tree.
514pub fn get_functions(expr: &Expression) -> Vec<&Expression> {
515    expr.find_all(|e| {
516        matches!(
517            e,
518            Expression::Function(_) | Expression::AggregateFunction(_)
519        )
520    })
521}
522
523/// Collect all literal value nodes in the expression tree.
524pub fn get_literals(expr: &Expression) -> Vec<&Expression> {
525    expr.find_all(|e| {
526        matches!(
527            e,
528            Expression::Literal(_) | Expression::Boolean(_) | Expression::Null(_)
529        )
530    })
531}
532
533/// Collect all subquery nodes in the expression tree.
534pub fn get_subqueries(expr: &Expression) -> Vec<&Expression> {
535    expr.find_all(|e| matches!(e, Expression::Subquery(_)))
536}
537
538/// Collect all aggregate function nodes in the expression tree.
539///
540/// Includes typed aggregates (`Count`, `Sum`, `Avg`, `Min`, `Max`, etc.)
541/// and generic `AggregateFunction` nodes.
542pub fn get_aggregate_functions(expr: &Expression) -> Vec<&Expression> {
543    expr.find_all(|e| {
544        matches!(
545            e,
546            Expression::AggregateFunction(_)
547                | Expression::Count(_)
548                | Expression::Sum(_)
549                | Expression::Avg(_)
550                | Expression::Min(_)
551                | Expression::Max(_)
552                | Expression::ApproxDistinct(_)
553                | Expression::ArrayAgg(_)
554                | Expression::GroupConcat(_)
555                | Expression::StringAgg(_)
556                | Expression::ListAgg(_)
557        )
558    })
559}
560
561/// Collect all window function nodes in the expression tree.
562pub fn get_window_functions(expr: &Expression) -> Vec<&Expression> {
563    expr.find_all(|e| matches!(e, Expression::WindowFunction(_)))
564}
565
566/// Count the total number of AST nodes in the expression tree.
567pub fn node_count(expr: &Expression) -> usize {
568    expr.dfs().count()
569}
570
571#[cfg(test)]
572mod tests {
573    use super::*;
574    use crate::parser::Parser;
575
576    fn parse_one(sql: &str) -> Expression {
577        let mut exprs = Parser::parse_sql(sql).unwrap();
578        exprs.remove(0)
579    }
580
581    #[test]
582    fn test_add_where() {
583        let expr = parse_one("SELECT a FROM t");
584        let cond = Expression::Eq(Box::new(BinaryOp::new(
585            Expression::column("b"),
586            Expression::number(1),
587        )));
588        let result = add_where(expr, cond, false);
589        let sql = result.sql();
590        assert!(sql.contains("WHERE"), "Expected WHERE in: {}", sql);
591        assert!(sql.contains("b = 1"), "Expected condition in: {}", sql);
592    }
593
594    #[test]
595    fn test_add_where_combines_with_and() {
596        let expr = parse_one("SELECT a FROM t WHERE x = 1");
597        let cond = Expression::Eq(Box::new(BinaryOp::new(
598            Expression::column("y"),
599            Expression::number(2),
600        )));
601        let result = add_where(expr, cond, false);
602        let sql = result.sql();
603        assert!(sql.contains("AND"), "Expected AND in: {}", sql);
604    }
605
606    #[test]
607    fn test_remove_where() {
608        let expr = parse_one("SELECT a FROM t WHERE x = 1");
609        let result = remove_where(expr);
610        let sql = result.sql();
611        assert!(!sql.contains("WHERE"), "Should not contain WHERE: {}", sql);
612    }
613
614    #[test]
615    fn test_set_limit() {
616        let expr = parse_one("SELECT a FROM t");
617        let result = set_limit(expr, 10);
618        let sql = result.sql();
619        assert!(sql.contains("LIMIT 10"), "Expected LIMIT in: {}", sql);
620    }
621
622    #[test]
623    fn test_set_limit_on_set_operation() {
624        let expr = parse_one("SELECT a FROM t UNION ALL SELECT a FROM u");
625        let result = set_limit(expr, 10);
626        let sql = result.sql();
627        assert_eq!(sql, "SELECT a FROM t UNION ALL SELECT a FROM u LIMIT 10");
628    }
629
630    #[test]
631    fn test_set_offset() {
632        let expr = parse_one("SELECT a FROM t");
633        let result = set_offset(expr, 5);
634        let sql = result.sql();
635        assert!(sql.contains("OFFSET 5"), "Expected OFFSET in: {}", sql);
636    }
637
638    #[test]
639    fn test_set_offset_on_set_operation() {
640        let expr = parse_one("SELECT a FROM t UNION ALL SELECT a FROM u");
641        let result = set_offset(expr, 5);
642        let sql = result.sql();
643        assert_eq!(sql, "SELECT a FROM t UNION ALL SELECT a FROM u OFFSET 5");
644    }
645
646    #[test]
647    fn test_set_order_by_on_set_operation() {
648        let expr = parse_one("SELECT a FROM t UNION ALL SELECT a FROM u");
649        let result = set_order_by(expr, vec![Expression::column("a")]);
650        let sql = result.sql();
651        assert_eq!(sql, "SELECT a FROM t UNION ALL SELECT a FROM u ORDER BY a");
652    }
653
654    #[test]
655    fn test_remove_limit_offset() {
656        let expr = parse_one("SELECT a FROM t LIMIT 10 OFFSET 5");
657        let result = remove_limit_offset(expr);
658        let sql = result.sql();
659        assert!(!sql.contains("LIMIT"), "Should not contain LIMIT: {}", sql);
660        assert!(
661            !sql.contains("OFFSET"),
662            "Should not contain OFFSET: {}",
663            sql
664        );
665    }
666
667    #[test]
668    fn test_get_column_names() {
669        let expr = parse_one("SELECT a, b, c FROM t");
670        let names = get_column_names(&expr);
671        assert!(names.contains(&"a".to_string()));
672        assert!(names.contains(&"b".to_string()));
673        assert!(names.contains(&"c".to_string()));
674    }
675
676    #[test]
677    fn test_get_output_column_names_select() {
678        let expr = parse_one("SELECT a, b AS c, 1 FROM t");
679        let names = get_output_column_names(&expr);
680        assert_eq!(names, vec!["a".to_string(), "c".to_string()]);
681    }
682
683    #[test]
684    fn test_get_output_column_names_union_left_projection() {
685        let expr =
686            parse_one("SELECT id, name FROM customers UNION ALL SELECT id, name FROM employees");
687        let names = get_output_column_names(&expr);
688        assert_eq!(names, vec!["id".to_string(), "name".to_string()]);
689    }
690
691    #[test]
692    fn test_get_output_column_names_union_uses_left_aliases() {
693        let expr = parse_one("SELECT id AS c1, name AS c2 FROM t1 UNION SELECT x, y FROM t2");
694        let names = get_output_column_names(&expr);
695        assert_eq!(names, vec!["c1".to_string(), "c2".to_string()]);
696    }
697
698    #[test]
699    fn test_get_column_names_union_still_returns_all_references() {
700        let expr =
701            parse_one("SELECT id, name FROM customers UNION ALL SELECT id, name FROM employees");
702        let names = get_column_names(&expr);
703        assert_eq!(
704            names,
705            vec![
706                "id".to_string(),
707                "name".to_string(),
708                "id".to_string(),
709                "name".to_string()
710            ]
711        );
712    }
713
714    #[test]
715    fn test_get_table_names() {
716        let expr = parse_one("SELECT a FROM users");
717        let names = get_table_names(&expr);
718        assert_eq!(names, vec!["users".to_string()]);
719    }
720
721    #[test]
722    fn test_get_table_names_excludes_cte_aliases() {
723        let expr = parse_one(
724            "WITH cte AS (SELECT * FROM users) SELECT * FROM cte JOIN orders o ON cte.id = o.id",
725        );
726        let names = get_table_names(&expr);
727        assert!(names.iter().any(|n| n == "users"));
728        assert!(names.iter().any(|n| n == "orders"));
729        assert!(!names.iter().any(|n| n == "cte"));
730    }
731
732    #[test]
733    fn test_get_table_names_includes_dml_targets() {
734        let insert_expr = parse_one("INSERT INTO users (id) VALUES (1)");
735        let insert_names = get_table_names(&insert_expr);
736        assert!(insert_names.iter().any(|n| n == "users"));
737
738        let update_expr =
739            parse_one("UPDATE users SET name = 'x' FROM accounts WHERE users.id = accounts.id");
740        let update_names = get_table_names(&update_expr);
741        assert!(update_names.iter().any(|n| n == "users"));
742        assert!(update_names.iter().any(|n| n == "accounts"));
743
744        let delete_expr =
745            parse_one("DELETE FROM users USING accounts WHERE users.id = accounts.id");
746        let delete_names = get_table_names(&delete_expr);
747        assert!(delete_names.iter().any(|n| n == "users"));
748        assert!(delete_names.iter().any(|n| n == "accounts"));
749
750        let create_expr = parse_one("CREATE TABLE out_table AS SELECT 1 AS id FROM src");
751        let create_names = get_table_names(&create_expr);
752        assert!(create_names.iter().any(|n| n == "out_table"));
753        assert!(create_names.iter().any(|n| n == "src"));
754    }
755
756    #[test]
757    fn test_node_count() {
758        let expr = parse_one("SELECT a FROM t");
759        let count = node_count(&expr);
760        assert!(count > 0, "Expected non-zero node count");
761    }
762
763    #[test]
764    fn test_rename_columns() {
765        let expr = parse_one("SELECT old_name FROM t");
766        let mut mapping = HashMap::new();
767        mapping.insert("old_name".to_string(), "new_name".to_string());
768        let result = rename_columns(expr, &mapping);
769        let sql = result.sql();
770        assert!(sql.contains("new_name"), "Expected new_name in: {}", sql);
771        assert!(
772            !sql.contains("old_name"),
773            "Should not contain old_name: {}",
774            sql
775        );
776    }
777
778    #[test]
779    fn test_rename_tables() {
780        let expr = parse_one("SELECT a FROM old_table");
781        let mut mapping = HashMap::new();
782        mapping.insert("old_table".to_string(), "new_table".to_string());
783        let result = rename_tables(expr, &mapping);
784        let sql = result.sql();
785        assert!(sql.contains("new_table"), "Expected new_table in: {}", sql);
786    }
787
788    #[test]
789    fn test_rename_tables_with_alias_renamed_tables() {
790        let expr = parse_one("SELECT a FROM old_table");
791        let mut mapping = HashMap::new();
792        mapping.insert("old_table".to_string(), "new_table".to_string());
793        let options = RenameTablesOptions::new().with_alias_renamed_tables(true);
794        let result = rename_tables_with_options(expr, &mapping, &options);
795        let sql = result.sql();
796
797        assert_eq!(sql, "SELECT a FROM new_table AS new_table");
798    }
799
800    #[test]
801    fn test_rename_tables_with_alias_preserves_existing_alias() {
802        let expr = parse_one("SELECT a FROM old_table AS t");
803        let mut mapping = HashMap::new();
804        mapping.insert("old_table".to_string(), "new_table".to_string());
805        let options = RenameTablesOptions::new().with_alias_renamed_tables(true);
806        let result = rename_tables_with_options(expr, &mapping, &options);
807        let sql = result.sql();
808
809        assert_eq!(sql, "SELECT a FROM new_table AS t");
810    }
811
812    #[test]
813    fn test_set_distinct() {
814        let expr = parse_one("SELECT a FROM t");
815        let result = set_distinct(expr, true);
816        let sql = result.sql();
817        assert!(sql.contains("DISTINCT"), "Expected DISTINCT in: {}", sql);
818    }
819
820    #[test]
821    fn test_add_select_columns() {
822        let expr = parse_one("SELECT a FROM t");
823        let result = add_select_columns(expr, vec![Expression::column("b")]);
824        let sql = result.sql();
825        assert!(
826            sql.contains("a, b") || sql.contains("a,b"),
827            "Expected a, b in: {}",
828            sql
829        );
830    }
831
832    #[test]
833    fn test_qualify_columns() {
834        let expr = parse_one("SELECT a, b FROM t");
835        let result = qualify_columns(expr, "t");
836        let sql = result.sql();
837        assert!(sql.contains("t.a"), "Expected t.a in: {}", sql);
838        assert!(sql.contains("t.b"), "Expected t.b in: {}", sql);
839    }
840
841    #[test]
842    fn test_get_functions() {
843        let expr = parse_one("SELECT COUNT(*), UPPER(name) FROM t");
844        let funcs = get_functions(&expr);
845        // UPPER is a typed function (Expression::Upper), not Expression::Function
846        // COUNT is Expression::Count, not Expression::AggregateFunction
847        // So get_functions (which checks Function | AggregateFunction) may return 0
848        // That's OK — we have separate get_aggregate_functions for typed aggs
849        let _ = funcs.len();
850    }
851
852    #[test]
853    fn test_get_aggregate_functions() {
854        let expr = parse_one("SELECT COUNT(*), SUM(x) FROM t");
855        let aggs = get_aggregate_functions(&expr);
856        assert!(
857            aggs.len() >= 2,
858            "Expected at least 2 aggregates, got {}",
859            aggs.len()
860        );
861    }
862}