refactor(query_plan): migrate the three boundary-crossing transformers onto walk.rs (R2, group 1)#33
Merged
Merged
Conversation
…dren First of the R2 transformer migrations (docs/ENGINE_REFACTORING.md). remove_from_expression was ~85 lines of hand-rolled recursion wrapping one real rule: clear `into_table` on every nested SelectStatement. The subquery arms stay explicit and must not be delegated. map_children treats a subquery statement as a scope boundary and deliberately does not descend into it -- right for the alias expanders, but exactly what this transformer has to do. Delegating them would have silently stopped INTO removal in nested queries. Not a pure refactor: the hand-rolled catch-all was also swallowing InSubqueryTuple / NotInSubqueryTuple, so INTO inside `WHERE (a, b) IN (SELECT ... INTO #t)` was never removed and reached the executor. Those arms are now explicit, which fixes it. The migration also picks up expression positions under MethodCall / ChainedMethodCall / WindowFunction / Unnest, though an INTO nested there is exotic. Regression test parses real SQL rather than hand-building an AST (R4); verified it fails without the tuple arms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second of the R2 transformer migrations. transform_expression was ~135 lines of hand-rolled recursion wrapping a single real rule (ILIKE -> UPPER() LIKE UPPER()); the traversal is now ~42. Subquery arms stay explicit. ILIKE -> LIKE is scope-independent so this transformer deliberately crosses into nested statements, which map_children will not do on its own. Delegating them would have silently stopped ILIKE being rewritten inside subqueries. Not a pure refactor -- two real fixes, both previously silent: 1. WindowSpec::order_by holds real expressions, and the old walker passed window_spec straight through. An ILIKE inside OVER (ORDER BY ...) was left as ILIKE and reached the executor as an unknown operator. 2. InSubqueryTuple / NotInSubqueryTuple fell into the catch-all, so neither the LHS operands nor the subquery body were transformed. Both are covered by tests that parse real SQL (R4). Verified both fail against the old behaviour -- restoring the old WindowFunction arm and dropping the tuple arms leaves ops: ["ILIKE"] in each case. Also drops two imports the migration made redundant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Third of the R2 transformer migrations, and the exemplar the walk.rs module docs name: a caller that deliberately crosses the subquery scope boundary. Both walks keep explicit subquery arms and delegate the rest. - hoist_from_expression -> walk::map_children (~105 lines to ~37) - find_cte_refs_in_expression -> walk::visit_children (~60 to ~30) - deletes dead hoist_from_condition (R5; never called, flagged in docs/ENGINE_REFACTORING.md) Unlike the previous two migrations this one is, as far as I can demonstrate, a PURE REFACTOR rather than a fix. The survey predicted the tuple-subquery arms would be a behaviour change, but probing the parser shows WITH is rejected everywhere in expression position: (a, b) IN (WITH c AS (...) SELECT ...) -> "Tuple IN requires a subquery" x = UPPER((WITH c AS (...) SELECT ...)) -> parse error x BETWEEN (WITH ...) AND 5 -> parse error So no input reaches the new hoisting arms today -- and by the same argument the pre-existing ScalarSubquery/InSubquery arms are equally unreachable. I could not write a test that fails without the change, so I have not claimed one; the comments in the code say "defensive, not a demonstrable fix" rather than implying a bug was fixed. The one genuinely reachable gain is in find_cte_refs_in_expression: its tuple arms only need a *reference* to an already-hoisted CTE, not a nested WITH, so dependency ordering now sees those. Verified no function was lost in the rewrite by diffing the fn inventory against HEAD -- only hoist_from_condition, which was the intended deletion. (An earlier span-based edit had silently swallowed hoist_from_where_clause; the compiler caught it, and the inventory diff is the check that would have caught it sooner.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 2 of the R2 work (
docs/ENGINE_REFACTORING.md), following #31. Three commits, one per transformer. Net −109 lines (327 deleted, 218 added — the added side includes new tests and doc comments).Why these three first
ilike_to_like,cte_hoisterandinto_clause_removerare the transformers that currently descend into subquery statements.walk::map_childrentreats a subquery as a scope boundary and won't cross it, so a naive migration would silently stop each of them working inside nested queries. They need a hybrid — explicit subquery arms, delegate the rest — and getting that pattern right on the risky files first seemed better than saving them for last.Correction to my earlier framing
I told you step 2 would be "low risk, isolated" and implied a pure refactor. That was wrong for two of these, and — as it turns out — wrong in the opposite direction for the third.
into_clause_removerilike_to_likecte_hoisterThe fixes (each with a test verified to fail on the old code)
ilike_to_like, window specs.WindowSpec::order_byholds real expressions and the old walker passedwindow_specstraight through. An ILIKE insideOVER (ORDER BY CASE WHEN name ILIKE ... END)was left asILIKEand reached the executor as an unknown operator. Probe output on the old code:found ops: ["ILIKE"].ilike_to_like+into_clause_remover, tuple subqueries.InSubqueryTuple/NotInSubqueryTuplefell into the catch-all, soWHERE (a, b) IN (SELECT ... INTO #t)kept its INTO, and an ILIKE in a tuple subquery body was never rewritten.Where the survey was wrong, and I checked
The plan predicted
cte_hoister's tuple arms would be a behaviour change too. Probing the parser says otherwise —WITHis rejected everywhere in expression position:So nothing reaches the new hoisting arms — and by the same argument the pre-existing
ScalarSubquery/InSubqueryarms in that function are equally unreachable. I couldn't write a test that fails without the change, so I haven't claimed one. The code comments say "defensive, not a demonstrable fix" rather than implying a bug was fixed, and I dropped the test I'd written once I found it couldn't parse.The one reachable gain there is in
find_cte_refs_in_expression: its tuple arms need only a reference to an already-hoisted CTE, not a nestedWITH, so dependency ordering now sees those.Also
cte_hoister::hoist_from_condition(R5).Process note: a span-based edit silently swallowed
hoist_from_where_clausewhile removing the dead function. The compiler caught it immediately, but I've since diffed the fullfninventory againstHEADto confirm nothing else went missing — only the intended deletion. Worth repeating on the remaining migrations.Verification
cargo test: 683 passed. One pre-existing failure,history_protection_integration— unrelated, reads the real roaming history.--check: contract holds, unchanged at 64 AGREE / 1 DIFFER / 7 GAP.cargo fmtclean.Next
Group 2 —
where_alias_expander(~200 lines),expression_lifter(3 walks that have drifted apart), andhaving_alias_transformer, which is the biggest correctness win in the set: it handles onlyFunctionCall/BinaryOp/Not, soHAVING COUNT(*) BETWEEN 1 AND 5andHAVING CASE WHEN COUNT(*) > 5 ...are silently not rewritten today.🤖 Generated with Claude Code