Repository navigation
fix: keep renamed columns intact in leaf projection pushdown - #25478
Merged
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #25478 +/- ##
==========================================
- Coverage 82.92% 82.92% -0.01%
==========================================
Files 1155 1155
Lines 458204 458268 +64
Branches 458204 458268 +64
==========================================
+ Hits 379965 380011 +46
- Misses 55205 55206 +1
- Partials 23034 23051 +17 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
A sub-query projection that renames a column to the name of a different
column of the same input made `push_down_leaf_projections` fail when a
struct field is read above it:
Schema error: Schema contains qualified field name t.a and
unqualified field name a which would be ambiguous
The merge of the extraction projection into the projection below it
resolved every column the parent needs through the projection's rename
map, then added the input column that the name resolves to. For
`select t.a as b, t.b as a, s from t`, the parent's `b` resolves to
`t.a`, and `t.a` lands beside the output field `a`.
The merge keeps every expression of the projection below it, so the
parent can still read each name that projection produces. Skip those
names instead of resolving them, and never add a pass-through column
whose name is one of the projection's output names.
The recovery check then needs the same care, or the shape gives wrong
results instead of an error. Equal sets of field names do not prove that
the plan below carries the same values. That hunk is the same change as
apache#25445, so the two merge
without a conflict.
Three plan snapshots lose a duplicate pass-through column in the
intermediate stage. The optimized plans do not change.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Add the statements from the report as sqllogictest cases: a rename beside a same-name alias under a filter, a limit, an order by and a group by, a swap of two column names, and a swap where the struct field name is also a column name. Add two optimizer unit tests: one for the ambiguous schema, one for the recovery projection that must keep a rename alive. Co-Authored-By: Claude Fable 5.1 <[email protected]>
adriangb
force-pushed
the
fix-leaf-rename-swap-ambiguity
branch
from
October 3, 2026 20:25
ba695d4 to
dcbc474
Compare
cetra3
approved these changes
Oct 6, 2026
cetra3
left a comment
Contributor
There was a problem hiding this comment.
LGTM. I hit this bug again today so would be good to get it merged
kosiew
approved these changes
Oct 7, 2026
|
|
||
| # a rename beside a same-name alias, under a filter | ||
| query IIT | ||
| select b, a, s['x'] from (select rename_swap_struct.a as b, rename_swap_struct.b as a, s from rename_swap_struct) where a > 0 order by b; |
Contributor
There was a problem hiding this comment.
Optional: could we change this to where a > 15 and expect only 2 20 q? The current a > 0 case passes with either possible binding because both source columns are positive, while a > 15 would also verify the alias binding at the result level.
Resolve conflicts with the pass-through column resolution from main (resolve_against), keeping the output-name guard. Change the filter in the rename-swap struct test to a > 15 so the result distinguishes the two possible bindings (review suggestion). Co-Authored-By: Claude Opus 5.5 <[email protected]>
adriangb
enabled auto-merge
October 19, 2026 19:57
cetra3
pushed a commit
to pydantic/datafusion
that referenced
this pull request
Nov 9, 2026
…25478) ## Which issue does this PR close? - Closes apache#25446. Part of the leaf-pushdown EPIC: apache#25459 ## Rationale for this change A valid statement fails to plan. A sub-query projection renames a column to the name of a different column of the same input, and the query reads a struct field above it. `datafusion.optimizer.enable_leaf_expression_pushdown` is `true` by default, so this is a plain planning failure. ```sql create table t(a int, b int, s struct<x varchar>) as values (1, 10, {x: 'p'}), (2, 20, {x: 'q'}); -- 1. a rename beside a same-name alias, under a filter select b, a, s['x'] from (select t.a as b, t.b as a, s from t) where a > 0; -- 2. the same shape under a limit select b, a, s['x'] from (select t.a as b, t.b as a, s from t) limit 10; -- 3. a swap of two column names select a, c, s['x'] from (select t.b as a, t.a as c, s from t) where a > 0; ``` Observed on `main`, for all three: ```text Optimizer rule 'push_down_leaf_projections' failed caused by Schema error: Schema contains qualified field name t.a and unqualified field name a which would be ambiguous ``` Expected, and what you get with `set datafusion.optimizer.enable_leaf_expression_pushdown = false;`: ```text +---+----+--------+ +----+---+--------+ | b | a | t.s[x] | | a | c | t.s[x] | +---+----+--------+ +----+---+--------+ | 1 | 10 | p | | 10 | 1 | p | | 2 | 20 | q | | 20 | 2 | q | +---+----+--------+ +----+---+--------+ statements 1 and 2 statement 3 ``` `order by` and `group by` above the rename fail the same way. Query generators emit this shape. ## What changes are included in this PR? Both changes are in `datafusion/optimizer/src/extract_leaf_expressions.rs`. **1. The merge of the extraction projection.** `build_extraction_projection_impl` merges the extraction projection into the projection below it. For every column the parent needs, it resolved the name through the projection's rename map and then added the *input* column that the name resolves to. For `select t.a as b, t.b as a, s from t` the parent's `b` resolves to `t.a`, and `t.a` lands beside the output field `a` of the other rename. `Projection::try_new` rejects that schema. The merge keeps every expression of the projection below it, so the parent can still read each name that projection produces. Those names are now skipped instead of resolved, and a pass-through column whose name is one of the projection's output names is never added. A rename is a computed output, and the parent refers to it by its output name only. **2. The recovery check.** With the merge fixed, the shape reaches `split_and_push_projection`, which decided if it needs a recovery projection from the set of unqualified field names alone. Equal name sets do not prove equal values: the pushed plan exposes the table column `t.a` where the projection computed `t.a AS b`. Without this hunk the statements above return the columns swapped instead of an error. The recovery projection now also stays when a recovery expression is not a pass-through of a column. That second hunk is the same change as apache#25445, down to the text, so the two merge without a conflict. If that PR lands first, the hunk falls out of the rebase. **How this composes with the other open PRs on this function.** apache#25412 resolves both sides of the pass-through comparison against the input schema, and apache#25456 replaces the rename map with `ProjectionInliner`. This change sits above both: it decides which columns to add before either one resolves a name. I merged this branch into a local branch that holds both PRs. The single conflict is the loop header, which takes the guards of this PR around the body of the other two. The optimizer tests, the `struct`, `cse` and `push_down_filter` sqllogictest files, and the differential fuzz all pass there with the swap-alias shape of the fuzz generator turned on: 250 short cases, 125 short Parquet cases, 5000 extended cases and 1000 extended Parquet cases, 0 failures and 0 skips. ## What is the testing strategy for this PR? - `datafusion/sqllogictest/test_files/struct.slt`: the three statements above, plus the `order by` and `group by` variants, plus a swap where the struct field name is also a column name of the table, plus one `EXPLAIN` that shows the renames above the extraction projection and the struct field still read at the scan. - `datafusion/optimizer/src/extract_leaf_expressions.rs`: `test_extract_above_projection_that_swaps_column_names` covers the ambiguous schema. `test_extract_above_projection_that_redefines_column_name` covers the recovery projection that must keep a rename alive. - Three plan snapshots of neighbouring tests lose a duplicate pass-through column in the intermediate stage. The optimized plans of those three tests do not change. Commands and results: ```text cargo test --profile ci -p datafusion-optimizer 884, 26, 5 passed; 0 failed cargo test --profile ci -p datafusion-sqllogictest --test sqllogictests 520/520 files completed, 0 failures cargo clippy --profile ci -p datafusion-optimizer --all-targets -- -D warnings clean ``` ## Are there any user-facing changes? Statements of this shape plan and run. There is no API change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <[email protected]> Backport of apache#25478 (merged upstream as 233aeab).
roeap
added a commit
to roeap/delta-rs
that referenced
this pull request
Nov 13, 2026
Scan::declarative_metadata_scan_plan failed on every table. Three changes make it plan and run: - Leave DataFusion's extract_leaf_expressions and push_down_leaf_projections out of the executor's sessions. In DF 55 they fail with DuplicateUnqualifiedField / AmbiguousReference when a get_field moves below a projection that renames its input's columns, which every lowered scan has (scan_json.add AS add). Fixed upstream by apache/datafusion#25478 (issue #25446), first in DataFusion 56; restore the rules with that upgrade. - Project a value to its declared type through a declared_cast UDF (arrow's cast, struct NULLs unioned into children) where only nested nullability differs. DataFusion's CAST refuses to tighten it, and kernel declares the scan key's nullable deletionVector with non-nullable children. - Narrow a semi join's build side to its keys, under names the probe side does not use, instead of overwriting the join schema. The scan plan anti-joins two relations that both carry `add` and `file_action_key`, which any rule that recomputes the join schema rejects. metadata_scan_shape_plans_and_runs reproduces the plan's shape and fails without either of the first two changes. Co-authored-by: Isaac <[email protected]>
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.
Which issue does this PR close?
push_down_leaf_projectionsfails with an ambiguous schema when a sub-query projection renames columns #25446.Part of the leaf-pushdown EPIC: #25459
Rationale for this change
A valid statement fails to plan. A sub-query projection renames a column to the name of a different column of the same input, and the query reads a struct field above it.
datafusion.optimizer.enable_leaf_expression_pushdownistrueby default, so this is a plain planning failure.Observed on
main, for all three:Expected, and what you get with
set datafusion.optimizer.enable_leaf_expression_pushdown = false;:order byandgroup byabove the rename fail the same way. Query generators emit this shape.What changes are included in this PR?
Both changes are in
datafusion/optimizer/src/extract_leaf_expressions.rs.1. The merge of the extraction projection.
build_extraction_projection_implmerges the extraction projection into the projection below it. For every column the parent needs, it resolved the name through the projection's rename map and then added the input column that the name resolves to. Forselect t.a as b, t.b as a, s from tthe parent'sbresolves tot.a, andt.alands beside the output fieldaof the other rename.Projection::try_newrejects that schema.The merge keeps every expression of the projection below it, so the parent can still read each name that projection produces. Those names are now skipped instead of resolved, and a pass-through column whose name is one of the projection's output names is never added. A rename is a computed output, and the parent refers to it by its output name only.
2. The recovery check. With the merge fixed, the shape reaches
split_and_push_projection, which decided if it needs a recovery projection from the set of unqualified field names alone. Equal name sets do not prove equal values: the pushed plan exposes the table columnt.awhere the projection computedt.a AS b. Without this hunk the statements above return the columns swapped instead of an error. The recovery projection now also stays when a recovery expression is not a pass-through of a column.That second hunk is the same change as #25445, down to the text, so the two merge without a conflict. If that PR lands first, the hunk falls out of the rebase.
How this composes with the other open PRs on this function. #25412 resolves both sides of the pass-through comparison against the input schema, and #25456 replaces the rename map with
ProjectionInliner. This change sits above both: it decides which columns to add before either one resolves a name. I merged this branch into a local branch that holds both PRs. The single conflict is the loop header, which takes the guards of this PR around the body of the other two. The optimizer tests, thestruct,cseandpush_down_filtersqllogictest files, and the differential fuzz all pass there with the swap-alias shape of the fuzz generator turned on: 250 short cases, 125 short Parquet cases, 5000 extended cases and 1000 extended Parquet cases, 0 failures and 0 skips.What is the testing strategy for this PR?
datafusion/sqllogictest/test_files/struct.slt: the three statements above, plus theorder byandgroup byvariants, plus a swap where the struct field name is also a column name of the table, plus oneEXPLAINthat shows the renames above the extraction projection and the struct field still read at the scan.datafusion/optimizer/src/extract_leaf_expressions.rs:test_extract_above_projection_that_swaps_column_namescovers the ambiguous schema.test_extract_above_projection_that_redefines_column_namecovers the recovery projection that must keep a rename alive.Commands and results:
Are there any user-facing changes?
Statements of this shape plan and run. There is no API change.
🤖 Generated with Claude Code