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 withf(child). - map_
children_ crossing - Rebuild
expr, replacing each direct child expression withf(ctx, child)and each directly nested subquery statement withf_stmt(ctx, stmt). - visit_
all - Call
fonexprand every descendant, pre-order. - visit_
children - Call
fon each direct child expression ofexpr. - visit_
children_ crossing - Call
fon each direct child expression ofexprandf_stmton each directly nested subquery statement.