6868//! the *pre-merged* [`Accumulator::state`] for each aggregate window
6969//! expression (indexed by position in `BoundedWindowAggExec::window_expr()`).
7070//! Only consumed by [`WindowApply::Aggregate`] entries;
71- //! [`WindowApply::Scalar`] carries its own offsets inline. The DF-side getter
72- //! already commits to at-most-one PARTITION BY group per DF partition (see
73- //! `apache/datafusion#24007`), so no per-key dimension is exposed here. The
74- //! scheduler bakes the state when it constructs the downstream stage after
75- //! the upstream stage's tasks complete.
71+ //! [`WindowApply::Scalar`] carries its own offsets inline. No PARTITION BY
72+ //! dimension is exposed: DataFusion publishes state per closed group, but the
73+ //! rewrite that plants this operator only fires on windows without a
74+ //! PARTITION BY, and the scheduler rejects any report carrying a key rather
75+ //! than flattening two groups together. A window that does have a PARTITION
76+ //! BY needs no help from this operator, since `BoundedWindowAggExec` asks for
77+ //! `KeyPartitioned` input and each partition's window is already independent.
78+ //! The scheduler bakes the state when it constructs the downstream stage
79+ //! after the upstream stage's tasks complete.
7680//!
7781//! **Status.** Both apply paths are implemented.
7882//!
@@ -148,12 +152,48 @@ use crate::execution_plans::plan_algebra::{
148152/// value per window expression — see the `prefix_merge` module docs for the
149153/// division of labor. This operator applies it; it does not compute it.
150154///
151- /// The type mirrors `datafusion::physical_plan::windows::FinalizedPartitionState`
152- /// from [apache/datafusion#24007]; the alias here is a local stand-in so
153- /// this crate compiles against stable DataFusion 54 until that PR lands.
154- ///
155- /// [apache/datafusion#24007]: https://github.com/apache/datafusion/pull/24007
156- pub type FinalizedPartitionState = Vec < Option < Vec < ScalarValue > > > ;
155+ /// A newtype rather than an alias for `Vec<Option<Vec<ScalarValue>>>`. That
156+ /// type appears in this operator's public signatures, where it is neither
157+ /// readable nor searchable, and it spells "no state for this window
158+ /// expression" two ways — a missing index and a `None` — which every caller
159+ /// then has to handle. [`Self::slot`] collapses both into one answer. Any
160+ /// later change to the representation also stays internal rather than
161+ /// breaking whoever wrote the concrete type.
162+ #[ derive( Debug , Clone , Default , PartialEq ) ]
163+ pub struct FinalizedPartitionState {
164+ /// Indexed by position in the upstream operator's `window_expr()` list.
165+ per_window_expr : Vec < Option < Vec < ScalarValue > > > ,
166+ }
167+
168+ impl FinalizedPartitionState {
169+ /// Build from one slot per window expression, in `window_expr()` order.
170+ pub fn new ( per_window_expr : Vec < Option < Vec < ScalarValue > > > ) -> Self {
171+ Self { per_window_expr }
172+ }
173+
174+ /// Merged state for `window_expr_index`, or `None` when that expression
175+ /// published none or the index is past the end.
176+ pub fn slot ( & self , window_expr_index : usize ) -> Option < & Vec < ScalarValue > > {
177+ self . per_window_expr
178+ . get ( window_expr_index)
179+ . and_then ( |slot| slot. as_ref ( ) )
180+ }
181+
182+ /// Every slot in window-expression order, for wire encoding.
183+ pub fn slots ( & self ) -> & [ Option < Vec < ScalarValue > > ] {
184+ & self . per_window_expr
185+ }
186+
187+ /// Number of window expressions this state covers.
188+ pub fn len ( & self ) -> usize {
189+ self . per_window_expr . len ( )
190+ }
191+
192+ /// True when no window expression published state.
193+ pub fn is_empty ( & self ) -> bool {
194+ self . per_window_expr . is_empty ( )
195+ }
196+ }
157197
158198/// How to combine each row's existing value in an output column with a
159199/// scheduler-provided scalar offset. The result overwrites the column.
@@ -624,10 +664,10 @@ impl ExecutionPlan for PrefixMergeExec {
624664 let input_schema = self . input . schema ( ) ;
625665 let output_schema = self . schema ( ) ;
626666
627- // The DF-side getter (BoundedWindowAggExec::finalized_partition_state)
628- // already commits to at-most-one PARTITION BY group per DF partition,
629- // so this operator receives the state indexed only by window
630- // expression — no PARTITION BY dimension to project away.
667+ // Indexed by window expression only. The scheduler rejects any
668+ // report carrying a PARTITION BY key, so at most one group per
669+ // partition reaches here and there is no key dimension to project
670+ // away.
631671 let key_state = & resolved[ partition] ;
632672
633673 let mut appliers: Vec < PreparedApply > = Vec :: with_capacity ( self . applies . len ( ) ) ;
@@ -639,9 +679,7 @@ impl ExecutionPlan for PrefixMergeExec {
639679 output_column,
640680 window_expr_index,
641681 } => {
642- let offset_state: Option < & Vec < ScalarValue > > = key_state
643- . get ( * window_expr_index)
644- . and_then ( |slot| slot. as_ref ( ) ) ;
682+ let offset_state = key_state. slot ( * window_expr_index) ;
645683 appliers. push ( PreparedApply :: Aggregate ( AggregateApply :: new (
646684 i,
647685 udf,
@@ -1042,16 +1080,22 @@ mod tests {
10421080 }
10431081
10441082 fn empty_state ( partitions : usize ) -> Vec < FinalizedPartitionState > {
1045- ( 0 ..partitions) . map ( |_| Vec :: new ( ) ) . collect ( )
1083+ ( 0 ..partitions)
1084+ . map ( |_| FinalizedPartitionState :: default ( ) )
1085+ . collect ( )
10461086 }
10471087
10481088 /// Mismatched state length surfaces as an error rather than a panic at
10491089 /// runtime.
10501090 #[ test]
10511091 fn try_new_rejects_state_length_mismatch ( ) {
10521092 let input = partitioned_source ( 2 , 3 ) ;
1053- let err = PrefixMergeExec :: try_new_resolved ( input, vec ! [ ] , vec ! [ Vec :: new( ) ] )
1054- . expect_err ( "length mismatch must surface as an error" ) ;
1093+ let err = PrefixMergeExec :: try_new_resolved (
1094+ input,
1095+ vec ! [ ] ,
1096+ vec ! [ FinalizedPartitionState :: default ( ) ] ,
1097+ )
1098+ . expect_err ( "length mismatch must surface as an error" ) ;
10551099 assert ! (
10561100 err. to_string( )
10571101 . contains( "does not match input partition count" ) ,
@@ -1176,8 +1220,10 @@ mod tests {
11761220 // Partition 1 gets partition 0's HLL state at the one aggregate slot
11771221 // (window_expr_index 0). Partition 0 gets an empty state — nothing
11781222 // to merge in.
1179- let per_partition_state: Vec < FinalizedPartitionState > =
1180- vec ! [ vec![ ] , vec![ Some ( p0_state) ] ] ;
1223+ let per_partition_state: Vec < FinalizedPartitionState > = vec ! [
1224+ FinalizedPartitionState :: default ( ) ,
1225+ FinalizedPartitionState :: new( vec![ Some ( p0_state) ] ) ,
1226+ ] ;
11811227
11821228 let apply = WindowApply :: Aggregate {
11831229 udf : Arc :: clone ( & udf) ,
@@ -1300,8 +1346,10 @@ mod tests {
13001346 } ;
13011347 // Partition 1 gets partition 0's (sum, count) state at the one
13021348 // aggregate slot; partition 0 has nothing to merge in.
1303- let per_partition_state: Vec < FinalizedPartitionState > =
1304- vec ! [ vec![ ] , vec![ Some ( p0_state) ] ] ;
1349+ let per_partition_state: Vec < FinalizedPartitionState > = vec ! [
1350+ FinalizedPartitionState :: default ( ) ,
1351+ FinalizedPartitionState :: new( vec![ Some ( p0_state) ] ) ,
1352+ ] ;
13051353
13061354 let apply = WindowApply :: Aggregate {
13071355 udf : Arc :: clone ( & udf) ,
0 commit comments