This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git


The following commit(s) were added to refs/heads/main by this push:
     new 4c031a267e fix: preserve output field name when reversing aggregate 
window expressions (#24887)
4c031a267e is described below

commit 4c031a267e8621cb0ec8c5c5a9853c4c0e1c97c0
Author: Tim Saucer <[email protected]>
AuthorDate: Thu Sep 10 02:15:07 2026 +0000

    fix: preserve output field name when reversing aggregate window expressions 
(#24887)
    
    ## Which issue does this PR close?
    
    - Closes #24884.
    
    ## 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:
    
    ```
    EnsureRequirements
    caused by
    Internal error: Assertion failed: col.name() == matching_name: Input field 
name
    first_value(?table?.v) ORDER BY [?table?.t ASC NULLS FIRST] ROWS BETWEEN 
UNBOUNDED PRECEDING AND CURRENT ROW
    does not match with the projection expression
    last_value(?table?.v) ORDER BY [?table?.t DESC NULLS LAST] ROWS BETWEEN 
UNBOUNDED PRECEDING AND CURRENT ROW.
    ```
    
    `get_best_fitting_window` swaps in `WindowExpr::get_reverse_expr()`, and
    for aggregate-backed window expressions that reaches
    `AggregateFunctionExpr::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 from
    `WindowExpr::field()`, so the node's output column gets renamed while
    the parent `ProjectionExec` still holds a `Column` with the old name.
    
    Renaming on reversal only makes sense for `AggregateExec`, which pins
    its schema at construction (`try_new` builds the schema *before*
    reversing exprs) — there the renamed name is a useful signal in
    `EXPLAIN` of which implementation actually runs, and no parent
    references it. The window path has the opposite requirement, which is
    why `WindowUDFExpr::reverse_expr` already carries `name` over 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 the `last_value` *window UDF*, which reverses
    without renaming.
    
    ### Scope
    
    Reviewed the rest of this bug class while here:
    
    - `first_value` ↔ `last_value` is the only `ReversedUDAF::Reversed` pair
    with a different name; `array_agg`, `string_agg` and `nth_value` reverse
    to themselves, so the rewrite is a no-op for them.
    - Two rules reach `get_best_fitting_window` — `enforce_sorting` and
    `enforce_distribution` — so the fix is applied at the `get_reverse_expr`
    level to cover both.
    - `OptimizeAggregateOrder` also calls `reverse_expr`, but
    `AggregateExec::with_new_aggr_exprs` keeps the original schema, so it
    cannot rename an output field.
    - `OptimizationInvariantChecker` does 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_expr` is refactored into
    `reverse_expr_inner(preserve_name)`, with a new public
    `reverse_expr_preserving_name()`. Existing `reverse_expr` behavior is
    unchanged.
    - `PlainAggregateWindowExpr::get_reverse_expr` and
    `SlidingAggregateWindowExpr::get_reverse_expr` use the name-preserving
    variant. Their bodies were byte-identical, so they are factored into a
    shared `reverse_aggregate_window_expr` helper.
    - `get_best_fitting_window` now asserts that reversal did not change any
    output field name, so a future renaming `WindowExpr` implementation
    fails there — naming the culprit — instead of at a distant
    `ProjectionMapping` assertion.
    
    Note that the prefix-based `replace_fn_name_clause` also mangled
    user-supplied aliases beginning with `last_value`/`first_value` (e.g. an
    alias `last_value_desc` became `first_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_names` reproduces the issue
    through the DataFrame API and reproduces the reported error exactly. It
    asserts on `create_physical_plan()` rather than `collect()`, because
    execution then hits the separate missing-`retract_batch` gap tracked by
    #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_names` builds two
    opposite-`ORDER BY` aggregate-UDAF windows under a projection, runs
    `EnsureRequirements`, and asserts the optimized plan's schema field
    names are unchanged. Without the fix it fails with `Input field name
    first_value_desc does not match with the projection expression
    last_value_desc`.
    
    The existing `test_reverse_expr_preserves_non_aliased_display_path` and
    the two neighboring `reverse_expr` display 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
    warnings` clean.
    
    ## 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_name` is a new public
    method — additive, no breaking API changes.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 datafusion/core/tests/dataframe/mod.rs             |  63 ++++++++++++
 .../tests/physical_optimizer/window_optimize.rs    | 110 ++++++++++++++++++++-
 datafusion/physical-expr/src/aggregate.rs          |  30 +++++-
 datafusion/physical-expr/src/window/aggregate.rs   |  82 +++++++++------
 .../physical-expr/src/window/sliding_aggregate.rs  |  40 ++------
 datafusion/physical-plan/src/windows/mod.rs        |  16 ++-
 6 files changed, 272 insertions(+), 69 deletions(-)

diff --git a/datafusion/core/tests/dataframe/mod.rs 
b/datafusion/core/tests/dataframe/mod.rs
index 4e32ea3116..9b0ca1b276 100644
--- a/datafusion/core/tests/dataframe/mod.rs
+++ b/datafusion/core/tests/dataframe/mod.rs
@@ -1336,6 +1336,69 @@ async fn window_aggregates_with_filter() -> Result<()> {
     Ok(())
 }
 
+// Test issue: https://github.com/apache/datafusion/issues/24884
+//
+// When the physical optimizer reverses a window expression to avoid an extra
+// sort, the reversed expression must keep its output field name. Otherwise the
+// window exec's schema changes while the parent projection still references 
the
+// old column name, and planning fails.
+//
+// Note this only asserts on planning: executing the plan currently hits a
+// separate gap (missing `retract_batch` on the reversed sliding frame), 
tracked
+// by https://github.com/apache/datafusion/issues/24885. Once that is fixed, 
this
+// test can be extended to collect results.
+#[tokio::test]
+async fn window_reversal_preserves_output_field_names() -> Result<()> {
+    fn last_value_over(ascending: bool) -> Expr {
+        Expr::from(WindowFunction::new(
+            datafusion_functions_aggregate::first_last::last_value_udaf(),
+            vec![col("v")],
+        ))
+        .order_by(vec![col("t").sort(ascending, false)])
+        .build()
+        .unwrap()
+    }
+
+    // `t` must be non-nullable for the ordering equivalence that makes the
+    // optimizer reverse the second window instead of adding a second sort.
+    let schema = Arc::new(Schema::new(vec![
+        Field::new("t", DataType::Int64, false),
+        Field::new("v", DataType::Int64, true),
+    ]));
+    let batch = RecordBatch::try_new(
+        Arc::clone(&schema),
+        vec![
+            Arc::new(Int64Array::from(vec![1, 2])),
+            Arc::new(Int64Array::from(vec![None, Some(10)])),
+        ],
+    )?;
+
+    let ctx = SessionContext::new();
+    let df = ctx
+        .read_batch(batch)?
+        .with_column("asc_win", last_value_over(true))?
+        .with_column("desc_win", last_value_over(false))?;
+
+    let logical_schema = df.schema().clone();
+    // Planning used to fail here with an internal error from 
`EnsureRequirements`.
+    let physical_plan = df.create_physical_plan().await?;
+
+    let physical_names = physical_plan
+        .schema()
+        .fields()
+        .iter()
+        .map(|f| f.name().clone())
+        .collect::<Vec<_>>();
+    let logical_names = logical_schema
+        .fields()
+        .iter()
+        .map(|f| f.name().clone())
+        .collect::<Vec<_>>();
+    assert_eq!(physical_names, logical_names);
+
+    Ok(())
+}
+
 // Test issue: https://github.com/apache/datafusion/issues/10346
 #[tokio::test]
 async fn test_select_over_aggregate_schema() -> Result<()> {
diff --git a/datafusion/core/tests/physical_optimizer/window_optimize.rs 
b/datafusion/core/tests/physical_optimizer/window_optimize.rs
index 796f6b6259..909cb8b323 100644
--- a/datafusion/core/tests/physical_optimizer/window_optimize.rs
+++ b/datafusion/core/tests/physical_optimizer/window_optimize.rs
@@ -18,17 +18,23 @@
 #[cfg(test)]
 mod test {
     use arrow::array::{Int32Array, RecordBatch};
-    use arrow_schema::{DataType, Field, Schema};
+    use arrow_schema::{DataType, Field, Schema, SortOptions};
     use datafusion_common::Result;
+    use datafusion_common::config::ConfigOptions;
     use datafusion_datasource::memory::MemorySourceConfig;
     use datafusion_datasource::source::DataSourceExec;
     use datafusion_execution::TaskContext;
-    use datafusion_expr::WindowFrame;
+    use datafusion_expr::{WindowFrame, WindowFunctionDefinition};
     use datafusion_functions_aggregate::count::count_udaf;
+    use datafusion_functions_aggregate::first_last::last_value_udaf;
     use datafusion_physical_expr::aggregate::AggregateExprBuilder;
     use datafusion_physical_expr::expressions::{Column, col};
-    use datafusion_physical_expr::window::PlainAggregateWindowExpr;
-    use datafusion_physical_plan::windows::BoundedWindowAggExec;
+    use datafusion_physical_expr::window::{PlainAggregateWindowExpr, 
WindowExpr};
+    use datafusion_physical_expr::{LexOrdering, PhysicalExpr, 
PhysicalSortExpr};
+    use datafusion_physical_optimizer::PhysicalOptimizerRule;
+    use datafusion_physical_optimizer::ensure_requirements::EnsureRequirements;
+    use datafusion_physical_plan::projection::{ProjectionExec, ProjectionExpr};
+    use datafusion_physical_plan::windows::{BoundedWindowAggExec, 
create_window_expr};
     use datafusion_physical_plan::{ExecutionPlan, InputOrderMode, common};
     use std::sync::Arc;
 
@@ -64,6 +70,102 @@ mod test {
         Ok(())
     }
 
+    /// Test case for <https://github.com/apache/datafusion/issues/24884>
+    ///
+    /// `EnsureRequirements` reverses the second window expression to reuse the
+    /// `t ASC` ordering required by the first one. That reversal must not 
rename
+    /// the window's output field, otherwise the parent projection -- which
+    /// references those columns by name -- can no longer be resolved.
+    #[tokio::test]
+    async fn test_window_reversal_preserves_output_field_names() -> Result<()> 
{
+        // `t` must be non-nullable for the ordering equivalence that triggers
+        // the reversal.
+        let schema = Arc::new(Schema::new(vec![
+            Field::new("t", DataType::Int32, false),
+            Field::new("v", DataType::Int32, true),
+        ]));
+        let batch = RecordBatch::try_new(
+            Arc::clone(&schema),
+            vec![
+                Arc::new(Int32Array::from(vec![1, 2])),
+                Arc::new(Int32Array::from(vec![None, Some(10)])),
+            ],
+        )?;
+        let ordering =
+            LexOrdering::new([PhysicalSortExpr::new_default(col("t", 
&schema)?)])
+                .unwrap();
+        let source: Arc<dyn ExecutionPlan> = 
Arc::new(DataSourceExec::new(Arc::new(
+            MemorySourceConfig::try_new(&[vec![batch]], Arc::clone(&schema), 
None)?
+                .try_with_sort_information(vec![ordering])?,
+        )));
+
+        // Two `last_value` aggregate-UDAF windows with opposite ORDER BY
+        // directions, so the optimizer reverses the second one.
+        let window_expr = |ascending: bool| -> Result<Arc<dyn WindowExpr>> {
+            let sort_expr = PhysicalSortExpr::new(
+                col("t", &schema)?,
+                SortOptions::new(!ascending, false),
+            );
+            create_window_expr(
+                &WindowFunctionDefinition::AggregateUDF(last_value_udaf()),
+                format!("last_value_{}", if ascending { "asc" } else { "desc" 
}),
+                &[col("v", &schema)?],
+                &[],
+                std::slice::from_ref(&sort_expr),
+                Arc::new(WindowFrame::new(Some(false))),
+                Arc::clone(&schema),
+                false,
+                false,
+                None,
+            )
+        };
+
+        let mut plan: Arc<dyn ExecutionPlan> = source;
+        for ascending in [true, false] {
+            plan = Arc::new(BoundedWindowAggExec::try_new(
+                vec![window_expr(ascending)?],
+                plan,
+                InputOrderMode::Sorted,
+                false,
+            )?);
+        }
+
+        // A projection that references the window outputs by name, as the
+        // physical planner would produce.
+        let plan_schema = plan.schema();
+        let projection_exprs = plan_schema
+            .fields()
+            .iter()
+            .enumerate()
+            .map(|(idx, field)| ProjectionExpr {
+                expr: Arc::new(Column::new(field.name(), idx)) as Arc<dyn 
PhysicalExpr>,
+                alias: field.name().clone(),
+            })
+            .collect::<Vec<_>>();
+        let plan: Arc<dyn ExecutionPlan> =
+            Arc::new(ProjectionExec::try_new(projection_exprs, plan)?);
+
+        // Used to fail with an internal error from 
`ProjectionMapping::try_new`.
+        let optimized = EnsureRequirements::new()
+            .optimize(Arc::clone(&plan), &ConfigOptions::new())?;
+
+        let names = |plan: &Arc<dyn ExecutionPlan>| {
+            plan.schema()
+                .fields()
+                .iter()
+                .map(|f| f.name().clone())
+                .collect::<Vec<_>>()
+        };
+        assert_eq!(names(&optimized), names(&plan));
+
+        // The window exec below the projection must keep its field names too,
+        // otherwise the projection's columns would dangle.
+        let window = optimized.children()[0];
+        assert_eq!(names(window), names(&plan));
+
+        Ok(())
+    }
+
     pub fn mock_data() -> Result<Arc<DataSourceExec>> {
         let schema = Arc::new(Schema::new(vec![
             Field::new("a", DataType::Int32, true),
diff --git a/datafusion/physical-expr/src/aggregate.rs 
b/datafusion/physical-expr/src/aggregate.rs
index 5f045ca8c7..df22bc69d8 100644
--- a/datafusion/physical-expr/src/aggregate.rs
+++ b/datafusion/physical-expr/src/aggregate.rs
@@ -933,24 +933,44 @@ impl AggregateFunctionExpr {
     /// For aggregates that do not support calculation in reverse,
     /// returns None (which is the default value).
     pub fn reverse_expr(&self) -> Option<AggregateFunctionExpr> {
+        self.reverse_expr_inner(false)
+    }
+
+    /// Same as [`Self::reverse_expr`], but the output `name` is always carried
+    /// over unchanged.
+    ///
+    /// Window execs derive their output schema from `WindowExpr::field()`, 
which
+    /// for aggregate-backed window expressions is this expression's `name`. If
+    /// reversal renamed it, the rebuilt window exec would expose a differently
+    /// named column while parent plan nodes still reference the old one. This
+    /// mirrors `WindowUDFExpr::reverse_expr`, which preserves its name for the
+    /// same reason. `AggregateExec`, by contrast, pins its schema at
+    /// construction, so it can use [`Self::reverse_expr`] and let the name
+    /// reflect the function actually being evaluated.
+    pub(crate) fn reverse_expr_preserving_name(&self) -> 
Option<AggregateFunctionExpr> {
+        self.reverse_expr_inner(true)
+    }
+
+    fn reverse_expr_inner(&self, preserve_name: bool) -> 
Option<AggregateFunctionExpr> {
         match self.fun.reverse_udf() {
             ReversedUDAF::NotSupported => None,
             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();
                 let mut name = self.name().to_string();
                 let mut human_display = self.human_display.clone();
                 // Reversing display follows two paths:
-                // - aliased display keeps the output `name` unchanged and 
rewrites only
-                //   the lowered expression in `human_display`.
+                // - aliased display (or an explicit request to keep the name) 
keeps
+                //   the output `name` unchanged and rewrites only the lowered
+                //   expression in `human_display`.
                 // - non-aliased display rewrites the canonical `name`, and 
rewrites
                 //   `human_display` only when present.
                 // If the function is changed, we need to reverse order_by 
clause as well
                 // i.e. First(a order by b asc null first) -> Last(a order by 
b desc null last)
-                if !was_aliased && self.fun().name() != reverse_udf.name() {
+                if !keep_name && self.fun().name() != reverse_udf.name() {
                     replace_order_by_clause(&mut name);
                 }
-                if !was_aliased {
+                if !keep_name {
                     replace_fn_name_clause(
                         &mut name,
                         self.fun.name(),
diff --git a/datafusion/physical-expr/src/window/aggregate.rs 
b/datafusion/physical-expr/src/window/aggregate.rs
index 7cfdcb167f..a021588edd 100644
--- a/datafusion/physical-expr/src/window/aggregate.rs
+++ b/datafusion/physical-expr/src/window/aggregate.rs
@@ -39,6 +39,53 @@ use datafusion_common::{Result, ScalarValue, 
exec_datafusion_err};
 use datafusion_expr::{Accumulator, WindowFrame, WindowFrameBound, 
WindowFrameUnits};
 use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
 
+/// Reverses an aggregate-backed window expression.
+///
+/// Shared by [`PlainAggregateWindowExpr::get_reverse_expr`] and
+/// [`SlidingAggregateWindowExpr::get_reverse_expr`], which are otherwise
+/// identical. The reversed frame decides which of the two variants is 
produced:
+/// an ever-expanding frame can be evaluated by the plain (accumulating) 
variant,
+/// anything else needs the sliding variant.
+///
+/// The aggregate is reversed with
+/// [`AggregateFunctionExpr::reverse_expr_preserving_name`] so that the 
window's
+/// output field name is unchanged -- the window exec's schema is derived from
+/// [`WindowExpr::field`], and parent plan nodes reference those columns by 
name.
+pub(crate) fn reverse_aggregate_window_expr(
+    aggregate: &AggregateFunctionExpr,
+    partition_by: &[Arc<dyn PhysicalExpr>],
+    order_by: &[PhysicalSortExpr],
+    window_frame: &WindowFrame,
+    filter: Option<&Arc<dyn PhysicalExpr>>,
+) -> Option<Arc<dyn WindowExpr>> {
+    aggregate
+        .reverse_expr_preserving_name()
+        .map(|reverse_expr| {
+            let reverse_expr = Arc::new(reverse_expr);
+            let reverse_order_by =
+                order_by.iter().map(|e| e.reverse()).collect::<Vec<_>>();
+            let reverse_window_frame = Arc::new(window_frame.reverse());
+            let filter = filter.cloned();
+            if reverse_window_frame.is_ever_expanding() {
+                Arc::new(PlainAggregateWindowExpr::new(
+                    reverse_expr,
+                    partition_by,
+                    &reverse_order_by,
+                    reverse_window_frame,
+                    filter,
+                )) as _
+            } else {
+                Arc::new(SlidingAggregateWindowExpr::new(
+                    reverse_expr,
+                    partition_by,
+                    &reverse_order_by,
+                    reverse_window_frame,
+                    filter,
+                )) as _
+            }
+        })
+}
+
 /// A window expr that takes the form of an aggregate function.
 ///
 /// See comments on [`WindowExpr`] for more details.
@@ -185,34 +232,13 @@ impl WindowExpr for PlainAggregateWindowExpr {
     }
 
     fn get_reverse_expr(&self) -> Option<Arc<dyn WindowExpr>> {
-        self.aggregate.reverse_expr().map(|reverse_expr| {
-            let reverse_window_frame = self.window_frame.reverse();
-            if reverse_window_frame.is_ever_expanding() {
-                Arc::new(PlainAggregateWindowExpr::new(
-                    Arc::new(reverse_expr),
-                    &self.partition_by.clone(),
-                    &self
-                        .order_by
-                        .iter()
-                        .map(|e| e.reverse())
-                        .collect::<Vec<_>>(),
-                    Arc::new(self.window_frame.reverse()),
-                    self.filter.clone(),
-                )) as _
-            } else {
-                Arc::new(SlidingAggregateWindowExpr::new(
-                    Arc::new(reverse_expr),
-                    &self.partition_by.clone(),
-                    &self
-                        .order_by
-                        .iter()
-                        .map(|e| e.reverse())
-                        .collect::<Vec<_>>(),
-                    Arc::new(self.window_frame.reverse()),
-                    self.filter.clone(),
-                )) as _
-            }
-        })
+        reverse_aggregate_window_expr(
+            &self.aggregate,
+            &self.partition_by,
+            &self.order_by,
+            &self.window_frame,
+            self.filter.as_ref(),
+        )
     }
 
     fn uses_bounded_memory(&self) -> bool {
diff --git a/datafusion/physical-expr/src/window/sliding_aggregate.rs 
b/datafusion/physical-expr/src/window/sliding_aggregate.rs
index a39334f057..0954037acb 100644
--- a/datafusion/physical-expr/src/window/sliding_aggregate.rs
+++ b/datafusion/physical-expr/src/window/sliding_aggregate.rs
@@ -22,12 +22,11 @@ use std::ops::Range;
 use std::sync::Arc;
 
 use crate::aggregate::AggregateFunctionExpr;
+use crate::window::aggregate::reverse_aggregate_window_expr;
 use crate::window::window_expr::{
     AggregateWindowExpr, WindowEvalContext, WindowFn, filter_array,
 };
-use crate::window::{
-    PartitionBatches, PartitionWindowAggStates, PlainAggregateWindowExpr, 
WindowExpr,
-};
+use crate::window::{PartitionBatches, PartitionWindowAggStates, WindowExpr};
 use crate::{PhysicalExpr, expressions::PhysicalSortExpr};
 
 use arrow::array::{ArrayRef, BooleanArray};
@@ -122,34 +121,13 @@ impl WindowExpr for SlidingAggregateWindowExpr {
     }
 
     fn get_reverse_expr(&self) -> Option<Arc<dyn WindowExpr>> {
-        self.aggregate.reverse_expr().map(|reverse_expr| {
-            let reverse_window_frame = self.window_frame.reverse();
-            if reverse_window_frame.is_ever_expanding() {
-                Arc::new(PlainAggregateWindowExpr::new(
-                    Arc::new(reverse_expr),
-                    &self.partition_by.clone(),
-                    &self
-                        .order_by
-                        .iter()
-                        .map(|e| e.reverse())
-                        .collect::<Vec<_>>(),
-                    Arc::new(self.window_frame.reverse()),
-                    self.filter.clone(),
-                )) as _
-            } else {
-                Arc::new(SlidingAggregateWindowExpr::new(
-                    Arc::new(reverse_expr),
-                    &self.partition_by.clone(),
-                    &self
-                        .order_by
-                        .iter()
-                        .map(|e| e.reverse())
-                        .collect::<Vec<_>>(),
-                    Arc::new(self.window_frame.reverse()),
-                    self.filter.clone(),
-                )) as _
-            }
-        })
+        reverse_aggregate_window_expr(
+            &self.aggregate,
+            &self.partition_by,
+            &self.order_by,
+            &self.window_frame,
+            self.filter.as_ref(),
+        )
     }
 
     fn uses_bounded_memory(&self) -> bool {
diff --git a/datafusion/physical-plan/src/windows/mod.rs 
b/datafusion/physical-plan/src/windows/mod.rs
index 3f33dfedfd..7c5f55f661 100644
--- a/datafusion/physical-plan/src/windows/mod.rs
+++ b/datafusion/physical-plan/src/windows/mod.rs
@@ -33,7 +33,7 @@ use crate::{
 
 use arrow::datatypes::{Schema, SchemaRef};
 use arrow_schema::{FieldRef, SortOptions};
-use datafusion_common::{Result, exec_err};
+use datafusion_common::{Result, assert_or_internal_err, exec_err};
 use datafusion_expr::{
     LimitEffect, PartitionEvaluator, ReversedUDWF, SetMonotonicity, 
WindowFrame,
     WindowFunctionDefinition, WindowUDF,
@@ -623,6 +623,20 @@ pub fn get_best_fitting_window(
             .map(|e| e.get_reverse_expr())
             .collect::<Option<Vec<_>>>()
         {
+            // The rebuilt exec derives its schema from `WindowExpr::field()`, 
so a
+            // reversal that renames the output field would silently change 
this
+            // node's schema while parent nodes still reference the old column
+            // names. Catch that here, where the culprit is identifiable.
+            for (reversed, original) in 
reversed_window_expr.iter().zip(window_exprs) {
+                let (reversed_field, original_field) =
+                    (reversed.field()?, original.field()?);
+                assert_or_internal_err!(
+                    reversed_field.name() == original_field.name(),
+                    "Reversing window expression changed its output field name 
from {} to {}",
+                    original_field.name(),
+                    reversed_field.name()
+                );
+            }
             reversed_window_expr
         } else {
             // Cannot take reverse of any of the window expr


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to