fix: preserve output field name when reversing aggregate window expressions - #24887
Open
timsaucer wants to merge 2 commits into
Open
fix: preserve output field name when reversing aggregate window expressions#24887timsaucer wants to merge 2 commits into
timsaucer wants to merge 2 commits into
Conversation
When the physical optimizer reverses a window expression to avoid an extra sort, and that window expression is backed by an aggregate UDAF, the reversed expression's output field name was rewritten (`last_value(v) ORDER BY [t DESC]` -> `first_value(v) ORDER BY [t ASC]`). The window exec derives its schema from `WindowExpr::field()`, so the node's output column was renamed while parent plan nodes still referenced the old name, and planning failed in `ProjectionMapping::try_new`. Add `AggregateFunctionExpr::reverse_expr_preserving_name` and use it from `PlainAggregateWindowExpr`/`SlidingAggregateWindowExpr::get_reverse_expr`, so the window path keeps its name the way `WindowUDFExpr::reverse_expr` already does. `AggregateExec` pins its schema at construction, so the plain-aggregate `reverse_expr` keeps renaming and continues to show which implementation runs. The two `get_reverse_expr` bodies were identical; factor them into a shared `reverse_aggregate_window_expr` helper. Also assert in `get_best_fitting_window` that reversal did not change any output field name, so a future renaming `WindowExpr` fails there instead of at a distant `ProjectionMapping` assertion. Closes apache#24884 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24887 +/- ##
=======================================
Coverage 81.64% 81.64%
=======================================
Files 1123 1123
Lines 410248 410253 +5
Branches 410248 410253 +5
=======================================
+ Hits 334940 334952 +12
+ Misses 55617 55609 -8
- Partials 19691 19692 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
timsaucer
commented
Sep 2, 2026
| ReversedUDAF::Identical => Some(self.clone()), | ||
| ReversedUDAF::Reversed(reverse_udf) => { | ||
| let was_aliased = self.human_display_alias().is_some(); | ||
| let keep_name = preserve_name || self.human_display_alias().is_some(); |
Member
Author
There was a problem hiding this comment.
This is really the crux of this PR. There is a bit of refactoring to take common code from datafusion/physical-expr/src/window/sliding_aggregate.rs and also datafusion/physical-expr/src/window/aggregate.rs so that they treat it identically. Then we just need to pass around that boolean that says whether or not to preserver the name after rewriting.
timsaucer
marked this pull request as ready for review
September 3, 2026 16:59
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?
Rationale for this change
Using an aggregate UDAF as a window function (reachable through the DataFrame API) fails to plan whenever the physical optimizer decides to reverse the window to avoid an extra sort:
get_best_fitting_windowswaps inWindowExpr::get_reverse_expr(), and for aggregate-backed window expressions that reachesAggregateFunctionExpr::reverse_expr, which rewrites the aggregate's output name (last_value(v) ORDER BY [t DESC]→first_value(v) ORDER BY [t ASC]). A window exec derives its schema fromWindowExpr::field(), so the node's output column gets renamed while the parentProjectionExecstill holds aColumnwith the old name.Renaming on reversal only makes sense for
AggregateExec, which pins its schema at construction (try_newbuilds the schema before reversing exprs) — there the renamed name is a useful signal inEXPLAINof which implementation actually runs, and no parent references it. The window path has the opposite requirement, which is whyWindowUDFExpr::reverse_expralready carriesnameover unchanged. This makes the aggregate-backed window path behave the same way.The equivalent SQL query does not fail, because in SQL
last_value(v) OVER (...)resolves to thelast_valuewindow UDF, which reverses without renaming.Scope
Reviewed the rest of this bug class while here:
first_value↔last_valueis the onlyReversedUDAF::Reversedpair with a different name;array_agg,string_aggandnth_valuereverse to themselves, so the rewrite is a no-op for them.get_best_fitting_window—enforce_sortingandenforce_distribution— so the fix is applied at theget_reverse_exprlevel to cover both.OptimizeAggregateOrderalso callsreverse_expr, butAggregateExec::with_new_aggr_exprskeeps the original schema, so it cannot rename an output field.OptimizationInvariantCheckerdoes compare field names, but only for the root plan schema, so a rename on an intermediate node under a name-preserving projection is invisible to it. Hence the local assertion below.What changes are included in this PR?
AggregateFunctionExpr::reverse_expris refactored intoreverse_expr_inner(preserve_name), with a new publicreverse_expr_preserving_name(). Existingreverse_exprbehavior is unchanged.PlainAggregateWindowExpr::get_reverse_exprandSlidingAggregateWindowExpr::get_reverse_expruse the name-preserving variant. Their bodies were byte-identical, so they are factored into a sharedreverse_aggregate_window_exprhelper.get_best_fitting_windownow asserts that reversal did not change any output field name, so a future renamingWindowExprimplementation fails there — naming the culprit — instead of at a distantProjectionMappingassertion.Note that the prefix-based
replace_fn_name_clausealso mangled user-supplied aliases beginning withlast_value/first_value(e.g. an aliaslast_value_descbecamefirst_value_desc); that is fixed on the window path too.What is the testing strategy for this PR?
Two new regression tests, both verified to fail without the fix and pass with it:
datafusion/core/tests/dataframe/mod.rs—window_reversal_preserves_output_field_namesreproduces the issue through the DataFrame API and reproduces the reported error exactly. It asserts oncreate_physical_plan()rather thancollect(), because execution then hits the separate missing-retract_batchgap tracked by Support first_value/last_value aggregates as sliding window aggregates (blocks FILTER + windowed first/last value) #24885; a comment marks where to upgrade the assertion once that lands.datafusion/core/tests/physical_optimizer/window_optimize.rs—test_window_reversal_preserves_output_field_namesbuilds two opposite-ORDER BYaggregate-UDAF windows under a projection, runsEnsureRequirements, and asserts the optimized plan's schema field names are unchanged. Without the fix it fails withInput field name first_value_desc does not match with the projection expression last_value_desc.The existing
test_reverse_expr_preserves_non_aliased_display_pathand the two neighboringreverse_exprdisplay tests still pass, confirming the plain-aggregate renaming is untouched. All 510 sqllogictest files pass with no expectation changes — SQL cannot reach the renaming branch, and existing reversal expectations already show the original name with a reversed frame, which is exactly the shape this fix produces.Full extended suite (
--features avro,json,backtrace,extended_tests,recursive_protection,parquet_encryption): 11055 passed, 0 failed.cargo clippy --all-targets --all-features -- -D warningsclean.Are there any user-facing changes?
Queries that previously failed to plan now plan successfully; no expected output or plan text changes for anything that worked before.
AggregateFunctionExpr::reverse_expr_preserving_nameis a new public method — additive, no breaking API changes.🤖 Generated with Claude Code