Skip to main content

Module walk

Module walk 

Source
Expand description

Generic traversal helpers for SqlExpression trees.

Before this module every consumer hand-rolled its own match expr { ... } over all 24 expression variants, each ending in a _ => {} catch-all. The duplication was not harmless: each copy silently skipped whichever variants its author forgot, so a transformer would quietly no-op on CASE, method calls, or tuple subqueries rather than fail.

The two helpers here are exhaustive by construction — neither has a catch-all arm — so adding a variant to SqlExpression becomes a compile error in this file instead of a silent miss spread across the codebase.

§Direct children only

Both helpers visit a node’s direct children and do not recurse. Callers drive the recursion, which is what lets a transformer intercept the nodes it cares about and delegate everything else:

fn transform(&self, expr: SqlExpression) -> SqlExpression {
    match expr {
        SqlExpression::BinaryOp { left, op, right } if op == "ILIKE" => {
            /* the one real rule */
        }
        other => walk::map_children(other, |e| self.transform(e)),
    }
}

visit_all is provided for the common collector case that genuinely wants every node.

§Scope boundaries

Subqueries are opaque by default. map_children and visit_children do not descend into the SelectStatement inside ScalarSubquery, InSubquery, NotInSubquery, InSubqueryTuple or NotInSubqueryTuple, because that statement is a different query scope. Descending automatically would be wrong for the alias expanders — a SELECT alias from the outer query must not be expanded inside a subquery that has its own FROM.

Same-scope operands of those variants are visited: InSubquery’s expr and InSubqueryTuple’s exprs belong to the enclosing query, only the subquery itself is skipped.

§Crossing the boundary

Some transformers legitimately need to cross it — ILIKE -> LIKE is scope-independent, INTO removal and CTE hoisting have to reach nested statements by definition. Those callers use map_children_crossing / visit_children_crossing, which take a second closure for the nested statement.

Both closures take an explicit ctx parameter rather than capturing what they need. That is forced, not stylistic: a transformer whose recursion is &mut self (the CTE hoister) cannot hand out two closures that each capture self mutably. Threading the state through as ctx gives one mutable borrow, split across the two calls by the helper.

The crossing forms are the primitives. map_children is defined as map_children_crossing with an identity statement handler, and visit_children as visit_children_crossing with a no-op one. This is deliberate: it means the set of subquery-bearing variants is written down exactly once in the codebase, in this file. A caller that hand-listed those variants itself would compile clean — and silently stop crossing — the day a new one is added (Exists, for instance). Here, adding a variant is a compile error in one place.

Window specs, by contrast, are same-scope: WindowSpec::order_by holds real expressions and is descended into. (partition_by is Vec<String>, so there is nothing to walk.)

Functions§

map_children
Rebuild expr, replacing each direct child expression with f(child).
map_children_crossing
Rebuild expr, replacing each direct child expression with f(ctx, child) and each directly nested subquery statement with f_stmt(ctx, stmt).
visit_all
Call f on expr and every descendant, pre-order.
visit_children
Call f on each direct child expression of expr.
visit_children_crossing
Call f on each direct child expression of expr and f_stmt on each directly nested subquery statement.