timsaucer opened a new issue, #24884:
URL: https://github.com/apache/datafusion/issues/24884

   ### Describe the bug
   
   When the physical optimizer reverses a window expression to avoid an extra 
sort, and that window expression is backed by an **aggregate UDAF** (rather 
than a window UDF), the reversed expression's *output field name* is rewritten. 
The window exec's schema changes, but parent plan nodes still reference the old 
column name, so planning fails with an internal error:
   
   ```
   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.
   ```
   
   Mechanism:
   
   1. `EnsureRequirements` sorts the input `t ASC` for the first window. For 
the second (`ORDER BY t DESC`) window, `get_window_mode` reports 
`should_reverse = true`, so `get_best_fitting_window` swaps in 
`e.get_reverse_expr()` to avoid a second sort 
(`datafusion/physical-plan/src/windows/mod.rs`, `get_best_fitting_window`).
   2. For an aggregate-backed window expr this reaches 
`AggregateFunctionExpr::reverse_expr` 
(`datafusion/physical-expr/src/aggregate.rs`). Because `human_display_alias` is 
`None` (`was_aliased == false`), it rewrites the aggregate's output name via 
`replace_order_by_clause` / `replace_fn_name_clause`:
      `last_value(v) ORDER BY [t DESC NULLS LAST] ...` → `first_value(v) ORDER 
BY [t ASC NULLS FIRST] ...`
   3. `adjust_window_sort_removal` 
(`datafusion/physical-optimizer/src/ensure_requirements/enforce_sorting/mod.rs`)
 installs the new window exec. Its schema field is now renamed, but the parent 
`ProjectionExec` still holds `Column("last_value(...) ORDER BY [... DESC ...]", 
3)`.
   4. `ProjectionMapping::try_new` 
(`datafusion/physical-expr/src/projection.rs`) asserts that the projection's 
column name matches the input schema field name, and fails.
   
   Note the contrast with the window-UDF path: `WindowUDFExpr::reverse_expr` 
(`datafusion/physical-plan/src/windows/mod.rs`) explicitly carries over `name: 
self.name.clone()`, so reversal there preserves the output field name and 
nothing breaks. Renaming on reversal only makes sense for `AggregateExec`, 
where no parent node references the aggregate by its schema name.
   
   This is why the equivalent SQL query does **not** fail: in SQL, 
`last_value(v) OVER (...)` resolves to the `last_value` *window UDF* 
(`datafusion/functions-window/src/nth_value.rs`), which reverses without 
renaming. The failure shows up when the aggregate UDAF (`last_value_udaf()`) is 
used as a window function, which is reachable through the DataFrame API.
   
   ### To Reproduce
   
   Add as `datafusion/core/tests/df_window_reverse_mre.rs` and run `cargo test 
-p datafusion --test df_window_reverse_mre`:
   
   ```rust
   use std::sync::Arc;
   
   use arrow::array::{Int64Array, RecordBatch};
   use arrow::datatypes::{DataType, Field, Schema};
   use datafusion::prelude::*;
   
   fn last_value_over(ascending: bool) -> Expr {
       use datafusion::logical_expr::{ExprFunctionExt as _, 
expr::WindowFunction};
       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()
   }
   
   #[tokio::test]
   async fn window_reverse_rename_breaks_parent_projection() {
       let schema = Arc::new(Schema::new(vec![
           Field::new("t", DataType::Int64, false), // NOT NULL is required
           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)])),
           ],
       )
       .unwrap();
   
       let ctx = SessionContext::new();
       let df = ctx
           .read_batch(batch)
           .unwrap()
           .with_column("asc_win", last_value_over(true))
           .unwrap()
           .with_column("desc_win", last_value_over(false))
           .unwrap();
   
       match df.collect().await {
           Ok(batches) => println!("OK: {} batches", batches.len()),
           Err(err) => panic!("FAILED: {err}"),
       }
   }
   ```
   
   Two window expressions with opposite `ORDER BY` directions are needed so 
that the optimizer reverses the second one; `t` must be non-nullable for the 
ordering equivalence to trigger the reversal.
   
   ### Expected behavior
   
   Reversing a window expression should not change the window's output field 
name; the plan schema must stay stable so parent projections keep resolving. 
Concretely, the aggregate-backed window reversal 
(`Plain`/`SlidingAggregateWindowExpr::get_reverse_expr`) should preserve the 
original output name, matching what `WindowUDFExpr::reverse_expr` already does.
   
   ### Additional context
   
   Reproduced on `main` (`d7b8e4fc1`) and originally observed on 54.1.0.
   
   Scope note: this issue is only about the renaming/plan-schema breakage. 
There is a separate, independent gap behind it — with the name preserved 
locally, the same query then fails at execution with:
   
   ```
   NotImplemented("Aggregate can not be used as a sliding accumulator because 
`retract_batch` is not implemented: last_value(...)")
   ```
   
   because the reversed frame `ROWS CURRENT ROW .. UNBOUNDED FOLLOWING` is not 
`is_ever_expanding()`, so `get_reverse_expr` builds a 
`SlidingAggregateWindowExpr` that requires `retract_batch`. That is a distinct 
problem (missing `retract_batch` / frame classification on reversal) and is not 
what this issue asks to fix.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to