Skip to main content

sql_cli/sql/parser/
walk.rs

1//! Generic traversal helpers for [`SqlExpression`] trees.
2//!
3//! Before this module every consumer hand-rolled its own `match expr { ... }`
4//! over all 24 expression variants, each ending in a `_ => {}` catch-all. The
5//! duplication was not harmless: each copy silently skipped whichever variants
6//! its author forgot, so a transformer would quietly no-op on `CASE`, method
7//! calls, or tuple subqueries rather than fail.
8//!
9//! The two helpers here are **exhaustive by construction** — neither has a
10//! catch-all arm — so adding a variant to `SqlExpression` becomes a compile
11//! error in this file instead of a silent miss spread across the codebase.
12//!
13//! # Direct children only
14//!
15//! Both helpers visit a node's *direct* children and do not recurse. Callers
16//! drive the recursion, which is what lets a transformer intercept the nodes it
17//! cares about and delegate everything else:
18//!
19//! ```ignore
20//! fn transform(&self, expr: SqlExpression) -> SqlExpression {
21//!     match expr {
22//!         SqlExpression::BinaryOp { left, op, right } if op == "ILIKE" => {
23//!             /* the one real rule */
24//!         }
25//!         other => walk::map_children(other, |e| self.transform(e)),
26//!     }
27//! }
28//! ```
29//!
30//! [`visit_all`] is provided for the common collector case that genuinely wants
31//! every node.
32//!
33//! # Scope boundaries
34//!
35//! **Subqueries are opaque by default.** [`map_children`] and
36//! [`visit_children`] do not descend into the `SelectStatement` inside
37//! `ScalarSubquery`, `InSubquery`, `NotInSubquery`, `InSubqueryTuple` or
38//! `NotInSubqueryTuple`, because that statement is a *different query scope*.
39//! Descending automatically would be wrong for the alias expanders — a SELECT
40//! alias from the outer query must not be expanded inside a subquery that has
41//! its own FROM.
42//!
43//! Same-scope operands of those variants *are* visited: `InSubquery`'s `expr`
44//! and `InSubqueryTuple`'s `exprs` belong to the enclosing query, only the
45//! `subquery` itself is skipped.
46//!
47//! # Crossing the boundary
48//!
49//! Some transformers legitimately need to cross it — `ILIKE` -> `LIKE` is
50//! scope-independent, INTO removal and CTE hoisting have to reach nested
51//! statements by definition. Those callers use
52//! [`map_children_crossing`] / [`visit_children_crossing`], which take a second
53//! closure for the nested statement.
54//!
55//! Both closures take an explicit `ctx` parameter rather than capturing what
56//! they need. That is forced, not stylistic: a transformer whose recursion is
57//! `&mut self` (the CTE hoister) cannot hand out two closures that each capture
58//! `self` mutably. Threading the state through as `ctx` gives one mutable
59//! borrow, split across the two calls by the helper.
60//!
61//! **The crossing forms are the primitives.** `map_children` is defined as
62//! `map_children_crossing` with an identity statement handler, and
63//! `visit_children` as `visit_children_crossing` with a no-op one. This is
64//! deliberate: it means the set of subquery-bearing variants is written down
65//! **exactly once in the codebase**, in this file. A caller that hand-listed
66//! those variants itself would compile clean — and silently stop crossing —
67//! the day a new one is added (`Exists`, for instance). Here, adding a variant
68//! is a compile error in one place.
69//!
70//! Window specs, by contrast, *are* same-scope: `WindowSpec::order_by` holds
71//! real expressions and is descended into. (`partition_by` is `Vec<String>`,
72//! so there is nothing to walk.)
73
74use super::ast::{SelectStatement, SimpleWhenBranch, SqlExpression, WhenBranch};
75
76/// Rebuild `expr`, replacing each direct child expression with `f(child)`.
77///
78/// Leaf nodes are returned unchanged. Subquery statements are a scope boundary
79/// and are **not** descended into — see the module docs. Use
80/// [`map_children_crossing`] when you need to rewrite them too.
81pub fn map_children(
82    expr: SqlExpression,
83    mut f: impl FnMut(SqlExpression) -> SqlExpression,
84) -> SqlExpression {
85    // The closure is its own context; the identity statement handler is what
86    // makes subqueries opaque. It hands the `Box` straight back, so the opaque
87    // path -- which is most callers -- does no work at all for a subquery.
88    map_children_crossing(expr, &mut f, |f, e| f(e), |_, stmt| stmt)
89}
90
91/// Rebuild `expr`, replacing each direct child expression with `f(ctx, child)`
92/// **and** each directly nested subquery statement with `f_stmt(ctx, stmt)`.
93///
94/// This is the primitive [`map_children`] is built on; it is the only
95/// exhaustive match over `SqlExpression` in the rewrite path. Callers that must
96/// reach into nested statements (CTE hoisting, INTO removal, scope-independent
97/// operator rewrites) use this instead of hand-listing the subquery variants,
98/// so a newly added subquery-bearing variant breaks the build here rather than
99/// being silently skipped at each call site.
100///
101/// `ctx` carries whatever mutable state the two closures share — typically the
102/// transformer itself. See the module docs for why it is a parameter rather
103/// than a capture.
104///
105/// `f_stmt` takes and returns the `Box`, not the statement, so that the opaque
106/// case ([`map_children`], whose handler is `|_, stmt| stmt`) is a passthrough
107/// rather than an unbox/realloc of a large struct at every subquery.
108pub fn map_children_crossing<C>(
109    expr: SqlExpression,
110    ctx: &mut C,
111    mut f: impl FnMut(&mut C, SqlExpression) -> SqlExpression,
112    mut f_stmt: impl FnMut(&mut C, Box<SelectStatement>) -> Box<SelectStatement>,
113) -> SqlExpression {
114    match expr {
115        // ---- Leaves: nothing to walk ----
116        e @ (SqlExpression::Column(_)
117        | SqlExpression::StringLiteral(_)
118        | SqlExpression::NumberLiteral(_)
119        | SqlExpression::BooleanLiteral(_)
120        | SqlExpression::Null
121        | SqlExpression::DateTimeConstructor { .. }
122        | SqlExpression::DateTimeToday { .. }) => e,
123
124        // ---- Scope boundary: only `f_stmt` may touch the inner statement ----
125        SqlExpression::ScalarSubquery { query } => SqlExpression::ScalarSubquery {
126            query: f_stmt(ctx, query),
127        },
128
129        // ---- Same-scope children ----
130        SqlExpression::MethodCall {
131            object,
132            method,
133            args,
134        } => SqlExpression::MethodCall {
135            object,
136            method,
137            args: args.into_iter().map(|e| f(&mut *ctx, e)).collect(),
138        },
139
140        SqlExpression::ChainedMethodCall { base, method, args } => {
141            SqlExpression::ChainedMethodCall {
142                base: Box::new(f(&mut *ctx, *base)),
143                method,
144                args: args.into_iter().map(|e| f(&mut *ctx, e)).collect(),
145            }
146        }
147
148        SqlExpression::FunctionCall {
149            name,
150            args,
151            distinct,
152        } => SqlExpression::FunctionCall {
153            name,
154            args: args.into_iter().map(|e| f(&mut *ctx, e)).collect(),
155            distinct,
156        },
157
158        SqlExpression::WindowFunction {
159            name,
160            args,
161            mut window_spec,
162        } => {
163            let args = args.into_iter().map(|e| f(&mut *ctx, e)).collect();
164            // partition_by is Vec<String>; only order_by carries expressions.
165            for item in &mut window_spec.order_by {
166                let taken = std::mem::replace(&mut item.expr, SqlExpression::Null);
167                item.expr = f(&mut *ctx, taken);
168            }
169            SqlExpression::WindowFunction {
170                name,
171                args,
172                window_spec,
173            }
174        }
175
176        SqlExpression::BinaryOp { left, op, right } => SqlExpression::BinaryOp {
177            left: Box::new(f(&mut *ctx, *left)),
178            op,
179            right: Box::new(f(&mut *ctx, *right)),
180        },
181
182        SqlExpression::InList { expr, values } => SqlExpression::InList {
183            expr: Box::new(f(&mut *ctx, *expr)),
184            values: values.into_iter().map(|e| f(&mut *ctx, e)).collect(),
185        },
186
187        SqlExpression::NotInList { expr, values } => SqlExpression::NotInList {
188            expr: Box::new(f(&mut *ctx, *expr)),
189            values: values.into_iter().map(|e| f(&mut *ctx, e)).collect(),
190        },
191
192        SqlExpression::Between { expr, lower, upper } => SqlExpression::Between {
193            expr: Box::new(f(&mut *ctx, *expr)),
194            lower: Box::new(f(&mut *ctx, *lower)),
195            upper: Box::new(f(&mut *ctx, *upper)),
196        },
197
198        SqlExpression::Not { expr } => SqlExpression::Not {
199            expr: Box::new(f(&mut *ctx, *expr)),
200        },
201
202        SqlExpression::CaseExpression {
203            when_branches,
204            else_branch,
205        } => SqlExpression::CaseExpression {
206            when_branches: when_branches
207                .into_iter()
208                .map(|b| WhenBranch {
209                    condition: Box::new(f(&mut *ctx, *b.condition)),
210                    result: Box::new(f(&mut *ctx, *b.result)),
211                })
212                .collect(),
213            else_branch: else_branch.map(|e| Box::new(f(&mut *ctx, *e))),
214        },
215
216        SqlExpression::SimpleCaseExpression {
217            expr,
218            when_branches,
219            else_branch,
220        } => SqlExpression::SimpleCaseExpression {
221            expr: Box::new(f(&mut *ctx, *expr)),
222            when_branches: when_branches
223                .into_iter()
224                .map(|b| SimpleWhenBranch {
225                    value: Box::new(f(&mut *ctx, *b.value)),
226                    result: Box::new(f(&mut *ctx, *b.result)),
227                })
228                .collect(),
229            else_branch: else_branch.map(|e| Box::new(f(&mut *ctx, *e))),
230        },
231
232        SqlExpression::Unnest { column, delimiter } => SqlExpression::Unnest {
233            column: Box::new(f(&mut *ctx, *column)),
234            delimiter,
235        },
236
237        // ---- Subquery variants: same-scope operand via `f`, statement via `f_stmt` ----
238        SqlExpression::InSubquery { expr, subquery } => SqlExpression::InSubquery {
239            expr: Box::new(f(&mut *ctx, *expr)),
240            subquery: f_stmt(ctx, subquery),
241        },
242
243        SqlExpression::NotInSubquery { expr, subquery } => SqlExpression::NotInSubquery {
244            expr: Box::new(f(&mut *ctx, *expr)),
245            subquery: f_stmt(ctx, subquery),
246        },
247
248        SqlExpression::InSubqueryTuple { exprs, subquery } => SqlExpression::InSubqueryTuple {
249            exprs: exprs.into_iter().map(|e| f(&mut *ctx, e)).collect(),
250            subquery: f_stmt(ctx, subquery),
251        },
252
253        SqlExpression::NotInSubqueryTuple { exprs, subquery } => {
254            SqlExpression::NotInSubqueryTuple {
255                exprs: exprs.into_iter().map(|e| f(&mut *ctx, e)).collect(),
256                subquery: f_stmt(ctx, subquery),
257            }
258        }
259    }
260}
261
262/// Call `f` on each direct child expression of `expr`.
263///
264/// The borrowing counterpart to [`map_children`], for collectors that only read.
265/// Same scope rules apply: subquery statements are not visited. Use
266/// [`visit_children_crossing`] when you need to reach them.
267pub fn visit_children<'a>(expr: &'a SqlExpression, mut f: impl FnMut(&'a SqlExpression)) {
268    // The closure is its own context; the no-op statement handler is what makes
269    // subqueries opaque.
270    visit_children_crossing(expr, &mut f, |f, e| f(e), |_, _| {});
271}
272
273/// Call `f` on each direct child expression of `expr` **and** `f_stmt` on each
274/// directly nested subquery statement.
275///
276/// The borrowing counterpart to [`map_children_crossing`], and the primitive
277/// [`visit_children`] is built on. As there, the subquery-bearing variants are
278/// listed only here, so a new one is a compile error rather than a silent skip
279/// at every call site, and `ctx` carries the state the two closures share.
280pub fn visit_children_crossing<'a, C>(
281    expr: &'a SqlExpression,
282    ctx: &mut C,
283    mut f: impl FnMut(&mut C, &'a SqlExpression),
284    mut f_stmt: impl FnMut(&mut C, &'a SelectStatement),
285) {
286    match expr {
287        // ---- Leaves: nothing to walk ----
288        SqlExpression::Column(_)
289        | SqlExpression::StringLiteral(_)
290        | SqlExpression::NumberLiteral(_)
291        | SqlExpression::BooleanLiteral(_)
292        | SqlExpression::Null
293        | SqlExpression::DateTimeConstructor { .. }
294        | SqlExpression::DateTimeToday { .. } => {}
295
296        // ---- Scope boundary: only `f_stmt` may see the inner statement ----
297        SqlExpression::ScalarSubquery { query } => f_stmt(ctx, query),
298
299        // ---- Same-scope children ----
300        SqlExpression::MethodCall { args, .. } | SqlExpression::FunctionCall { args, .. } => {
301            args.iter().for_each(|e| f(&mut *ctx, e));
302        }
303
304        SqlExpression::ChainedMethodCall { base, args, .. } => {
305            f(&mut *ctx, base);
306            args.iter().for_each(|e| f(&mut *ctx, e));
307        }
308
309        SqlExpression::WindowFunction {
310            args, window_spec, ..
311        } => {
312            args.iter().for_each(|e| f(&mut *ctx, e));
313            // partition_by is Vec<String>; only order_by carries expressions.
314            window_spec
315                .order_by
316                .iter()
317                .for_each(|item| f(&mut *ctx, &item.expr));
318        }
319
320        SqlExpression::BinaryOp { left, right, .. } => {
321            f(&mut *ctx, left);
322            f(&mut *ctx, right);
323        }
324
325        SqlExpression::InList { expr, values } | SqlExpression::NotInList { expr, values } => {
326            f(&mut *ctx, expr);
327            values.iter().for_each(|e| f(&mut *ctx, e));
328        }
329
330        SqlExpression::Between { expr, lower, upper } => {
331            f(&mut *ctx, expr);
332            f(&mut *ctx, lower);
333            f(&mut *ctx, upper);
334        }
335
336        SqlExpression::Not { expr } | SqlExpression::Unnest { column: expr, .. } => {
337            f(&mut *ctx, expr)
338        }
339
340        SqlExpression::CaseExpression {
341            when_branches,
342            else_branch,
343        } => {
344            for branch in when_branches {
345                f(&mut *ctx, &branch.condition);
346                f(&mut *ctx, &branch.result);
347            }
348            if let Some(e) = else_branch {
349                f(&mut *ctx, e);
350            }
351        }
352
353        SqlExpression::SimpleCaseExpression {
354            expr,
355            when_branches,
356            else_branch,
357        } => {
358            f(&mut *ctx, expr);
359            for branch in when_branches {
360                f(&mut *ctx, &branch.value);
361                f(&mut *ctx, &branch.result);
362            }
363            if let Some(e) = else_branch {
364                f(&mut *ctx, e);
365            }
366        }
367
368        // ---- Subquery variants: same-scope operand via `f`, statement via `f_stmt` ----
369        SqlExpression::InSubquery { expr, subquery }
370        | SqlExpression::NotInSubquery { expr, subquery } => {
371            f(&mut *ctx, expr);
372            f_stmt(ctx, subquery);
373        }
374
375        SqlExpression::InSubqueryTuple { exprs, subquery }
376        | SqlExpression::NotInSubqueryTuple { exprs, subquery } => {
377            exprs.iter().for_each(|e| f(&mut *ctx, e));
378            f_stmt(ctx, subquery);
379        }
380    }
381}
382
383/// Call `f` on `expr` and every descendant, pre-order.
384///
385/// The usual entry point for collectors ("find every column reference",
386/// "find every aggregate"). Subquery statements are still not descended into —
387/// see the module docs.
388pub fn visit_all<'a>(expr: &'a SqlExpression, f: &mut impl FnMut(&'a SqlExpression)) {
389    f(expr);
390    visit_children(expr, |child| visit_all(child, f));
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use crate::sql::parser::ast::ColumnRef;
397    use crate::sql::recursive_parser::Parser;
398
399    /// Parse `SELECT <expr> FROM t` and hand back the projected expression.
400    fn expr_of(select_expr: &str) -> SqlExpression {
401        let sql = format!("SELECT {select_expr} FROM t");
402        let stmt = Parser::new(&sql)
403            .parse()
404            .unwrap_or_else(|e| panic!("{sql} should parse: {e}"));
405
406        stmt.select_items
407            .into_iter()
408            .find_map(|item| match item {
409                crate::sql::parser::ast::SelectItem::Expression { expr, .. } => Some(expr),
410                _ => None,
411            })
412            .expect("expected a projected expression")
413    }
414
415    /// Collect every column name reachable from `expr`.
416    fn columns(expr: &SqlExpression) -> Vec<String> {
417        let mut found = Vec::new();
418        visit_all(expr, &mut |e| {
419            if let SqlExpression::Column(c) = e {
420                found.push(c.name.clone());
421            }
422        });
423        found
424    }
425
426    /// Rename every column reference, recursing via `map_children`.
427    fn rename_all(expr: SqlExpression, to: &str) -> SqlExpression {
428        match expr {
429            SqlExpression::Column(_) => SqlExpression::Column(ColumnRef::unquoted(to.to_string())),
430            other => map_children(other, |e| rename_all(e, to)),
431        }
432    }
433
434    #[test]
435    fn visit_all_reaches_case_branches() {
436        let expr = expr_of("CASE WHEN a > 1 THEN b ELSE c END");
437        let mut found = columns(&expr);
438        found.sort();
439        assert_eq!(found, vec!["a", "b", "c"]);
440    }
441
442    #[test]
443    fn visit_all_reaches_nested_function_args() {
444        let expr = expr_of("UPPER(TRIM(name))");
445        assert_eq!(columns(&expr), vec!["name"]);
446    }
447
448    #[test]
449    fn visit_all_reaches_between_operands() {
450        let expr = expr_of("x BETWEEN lo AND hi");
451        assert_eq!(columns(&expr), vec!["x", "lo", "hi"]);
452    }
453
454    /// The payoff case: `WindowSpec::order_by` holds real expressions, and
455    /// every hand-rolled walker in the codebase missed them.
456    #[test]
457    fn visit_all_reaches_window_order_by() {
458        let expr = expr_of("ROW_NUMBER() OVER (ORDER BY created_at)");
459        assert!(
460            columns(&expr).contains(&"created_at".to_string()),
461            "window ORDER BY expressions must be reachable"
462        );
463    }
464
465    /// Subqueries are a scope boundary: the operand is walked, the inner
466    /// statement is not.
467    #[test]
468    fn walkers_do_not_cross_into_subqueries() {
469        let expr = expr_of("(SELECT MAX(inner_col) FROM other)");
470        assert!(
471            matches!(expr, SqlExpression::ScalarSubquery { .. }),
472            "expected a scalar subquery"
473        );
474        assert!(
475            columns(&expr).is_empty(),
476            "must not descend into a subquery's own scope"
477        );
478
479        // ...but a same-scope operand alongside one still is.
480        let stmt = Parser::new("SELECT a FROM t WHERE outer_col IN (SELECT x FROM other)")
481            .parse()
482            .expect("should parse");
483        let cond = &stmt.where_clause.expect("where clause").conditions[0].expr;
484        assert_eq!(columns(cond), vec!["outer_col"]);
485    }
486
487    /// The mirror of the test above, and the property the whole `crossing`
488    /// split exists to provide: the same variants that `map_children` treats as
489    /// opaque *must* be reachable through the crossing forms. If a new
490    /// subquery-bearing variant is ever added and wired only into the leaves,
491    /// this is what notices.
492    #[test]
493    fn crossing_walkers_do_reach_into_subqueries() {
494        // Visit side: the nested statement is handed to `f_stmt`.
495        let expr = expr_of("(SELECT MAX(inner_col) FROM other)");
496        let mut statements_seen = 0;
497        visit_children_crossing(&expr, &mut statements_seen, |_, _| {}, |n, _| *n += 1);
498        assert_eq!(
499            statements_seen, 1,
500            "a scalar subquery's statement must be reachable when crossing"
501        );
502
503        // Map side: and it can be rewritten in place.
504        let rewritten = map_children_crossing(
505            expr,
506            &mut (),
507            |_, e| e,
508            |_, mut stmt| {
509                stmt.limit = Some(1);
510                stmt
511            },
512        );
513        match rewritten {
514            SqlExpression::ScalarSubquery { query } => assert_eq!(query.limit, Some(1)),
515            other => panic!("expected a scalar subquery, got {other:?}"),
516        }
517    }
518
519    /// The tuple forms carry *both* same-scope operands and a nested statement;
520    /// crossing must reach both, not one or the other.
521    #[test]
522    fn crossing_reaches_tuple_subquery_operands_and_statement() {
523        let stmt = Parser::new("SELECT a FROM t WHERE (a, b) IN (SELECT x, y FROM u)")
524            .parse()
525            .expect("should parse");
526        let cond = &stmt.where_clause.expect("where clause").conditions[0].expr;
527
528        let mut ctx = (Vec::<String>::new(), 0);
529        visit_children_crossing(
530            cond,
531            &mut ctx,
532            |ctx, e| {
533                if let SqlExpression::Column(c) = e {
534                    ctx.0.push(c.name.clone());
535                }
536            },
537            |ctx, _| ctx.1 += 1,
538        );
539
540        assert_eq!(ctx.0, vec!["a", "b"], "same-scope operands must be visited");
541        assert_eq!(ctx.1, 1, "the subquery statement must be visited too");
542    }
543
544    #[test]
545    fn map_children_rewrites_nested_expressions() {
546        let expr = expr_of("CASE WHEN a > 1 THEN UPPER(b) ELSE c END");
547        let renamed = rename_all(expr, "z");
548        assert_eq!(columns(&renamed), vec!["z", "z", "z"]);
549    }
550
551    #[test]
552    fn map_children_rewrites_window_order_by() {
553        let expr = expr_of("ROW_NUMBER() OVER (ORDER BY created_at)");
554        let renamed = rename_all(expr, "z");
555        assert_eq!(columns(&renamed), vec!["z"]);
556    }
557
558    #[test]
559    fn map_children_leaves_leaves_alone() {
560        let expr = expr_of("42");
561        let mapped = map_children(expr, |_| panic!("a literal has no children"));
562        assert!(matches!(mapped, SqlExpression::NumberLiteral(ref n) if n == "42"));
563    }
564}