Skip to main content

datafusion_physical_plan/windows/
bounded_window_agg_exec.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Stream and channel implementations for window function expressions.
19//! The executor given here uses bounded memory (does not maintain all
20//! the input data seen so far), which makes it appropriate when processing
21//! infinite inputs.
22
23use std::cmp::{Ordering, min};
24use std::collections::VecDeque;
25use std::pin::Pin;
26use std::sync::Arc;
27use std::task::{Context, Poll};
28
29use super::utils::create_schema;
30use crate::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
31use crate::statistics::{ChildStats, StatisticsArgs};
32use crate::stream::EmptyRecordBatchStream;
33use crate::windows::{
34    calc_requirements, get_ordered_partition_by_indices, get_partition_by_sort_exprs,
35    window_equivalence_properties,
36};
37use crate::{
38    ChildrenPropertiesMode, ColumnStatistics, DisplayAs, DisplayFormatType, Distribution,
39    ExecutionPlan, ExecutionPlanProperties, InputDistributionRequirements,
40    InputOrderMode, PlanProperties, RecordBatchStream, ReplaceChildrenOptions,
41    SendableRecordBatchStream, Statistics, WindowExpr, validate_child_count,
42};
43
44use arrow::compute::take_record_batch;
45use arrow::{
46    array::{Array, ArrayRef, RecordBatchOptions, UInt32Array, UInt32Builder},
47    compute::{concat, concat_batches, sort_to_indices, take_arrays},
48    datatypes::SchemaRef,
49    record_batch::RecordBatch,
50};
51use datafusion_common::hash_utils::create_hashes;
52use datafusion_common::stats::Precision;
53use datafusion_common::tree_node::TreeNodeRecursion;
54use datafusion_common::utils::{
55    evaluate_partition_ranges, get_at_indices, get_row_at_idx,
56};
57use datafusion_common::{
58    HashMap, Result, ScalarValue, arrow_datafusion_err, exec_datafusion_err, exec_err,
59};
60use datafusion_execution::TaskContext;
61use datafusion_expr::ColumnarValue;
62use datafusion_expr::window_state::{PartitionBatchState, WindowAggState};
63use datafusion_physical_expr::window::{
64    PartitionBatches, PartitionKey, PartitionWindowAggStates, WindowEvalContext,
65    WindowState,
66};
67use datafusion_physical_expr_common::physical_expr::PhysicalExpr;
68use datafusion_physical_expr_common::sort_expr::{
69    OrderingRequirements, PhysicalSortExpr,
70};
71
72use crate::execution_plan::CardinalityEffect;
73use datafusion_common::hash_utils::RandomState;
74use futures::stream::Stream;
75use futures::{StreamExt, ready};
76use hashbrown::hash_table::HashTable;
77use indexmap::IndexMap;
78use log::debug;
79
80/// Callback receiver for per-partition window state.
81///
82/// `state` is the result of [`Accumulator::state`], which is a `&mut self`
83/// call whose trait doc states "this function should not be called twice."
84/// Several built-in aggregates (`median`, `percentile_cont`, `string_agg`,
85/// `min_max_bytes`/`min_max_struct`) `std::mem::take` their internal
86/// buffers to build that state — so `state` is a destructive read, not a
87/// snapshot. The exec fires this at most once per group; a callee that
88/// needs the value beyond the callback must retain it (e.g. clone into
89/// owned storage).
90///
91/// [`Accumulator::state`]: datafusion_expr::Accumulator::state
92pub trait WindowStateObserver: Send + Sync {
93    /// Invoked once per (output-partition-index, window-expression,
94    /// PARTITION BY tuple) as each PARTITION BY group closes, for every
95    /// aggregate window expression on the exec. Non-aggregate window
96    /// functions (e.g. `row_number`, `rank`, `lead`/`lag`) do not fire this
97    /// callback.
98    ///
99    /// # Arguments
100    ///
101    /// * `partition_idx` - Output partition index of the [`BoundedWindowAggExec`]
102    ///   stream firing this callback.
103    /// * `window_expr` - The window expression whose state just closed.
104    /// * `partition_key` - The PARTITION BY tuple that just closed.
105    /// * `state` - [`Accumulator::state`] for the closed group of
106    ///   `window_expr`. See the trait-level doc for the destructive-read
107    ///   contract.
108    ///
109    /// [`Accumulator::state`]: datafusion_expr::Accumulator::state
110    fn finalize_window_aggregate(
111        &self,
112        partition_idx: usize,
113        window_expr: &Arc<dyn WindowExpr>,
114        partition_key: &PartitionKey,
115        state: Vec<ScalarValue>,
116    ) -> Result<()>;
117}
118
119/// Window execution plan
120#[derive(Clone)]
121pub struct BoundedWindowAggExec {
122    /// Input plan
123    input: Arc<dyn ExecutionPlan>,
124    /// Window function expression
125    window_expr: Vec<Arc<dyn WindowExpr>>,
126    /// Schema after the window is run
127    schema: SchemaRef,
128    /// Execution metrics
129    metrics: ExecutionPlanMetricsSet,
130    /// Describes how the input is ordered relative to the partition keys
131    pub input_order_mode: InputOrderMode,
132    /// Partition by indices that define ordering
133    // For example, if input ordering is ORDER BY a, b and window expression
134    // contains PARTITION BY b, a; `ordered_partition_by_indices` would be 1, 0.
135    // Similarly, if window expression contains PARTITION BY a, b; then
136    // `ordered_partition_by_indices` would be 0, 1.
137    // See `get_ordered_partition_by_indices` for more details.
138    ordered_partition_by_indices: Vec<usize>,
139    /// Cache holding plan properties like equivalences, output partitioning etc.
140    cache: Arc<PlanProperties>,
141    /// If `can_rerepartition` is false, partition_keys is always empty.
142    can_repartition: bool,
143    /// Invoked at partition-close to publish finalized per-partition window
144    /// state. Storage and multi-group handling are the caller's; the exec is
145    /// a pure event source.
146    state_observer: Option<Arc<dyn WindowStateObserver>>,
147}
148
149impl std::fmt::Debug for BoundedWindowAggExec {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        f.debug_struct("BoundedWindowAggExec")
152            .field("input", &self.input)
153            .field("window_expr", &self.window_expr)
154            .field("schema", &self.schema)
155            .field("metrics", &self.metrics)
156            .field("input_order_mode", &self.input_order_mode)
157            .field(
158                "ordered_partition_by_indices",
159                &self.ordered_partition_by_indices,
160            )
161            .field("cache", &self.cache)
162            .field("can_repartition", &self.can_repartition)
163            .field(
164                "state_observer",
165                &self.state_observer.as_ref().map(|_| "..."),
166            )
167            .finish()
168    }
169}
170
171impl BoundedWindowAggExec {
172    /// Create a new execution plan for window aggregates
173    pub fn try_new(
174        window_expr: Vec<Arc<dyn WindowExpr>>,
175        input: Arc<dyn ExecutionPlan>,
176        input_order_mode: InputOrderMode,
177        can_repartition: bool,
178    ) -> Result<Self> {
179        let schema = create_schema(&input.schema(), &window_expr)?;
180        let schema = Arc::new(schema);
181        let partition_by_exprs = window_expr[0].partition_by();
182        let ordered_partition_by_indices = match &input_order_mode {
183            InputOrderMode::Sorted => {
184                let indices = get_ordered_partition_by_indices(
185                    window_expr[0].partition_by(),
186                    &input,
187                )?;
188                if indices.len() == partition_by_exprs.len() {
189                    indices
190                } else {
191                    (0..partition_by_exprs.len()).collect::<Vec<_>>()
192                }
193            }
194            InputOrderMode::PartiallySorted(ordered_indices) => ordered_indices.clone(),
195            InputOrderMode::Linear => {
196                vec![]
197            }
198        };
199        let cache = Self::compute_properties(&input, &schema, &window_expr)?;
200        Ok(Self {
201            input,
202            window_expr,
203            schema,
204            metrics: ExecutionPlanMetricsSet::new(),
205            input_order_mode,
206            ordered_partition_by_indices,
207            cache: Arc::new(cache),
208            can_repartition,
209            state_observer: None,
210        })
211    }
212
213    /// Install (or clear) a [`WindowStateObserver`] that receives each
214    /// PARTITION BY group's finalized window state at partition close.
215    ///
216    /// Errors when `observer` is `Some` and any window expression on this
217    /// exec has a non-ever-expanding frame (i.e. its start bound is not
218    /// `UNBOUNDED PRECEDING`). Those frames use `SlidingAggregateWindowExpr`
219    /// under the hood, whose accumulator calls `retract_batch` — at
220    /// partition close the accumulator holds only the last frame's rows,
221    /// not the partition aggregate, so the observed state would silently
222    /// misrepresent the group.
223    pub fn with_state_observer(
224        mut self,
225        observer: Option<Arc<dyn WindowStateObserver>>,
226    ) -> Result<Self> {
227        if observer.is_some() {
228            for expr in &self.window_expr {
229                if !expr.get_window_frame().is_ever_expanding() {
230                    return exec_err!(
231                        "cannot install WindowStateObserver on BoundedWindowAggExec \
232                         with a sliding aggregate window frame (start != \
233                         UNBOUNDED PRECEDING) for `{}`; sliding accumulator state \
234                         is frame-only, not the partition aggregate",
235                        expr.name()
236                    );
237                }
238            }
239        }
240        self.state_observer = observer;
241        Ok(self)
242    }
243
244    /// The currently-installed [`WindowStateObserver`], if any. Optimizer
245    /// rules that rebuild this exec via
246    /// [`crate::windows::get_best_fitting_window`] or a direct `try_new`
247    /// call must read this and reinstall it on the new exec, otherwise a
248    /// caller-installed observer is silently dropped by the rewrite.
249    pub fn state_observer(&self) -> Option<&Arc<dyn WindowStateObserver>> {
250        self.state_observer.as_ref()
251    }
252
253    /// Window expressions
254    pub fn window_expr(&self) -> &[Arc<dyn WindowExpr>] {
255        &self.window_expr
256    }
257
258    /// Input plan
259    pub fn input(&self) -> &Arc<dyn ExecutionPlan> {
260        &self.input
261    }
262
263    /// Return the output sort order of partition keys: For example
264    /// OVER(PARTITION BY a, ORDER BY b) -> would give sorting of the column a
265    // We are sure that partition by columns are always at the beginning of sort_keys
266    // Hence returned `PhysicalSortExpr` corresponding to `PARTITION BY` columns can be used safely
267    // to calculate partition separation points
268    pub fn partition_by_sort_keys(&self) -> Result<Vec<PhysicalSortExpr>> {
269        let partition_by = self.window_expr()[0].partition_by();
270        get_partition_by_sort_exprs(
271            &self.input,
272            partition_by,
273            &self.ordered_partition_by_indices,
274        )
275    }
276
277    /// Initializes the appropriate [`PartitionSearcher`] implementation from
278    /// the state.
279    fn get_search_algo(&self) -> Result<Box<dyn PartitionSearcher>> {
280        let partition_by_sort_keys = self.partition_by_sort_keys()?;
281        let ordered_partition_by_indices = self.ordered_partition_by_indices.clone();
282        let input_schema = self.input().schema();
283        Ok(match &self.input_order_mode {
284            InputOrderMode::Sorted => {
285                // In Sorted mode, all partition by columns should be ordered.
286                if self.window_expr()[0].partition_by().len()
287                    != ordered_partition_by_indices.len()
288                {
289                    return exec_err!(
290                        "All partition by columns should have an ordering in Sorted mode."
291                    );
292                }
293                Box::new(SortedSearch {
294                    partition_by_sort_keys,
295                    ordered_partition_by_indices,
296                    input_schema,
297                })
298            }
299            InputOrderMode::Linear | InputOrderMode::PartiallySorted(_) => Box::new(
300                LinearSearch::new(ordered_partition_by_indices, input_schema),
301            ),
302        })
303    }
304
305    /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc.
306    fn compute_properties(
307        input: &Arc<dyn ExecutionPlan>,
308        schema: &SchemaRef,
309        window_exprs: &[Arc<dyn WindowExpr>],
310    ) -> Result<PlanProperties> {
311        // Calculate equivalence properties:
312        let eq_properties = window_equivalence_properties(schema, input, window_exprs)?;
313
314        // As we can have repartitioning using the partition keys, this can
315        // be either one or more than one, depending on the presence of
316        // repartitioning.
317        let output_partitioning = input.output_partitioning().clone();
318
319        // Construct properties cache
320        Ok(PlanProperties::new(
321            eq_properties,
322            output_partitioning,
323            // TODO: Emission type and boundedness information can be enhanced here
324            input.pipeline_behavior(),
325            input.boundedness(),
326        ))
327    }
328
329    pub fn partition_keys(&self) -> Vec<Arc<dyn PhysicalExpr>> {
330        if !self.can_repartition {
331            vec![]
332        } else {
333            let all_partition_keys = self
334                .window_expr()
335                .iter()
336                .map(|expr| expr.partition_by().to_vec())
337                .collect::<Vec<_>>();
338
339            all_partition_keys
340                .into_iter()
341                .min_by_key(|s| s.len())
342                .unwrap_or_else(Vec::new)
343        }
344    }
345
346    fn statistics_helper(&self, statistics: Statistics) -> Result<Statistics> {
347        let win_cols = self.window_expr.len();
348        let input_cols = self.input.schema().fields().len();
349        // TODO stats: some windowing function will maintain invariants such as min, max...
350        let mut column_statistics = Vec::with_capacity(win_cols + input_cols);
351        // copy stats of the input to the beginning of the schema.
352        column_statistics.extend(statistics.column_statistics);
353        for _ in 0..win_cols {
354            column_statistics.push(ColumnStatistics::new_unknown())
355        }
356        Ok(Statistics {
357            num_rows: statistics.num_rows,
358            column_statistics,
359            total_byte_size: Precision::Absent,
360        })
361    }
362}
363
364impl DisplayAs for BoundedWindowAggExec {
365    fn fmt_as(
366        &self,
367        t: DisplayFormatType,
368        f: &mut std::fmt::Formatter,
369    ) -> std::fmt::Result {
370        match t {
371            DisplayFormatType::Default | DisplayFormatType::Verbose => {
372                write!(f, "BoundedWindowAggExec: ")?;
373                let g: Vec<String> = self
374                    .window_expr
375                    .iter()
376                    .map(|e| {
377                        let field = match e.field() {
378                            Ok(f) => f.to_string(),
379                            Err(e) => format!("{e:?}"),
380                        };
381                        format!(
382                            "{}: {}, frame: {}",
383                            e.name().to_owned(),
384                            field,
385                            e.get_window_frame()
386                        )
387                    })
388                    .collect();
389                let mode = &self.input_order_mode;
390                write!(f, "wdw=[{}], mode=[{:?}]", g.join(", "), mode)?;
391            }
392            DisplayFormatType::TreeRender => {
393                let g: Vec<String> = self
394                    .window_expr
395                    .iter()
396                    .map(|e| e.name().to_owned().to_string())
397                    .collect();
398                writeln!(f, "select_list={}", g.join(", "))?;
399
400                let mode = &self.input_order_mode;
401                writeln!(f, "mode={mode:?}")?;
402            }
403        }
404        Ok(())
405    }
406}
407
408impl ExecutionPlan for BoundedWindowAggExec {
409    fn name(&self) -> &'static str {
410        "BoundedWindowAggExec"
411    }
412
413    /// Return a reference to Any that can be used for downcasting
414    fn properties(&self) -> &Arc<PlanProperties> {
415        &self.cache
416    }
417
418    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
419        vec![&self.input]
420    }
421
422    fn apply_expressions(
423        &self,
424        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
425    ) -> Result<TreeNodeRecursion> {
426        let expressions = self.window_expr.iter().flat_map(|window_expr| {
427            let expressions = window_expr.all_expressions();
428            expressions
429                .args
430                .into_iter()
431                .chain(expressions.partition_by_exprs)
432                .chain(expressions.order_by_exprs)
433        });
434        crate::apply_expression_roots(expressions, f)
435    }
436
437    fn required_input_ordering(&self) -> Vec<Option<OrderingRequirements>> {
438        let partition_bys = self.window_expr()[0].partition_by();
439        let order_keys = self.window_expr()[0].order_by();
440        let partition_bys = self
441            .ordered_partition_by_indices
442            .iter()
443            .map(|idx| &partition_bys[*idx]);
444        vec![calc_requirements(partition_bys, order_keys)]
445    }
446
447    fn required_input_distribution(&self) -> Vec<Distribution> {
448        self.input_distribution_requirements().into_per_child()
449    }
450
451    fn input_distribution_requirements(&self) -> InputDistributionRequirements {
452        if self.partition_keys().is_empty() {
453            debug!("No partition defined for BoundedWindowAggExec!!!");
454            InputDistributionRequirements::new(vec![Distribution::SinglePartition])
455        } else {
456            InputDistributionRequirements::new(vec![Distribution::KeyPartitioned(
457                self.partition_keys(),
458            )])
459        }
460    }
461
462    fn maintains_input_order(&self) -> Vec<bool> {
463        vec![true]
464    }
465
466    fn replace_children(
467        self: Arc<Self>,
468        mut children: Vec<Arc<dyn ExecutionPlan>>,
469        options: ReplaceChildrenOptions,
470    ) -> Result<Arc<dyn ExecutionPlan>> {
471        validate_child_count!(self, children);
472        match options.children_properties {
473            ChildrenPropertiesMode::Keep => Ok(Arc::new(Self {
474                input: children.swap_remove(0),
475                metrics: ExecutionPlanMetricsSet::new(),
476                ..Self::clone(&*self)
477            })),
478            ChildrenPropertiesMode::Recompute => {
479                let new = BoundedWindowAggExec::try_new(
480                    self.window_expr.clone(),
481                    Arc::clone(&children[0]),
482                    self.input_order_mode.clone(),
483                    self.can_repartition,
484                )?
485                .with_state_observer(self.state_observer.clone())?;
486                Ok(Arc::new(new))
487            }
488        }
489    }
490
491    fn with_new_children(
492        self: Arc<Self>,
493        children: Vec<Arc<dyn ExecutionPlan>>,
494    ) -> Result<Arc<dyn ExecutionPlan>> {
495        self.replace_children(
496            children,
497            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
498        )
499    }
500
501    fn with_new_children_and_same_properties(
502        self: Arc<Self>,
503        children: Vec<Arc<dyn ExecutionPlan>>,
504    ) -> Result<Arc<dyn ExecutionPlan>> {
505        self.replace_children(
506            children,
507            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Keep),
508        )
509    }
510
511    fn execute(
512        &self,
513        partition: usize,
514        context: Arc<TaskContext>,
515    ) -> Result<SendableRecordBatchStream> {
516        let input = self.input.execute(partition, context)?;
517        let search_mode = self.get_search_algo()?;
518        let stream = Box::pin(BoundedWindowAggStream::new(
519            Arc::clone(&self.schema),
520            self.window_expr.clone(),
521            input,
522            BaselineMetrics::new(&self.metrics, partition),
523            search_mode,
524            partition,
525            self.state_observer.clone(),
526        )?);
527        Ok(stream)
528    }
529
530    fn metrics(&self) -> Option<MetricsSet> {
531        Some(self.metrics.clone_inner())
532    }
533
534    fn child_stats_requests(&self, partition: Option<usize>) -> Vec<ChildStats> {
535        vec![ChildStats::At(partition)]
536    }
537
538    fn statistics_from_inputs(
539        &self,
540        input_stats: &[Arc<Statistics>],
541        _args: &StatisticsArgs,
542    ) -> Result<Arc<Statistics>> {
543        let input_stat = input_stats[0].as_ref().clone();
544        Ok(Arc::new(self.statistics_helper(input_stat)?))
545    }
546
547    fn cardinality_effect(&self) -> CardinalityEffect {
548        CardinalityEffect::Equal
549    }
550
551    #[cfg(feature = "proto")]
552    fn try_to_proto(
553        &self,
554        ctx: &crate::proto::ExecutionPlanEncodeCtx<'_>,
555    ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
556        use super::proto::encode_physical_window_expr;
557        use datafusion_proto_common::protobuf_common::EmptyMessage;
558        use datafusion_proto_models::protobuf;
559        use protobuf::window_agg_exec_node::InputOrderMode as ProtoInputOrderMode;
560
561        // Exhaustive destructure: adding a field to `BoundedWindowAggExec`
562        // without deciding how it is serialized is a compile error, not a
563        // silent round-trip gap.
564        let Self {
565            input,
566            window_expr,
567            // Derived at construction by `create_schema` from the input schema
568            // and the window expressions.
569            schema: _,
570            // Runtime execution state, rebuilt empty on decode.
571            metrics: _,
572            input_order_mode,
573            // Derived at construction from `input_order_mode` and the window
574            // expressions' PARTITION BY.
575            ordered_partition_by_indices: _,
576            // Derived at construction by `Self::compute_properties`.
577            cache: _,
578            // No wire field of its own; it is folded into `partition_keys`
579            // below, since `partition_keys()` returns an empty vec when this is
580            // false and the decoder recovers it as `!partition_keys.is_empty()`.
581            can_repartition: _,
582            // Runtime callback installed after planning; not part of the wire
583            // format. Any decoder that needs it must reinstall via
584            // `with_state_observer`.
585            state_observer: _,
586        } = self;
587
588        let input = ctx.encode_child(input)?;
589        let window_expr = window_expr
590            .iter()
591            .map(|expr| encode_physical_window_expr(expr, ctx))
592            .collect::<Result<Vec<_>>>()?;
593        let partition_keys = self
594            .partition_keys()
595            .iter()
596            .map(|expr| ctx.encode_expr(expr))
597            .collect::<Result<Vec<_>>>()?;
598        // A `Some(input_order_mode)` is what tells the shared `Window` decode
599        // arm to rebuild a `BoundedWindowAggExec` rather than a `WindowAggExec`.
600        let input_order_mode = match input_order_mode {
601            InputOrderMode::Linear => ProtoInputOrderMode::Linear(EmptyMessage {}),
602            InputOrderMode::PartiallySorted(columns) => {
603                ProtoInputOrderMode::PartiallySorted(
604                    protobuf::PartiallySortedInputOrderMode {
605                        columns: columns.iter().map(|column| *column as u64).collect(),
606                    },
607                )
608            }
609            InputOrderMode::Sorted => ProtoInputOrderMode::Sorted(EmptyMessage {}),
610        };
611
612        Ok(Some(protobuf::PhysicalPlanNode {
613            physical_plan_type: Some(
614                protobuf::physical_plan_node::PhysicalPlanType::Window(Box::new(
615                    protobuf::WindowAggExecNode {
616                        input: Some(Box::new(input)),
617                        window_expr,
618                        partition_keys,
619                        input_order_mode: Some(input_order_mode),
620                    },
621                )),
622            ),
623        }))
624    }
625}
626
627/// Trait that specifies how we search for (or calculate) partitions. It has two
628/// implementations: [`SortedSearch`] and [`LinearSearch`].
629trait PartitionSearcher: Send {
630    /// This method constructs output columns using the result of each window expression
631    /// (each entry in the output vector comes from a window expression).
632    /// Executor when producing output concatenates `input_buffer` (corresponding section), and
633    /// result of this function to generate output `RecordBatch`. `input_buffer` is used to determine
634    /// which sections of the window expression results should be used to generate output.
635    /// `partition_buffers` contains corresponding section of the `RecordBatch` for each partition.
636    /// `window_agg_states` stores per partition state for each window expression.
637    /// None case means that no result is generated
638    /// `Some(Vec<ArrayRef>)` is the result of each window expression.
639    fn calculate_out_columns(
640        &mut self,
641        input_buffer: &RecordBatch,
642        window_agg_states: &[PartitionWindowAggStates],
643        partition_buffers: &mut PartitionBatches,
644        window_expr: &[Arc<dyn WindowExpr>],
645    ) -> Result<Option<Vec<ArrayRef>>>;
646
647    /// Determine whether `[InputOrderMode]` is `[InputOrderMode::Linear]` or not.
648    fn is_mode_linear(&self) -> bool {
649        false
650    }
651
652    // Constructs corresponding batches for each partition for the record_batch.
653    fn evaluate_partition_batches(
654        &mut self,
655        record_batch: &RecordBatch,
656        window_expr: &[Arc<dyn WindowExpr>],
657    ) -> Result<Vec<(PartitionKey, RecordBatch)>>;
658
659    /// Prunes the state.
660    fn prune(&mut self, _n_out: usize) {}
661
662    /// Marks the partition as done if we are sure that corresponding partition
663    /// cannot receive any more values.
664    fn mark_partition_end(&self, partition_buffers: &mut PartitionBatches);
665
666    /// Updates `input_buffer` and `partition_buffers` with the new `record_batch`.
667    fn update_partition_batch(
668        &mut self,
669        input_buffer: &mut RecordBatch,
670        record_batch: RecordBatch,
671        window_expr: &[Arc<dyn WindowExpr>],
672        partition_buffers: &mut PartitionBatches,
673    ) -> Result<()> {
674        if record_batch.num_rows() == 0 {
675            return Ok(());
676        }
677        let partition_batches =
678            self.evaluate_partition_batches(&record_batch, window_expr)?;
679        for (partition_row, partition_batch) in partition_batches {
680            if let Some(partition_batch_state) = partition_buffers.get_mut(&partition_row)
681            {
682                partition_batch_state.extend(&partition_batch)?
683            } else {
684                let options = RecordBatchOptions::new()
685                    .with_row_count(Some(partition_batch.num_rows()));
686                // Use input_schema for the buffer schema, not `record_batch.schema()`
687                // as it may not have the "correct" schema in terms of output
688                // nullability constraints. For details, see the following issue:
689                // https://github.com/apache/datafusion/issues/9320
690                let partition_batch = RecordBatch::try_new_with_options(
691                    Arc::clone(self.input_schema()),
692                    partition_batch.columns().to_vec(),
693                    &options,
694                )?;
695                let partition_batch_state =
696                    PartitionBatchState::new_with_batch(partition_batch);
697                partition_buffers.insert(partition_row, partition_batch_state);
698            }
699        }
700
701        self.mark_partition_end(partition_buffers);
702
703        *input_buffer = if input_buffer.num_rows() == 0 {
704            record_batch
705        } else {
706            concat_batches(self.input_schema(), [input_buffer, &record_batch])?
707        };
708
709        Ok(())
710    }
711
712    fn input_schema(&self) -> &SchemaRef;
713}
714
715/// This object encapsulates the algorithm state for a simple linear scan
716/// algorithm for computing partitions.
717pub struct LinearSearch {
718    /// Keeps the hash of input buffer calculated from PARTITION BY columns.
719    /// Its length is equal to the `input_buffer` length.
720    input_buffer_hashes: VecDeque<u64>,
721    /// Used during hash value calculation.
722    random_state: RandomState,
723    /// Input ordering and partition by key ordering need not be the same, so
724    /// this vector stores the mapping between them. For instance, if the input
725    /// is ordered by a, b and the window expression contains a PARTITION BY b, a
726    /// clause, this attribute stores [1, 0].
727    ordered_partition_by_indices: Vec<usize>,
728    /// We use this [`HashTable`] to calculate unique partitions for each new
729    /// RecordBatch. First entry in the tuple is the hash value, the second
730    /// entry is the unique ID for each partition (increments from 0 to n).
731    row_map_batch: HashTable<(u64, usize)>,
732    /// We use this [`HashTable`] to calculate the output columns that we can
733    /// produce at each cycle. First entry in the tuple is the hash value, the
734    /// second entry is the unique ID for each partition (increments from 0 to n).
735    /// The third entry stores how many new outputs are calculated for the
736    /// corresponding partition.
737    row_map_out: HashTable<(u64, usize, usize)>,
738    input_schema: SchemaRef,
739}
740
741impl PartitionSearcher for LinearSearch {
742    /// This method constructs output columns using the result of each window expression.
743    // Assume input buffer is         |      Partition Buffers would be (Where each partition and its data is separated)
744    // a, 2                           |      a, 2
745    // b, 2                           |      a, 2
746    // a, 2                           |      a, 2
747    // b, 2                           |
748    // a, 2                           |      b, 2
749    // b, 2                           |      b, 2
750    // b, 2                           |      b, 2
751    //                                |      b, 2
752    // Also assume we happen to calculate 2 new values for a, and 3 for b (To be calculate missing values we may need to consider future values).
753    // Partition buffers effectively will be
754    // a, 2, 1
755    // a, 2, 2
756    // a, 2, (missing)
757    //
758    // b, 2, 1
759    // b, 2, 2
760    // b, 2, 3
761    // b, 2, (missing)
762    // When partition buffers are mapped back to the original record batch. Result becomes
763    // a, 2, 1
764    // b, 2, 1
765    // a, 2, 2
766    // b, 2, 2
767    // a, 2, (missing)
768    // b, 2, 3
769    // b, 2, (missing)
770    // This function calculates the column result of window expression(s) (First 4 entry of 3rd column in the above section.)
771    // 1
772    // 1
773    // 2
774    // 2
775    // Above section corresponds to calculated result which can be emitted without breaking input buffer ordering.
776    fn calculate_out_columns(
777        &mut self,
778        input_buffer: &RecordBatch,
779        window_agg_states: &[PartitionWindowAggStates],
780        partition_buffers: &mut PartitionBatches,
781        window_expr: &[Arc<dyn WindowExpr>],
782    ) -> Result<Option<Vec<ArrayRef>>> {
783        let partition_output_indices = self.calc_partition_output_indices(
784            input_buffer,
785            window_agg_states,
786            window_expr,
787        )?;
788
789        let n_window_col = window_agg_states.len();
790        let mut new_columns = vec![vec![]; n_window_col];
791        // Size of all_indices can be at most input_buffer.num_rows():
792        let mut all_indices = UInt32Builder::with_capacity(input_buffer.num_rows());
793        for (row, indices) in partition_output_indices {
794            let length = indices.len();
795            for (idx, window_agg_state) in window_agg_states.iter().enumerate() {
796                let partition = &window_agg_state[&row];
797                let values = Arc::clone(&partition.state.out_col.slice(0, length));
798                new_columns[idx].push(values);
799            }
800            let partition_batch_state = &mut partition_buffers[&row];
801            // Store how many rows are generated for each partition
802            partition_batch_state.n_out_row = length;
803            // For each row keep corresponding index in the input record batch
804            all_indices.append_slice(&indices);
805        }
806        let all_indices = all_indices.finish();
807        if all_indices.is_empty() {
808            // We couldn't generate any new value, return early:
809            return Ok(None);
810        }
811
812        // Concatenate results for each column by converting `Vec<Vec<ArrayRef>>`
813        // to Vec<ArrayRef> where inner `Vec<ArrayRef>`s are converted to `ArrayRef`s.
814        let new_columns = new_columns
815            .iter()
816            .map(|items| {
817                concat(&items.iter().map(|e| e.as_ref()).collect::<Vec<_>>())
818                    .map_err(|e| arrow_datafusion_err!(e))
819            })
820            .collect::<Result<Vec<_>>>()?;
821        // We should emit columns according to row index ordering.
822        let sorted_indices = sort_to_indices(&all_indices, None, None)?;
823        // Construct new column according to row ordering. This fixes ordering
824        take_arrays(&new_columns, &sorted_indices, None)
825            .map(Some)
826            .map_err(|e| arrow_datafusion_err!(e))
827    }
828
829    fn evaluate_partition_batches(
830        &mut self,
831        record_batch: &RecordBatch,
832        window_expr: &[Arc<dyn WindowExpr>],
833    ) -> Result<Vec<(PartitionKey, RecordBatch)>> {
834        let partition_bys =
835            evaluate_partition_by_column_values(record_batch, window_expr)?;
836        // NOTE: In Linear or PartiallySorted modes, we are sure that
837        //       `partition_bys` are not empty.
838        let (mut keys, permutation, bounds) =
839            self.compute_partition_permutation(&partition_bys, record_batch)?;
840        if keys.len() == 1 {
841            // The batch contains a single partition, so the gather below
842            // would be an identity permutation; use the batch as-is.
843            let key = keys.remove(0);
844            return Ok(vec![(key, record_batch.clone())]);
845        }
846        // Reorder the batch with a single `take` so that each partition's
847        // rows become contiguous, then hand each partition a zero-copy slice
848        // of the result. The slices share the gathered batch's buffers;
849        // `PartitionBatchState::extend` copies out of them the next time the
850        // partition receives rows.
851        let gathered = take_record_batch(record_batch, &UInt32Array::from(permutation))?;
852        Ok(keys
853            .into_iter()
854            .zip(bounds.windows(2))
855            .map(|(key, bound)| (key, gathered.slice(bound[0], bound[1] - bound[0])))
856            .collect())
857    }
858
859    fn prune(&mut self, n_out: usize) {
860        // Delete hashes for the rows that are outputted.
861        self.input_buffer_hashes.drain(0..n_out);
862    }
863
864    fn mark_partition_end(&self, partition_buffers: &mut PartitionBatches) {
865        // We should be in the `PartiallySorted` case, otherwise we can not
866        // tell when we are at the end of a given partition.
867        if !self.ordered_partition_by_indices.is_empty()
868            && let Some((last_row, _)) = partition_buffers.last()
869        {
870            let last_sorted_cols = self
871                .ordered_partition_by_indices
872                .iter()
873                .map(|idx| last_row[*idx].clone())
874                .collect::<Vec<_>>();
875            for (row, partition_batch_state) in partition_buffers.iter_mut() {
876                let sorted_cols = self
877                    .ordered_partition_by_indices
878                    .iter()
879                    .map(|idx| &row[*idx]);
880                // All the partitions other than `last_sorted_cols` are done.
881                // We are sure that we will no longer receive values for these
882                // partitions (arrival of a new value would violate ordering).
883                partition_batch_state.is_end = !sorted_cols.eq(&last_sorted_cols);
884            }
885        }
886    }
887
888    fn is_mode_linear(&self) -> bool {
889        self.ordered_partition_by_indices.is_empty()
890    }
891
892    fn input_schema(&self) -> &SchemaRef {
893        &self.input_schema
894    }
895}
896
897impl LinearSearch {
898    /// Initialize a new [`LinearSearch`] partition searcher.
899    fn new(ordered_partition_by_indices: Vec<usize>, input_schema: SchemaRef) -> Self {
900        LinearSearch {
901            input_buffer_hashes: VecDeque::new(),
902            random_state: Default::default(),
903            ordered_partition_by_indices,
904            row_map_batch: HashTable::with_capacity(256),
905            row_map_out: HashTable::with_capacity(256),
906            input_schema,
907        }
908    }
909
910    /// Splits the rows of `batch` by partition, according to the PARTITION BY
911    /// expression results in `columns`. Returns the distinct partition keys
912    /// in first-appearance order, a permutation of the row indices of
913    /// `batch` that groups each partition's rows together, and the
914    /// boundaries of each partition's run of rows within that permutation:
915    /// partition `p` occupies `permutation[bounds[p]..bounds[p + 1]]`, and
916    /// its indices are in ascending (stream) order.
917    fn compute_partition_permutation(
918        &mut self,
919        columns: &[ArrayRef],
920        batch: &RecordBatch,
921    ) -> Result<(Vec<PartitionKey>, Vec<u32>, Vec<usize>)> {
922        let num_rows = batch.num_rows();
923        let mut batch_hashes = vec![0; num_rows];
924        create_hashes(columns, &self.random_state, &mut batch_hashes)?;
925        self.input_buffer_hashes.extend(&batch_hashes);
926        // reset row_map for new calculation
927        self.row_map_batch.clear();
928        let mut keys: Vec<PartitionKey> = vec![];
929        // Partition id of each row, in row order:
930        let mut row_partition_ids = Vec::with_capacity(num_rows);
931        // Number of rows in each partition:
932        let mut counts: Vec<usize> = vec![];
933        for (hash, row_idx) in batch_hashes.into_iter().zip(0u32..) {
934            let entry = self.row_map_batch.find_mut(hash, |(_, group_idx)| {
935                let row = get_row_at_idx(columns, row_idx as usize).unwrap();
936                // Handle hash collisions with an equality check:
937                row == keys[*group_idx]
938            });
939            let group_idx = if let Some((_, group_idx)) = entry {
940                *group_idx
941            } else {
942                let group_idx = keys.len();
943                self.row_map_batch
944                    .insert_unique(hash, (hash, group_idx), |(hash, _)| *hash);
945                keys.push(get_row_at_idx(columns, row_idx as usize)?);
946                counts.push(0);
947                group_idx
948            };
949            row_partition_ids.push(group_idx);
950            counts[group_idx] += 1;
951        }
952        // A prefix sum over the counts gives each partition's run boundaries
953        // in the permutation.
954        let mut bounds = Vec::with_capacity(counts.len() + 1);
955        let mut total = 0;
956        bounds.push(0);
957        for count in counts {
958            total += count;
959            bounds.push(total);
960        }
961        // Scatter each row's index into its partition's run. Visiting rows
962        // in ascending order keeps each run in ascending row order.
963        let mut cursors: Vec<usize> = bounds[..bounds.len() - 1].to_vec();
964        let mut permutation = vec![0u32; num_rows];
965        for (row_idx, group_idx) in row_partition_ids.into_iter().enumerate() {
966            permutation[cursors[group_idx]] = row_idx as u32;
967            cursors[group_idx] += 1;
968        }
969        Ok((keys, permutation, bounds))
970    }
971
972    /// Calculates partition keys and result indices for each partition.
973    /// The return value is a vector of tuples where the first entry stores
974    /// the partition key (unique for each partition) and the second entry
975    /// stores indices of the rows for which the partition is constructed.
976    fn calc_partition_output_indices(
977        &mut self,
978        input_buffer: &RecordBatch,
979        window_agg_states: &[PartitionWindowAggStates],
980        window_expr: &[Arc<dyn WindowExpr>],
981    ) -> Result<Vec<(PartitionKey, Vec<u32>)>> {
982        let partition_by_columns =
983            evaluate_partition_by_column_values(input_buffer, window_expr)?;
984        // Reset the row_map state:
985        self.row_map_out.clear();
986        let mut partition_indices: Vec<(PartitionKey, Vec<u32>)> = vec![];
987        for (hash, row_idx) in self.input_buffer_hashes.iter().zip(0u32..) {
988            let entry = self.row_map_out.find_mut(*hash, |(_, group_idx, _)| {
989                let row =
990                    get_row_at_idx(&partition_by_columns, row_idx as usize).unwrap();
991                row == partition_indices[*group_idx].0
992            });
993            if let Some((_, group_idx, n_out)) = entry {
994                let (_, indices) = &mut partition_indices[*group_idx];
995                if indices.len() >= *n_out {
996                    break;
997                }
998                indices.push(row_idx);
999            } else {
1000                let row = get_row_at_idx(&partition_by_columns, row_idx as usize)?;
1001                let min_out = window_agg_states
1002                    .iter()
1003                    .map(|window_agg_state| {
1004                        window_agg_state
1005                            .get(&row)
1006                            .map(|partition| partition.state.out_col.len())
1007                            .unwrap_or(0)
1008                    })
1009                    .min()
1010                    .unwrap_or(0);
1011                if min_out == 0 {
1012                    break;
1013                }
1014                self.row_map_out.insert_unique(
1015                    *hash,
1016                    (*hash, partition_indices.len(), min_out),
1017                    |(hash, _, _)| *hash,
1018                );
1019                partition_indices.push((row, vec![row_idx]));
1020            }
1021        }
1022        Ok(partition_indices)
1023    }
1024}
1025
1026/// This object encapsulates the algorithm state for sorted searching
1027/// when computing partitions.
1028pub struct SortedSearch {
1029    /// Stores partition by columns and their ordering information
1030    partition_by_sort_keys: Vec<PhysicalSortExpr>,
1031    /// Input ordering and partition by key ordering need not be the same, so
1032    /// this vector stores the mapping between them. For instance, if the input
1033    /// is ordered by a, b and the window expression contains a PARTITION BY b, a
1034    /// clause, this attribute stores [1, 0].
1035    ordered_partition_by_indices: Vec<usize>,
1036    input_schema: SchemaRef,
1037}
1038
1039impl PartitionSearcher for SortedSearch {
1040    /// This method constructs new output columns using the result of each window expression.
1041    fn calculate_out_columns(
1042        &mut self,
1043        _input_buffer: &RecordBatch,
1044        window_agg_states: &[PartitionWindowAggStates],
1045        partition_buffers: &mut PartitionBatches,
1046        _window_expr: &[Arc<dyn WindowExpr>],
1047    ) -> Result<Option<Vec<ArrayRef>>> {
1048        let n_out = self.calculate_n_out_row(window_agg_states, partition_buffers);
1049        if n_out == 0 {
1050            Ok(None)
1051        } else {
1052            window_agg_states
1053                .iter()
1054                .map(|map| get_aggregate_result_out_column(map, n_out).map(Some))
1055                .collect()
1056        }
1057    }
1058
1059    fn evaluate_partition_batches(
1060        &mut self,
1061        record_batch: &RecordBatch,
1062        _window_expr: &[Arc<dyn WindowExpr>],
1063    ) -> Result<Vec<(PartitionKey, RecordBatch)>> {
1064        let num_rows = record_batch.num_rows();
1065        // Calculate result of partition by column expressions
1066        let partition_columns = self
1067            .partition_by_sort_keys
1068            .iter()
1069            .map(|elem| elem.evaluate_to_sort_column(record_batch))
1070            .collect::<Result<Vec<_>>>()?;
1071        // Reorder `partition_columns` such that its ordering matches input ordering.
1072        let partition_columns_ordered =
1073            get_at_indices(&partition_columns, &self.ordered_partition_by_indices)?;
1074        let partition_points =
1075            evaluate_partition_ranges(num_rows, &partition_columns_ordered)?;
1076        let partition_bys = partition_columns
1077            .into_iter()
1078            .map(|arr| arr.values)
1079            .collect::<Vec<ArrayRef>>();
1080
1081        partition_points
1082            .iter()
1083            .map(|range| {
1084                let row = get_row_at_idx(&partition_bys, range.start)?;
1085                let len = range.end - range.start;
1086                let slice = record_batch.slice(range.start, len);
1087                Ok((row, slice))
1088            })
1089            .collect::<Result<Vec<_>>>()
1090    }
1091
1092    fn mark_partition_end(&self, partition_buffers: &mut PartitionBatches) {
1093        // In Sorted case. We can mark all partitions besides last partition as ended.
1094        // We are sure that those partitions will never receive any values.
1095        // (Otherwise ordering invariant is violated.)
1096        let n_partitions = partition_buffers.len();
1097        for (idx, (_, partition_batch_state)) in partition_buffers.iter_mut().enumerate()
1098        {
1099            partition_batch_state.is_end |= idx < n_partitions - 1;
1100        }
1101    }
1102
1103    fn input_schema(&self) -> &SchemaRef {
1104        &self.input_schema
1105    }
1106}
1107
1108impl SortedSearch {
1109    /// Calculates how many rows we can output.
1110    fn calculate_n_out_row(
1111        &mut self,
1112        window_agg_states: &[PartitionWindowAggStates],
1113        partition_buffers: &mut PartitionBatches,
1114    ) -> usize {
1115        // Different window aggregators may produce results at different rates.
1116        // We produce the overall batch result only as fast as the slowest one.
1117        let mut counts = vec![];
1118        let out_col_counts = window_agg_states.iter().map(|window_agg_state| {
1119            // Store how many elements are generated for the current
1120            // window expression:
1121            let mut cur_window_expr_out_result_len = 0;
1122            // We iterate over `window_agg_state`, which is an IndexMap.
1123            // Iterations follow the insertion order, hence we preserve
1124            // sorting when partition columns are sorted.
1125            let mut per_partition_out_results = HashMap::new();
1126            for (row, WindowState { state, .. }) in window_agg_state.iter() {
1127                cur_window_expr_out_result_len += state.out_col.len();
1128                let count = per_partition_out_results.entry(row).or_insert(0);
1129                if *count < state.out_col.len() {
1130                    *count = state.out_col.len();
1131                }
1132                // If we do not generate all results for the current
1133                // partition, we do not generate results for next
1134                // partition --  otherwise we will lose input ordering.
1135                if state.n_row_result_missing > 0 {
1136                    break;
1137                }
1138            }
1139            counts.push(per_partition_out_results);
1140            cur_window_expr_out_result_len
1141        });
1142        argmin(out_col_counts).map_or(0, |(min_idx, minima)| {
1143            let mut slowest_partition = counts.swap_remove(min_idx);
1144            for (partition_key, partition_batch) in partition_buffers.iter_mut() {
1145                if let Some(count) = slowest_partition.remove(partition_key) {
1146                    partition_batch.n_out_row = count;
1147                }
1148            }
1149            minima
1150        })
1151    }
1152}
1153
1154/// Calculates partition by expression results for each window expression
1155/// on `record_batch`.
1156fn evaluate_partition_by_column_values(
1157    record_batch: &RecordBatch,
1158    window_expr: &[Arc<dyn WindowExpr>],
1159) -> Result<Vec<ArrayRef>> {
1160    window_expr[0]
1161        .partition_by()
1162        .iter()
1163        .map(|item| match item.evaluate(record_batch)? {
1164            ColumnarValue::Array(array) => Ok(array),
1165            ColumnarValue::Scalar(scalar) => {
1166                scalar.to_array_of_size(record_batch.num_rows())
1167            }
1168        })
1169        .collect()
1170}
1171
1172/// Stream for the bounded window aggregation plan.
1173pub struct BoundedWindowAggStream {
1174    schema: SchemaRef,
1175    input: SendableRecordBatchStream,
1176    /// The record batch executor receives as input (i.e. the columns needed
1177    /// while calculating aggregation results).
1178    input_buffer: RecordBatch,
1179    /// Each partition's rows, accumulated across input batches. All window
1180    /// expressions calculate their results against these shared rows without
1181    /// copying.
1182    partition_buffers: PartitionBatches,
1183    /// An executor can run multiple window expressions if the PARTITION BY
1184    /// and ORDER BY sections are same. We keep state of the each window
1185    /// expression inside `window_agg_states`.
1186    window_agg_states: Vec<PartitionWindowAggStates>,
1187    finished: bool,
1188    window_expr: Vec<Arc<dyn WindowExpr>>,
1189    baseline_metrics: BaselineMetrics,
1190    /// Search mode for partition columns. This determines the algorithm with
1191    /// which we group each partition.
1192    search_mode: Box<dyn PartitionSearcher>,
1193    /// In `Linear` mode, a single-row batch containing the most recent input
1194    /// row (whichever partition that row belongs to); `None` in other modes
1195    /// and before the first non-empty batch arrives. Since in `Linear` mode
1196    /// the input is sorted by the first ORDER BY column, no future input row
1197    /// -- in any partition -- can precede this row in that column. Every
1198    /// partition's evaluation consults this bound to decide whether pending
1199    /// window frames can be finalized before the partition receives more
1200    /// data (which in turn allows buffered state to be pruned). Note that
1201    /// only the first ORDER BY column provides this guarantee. As a counter
1202    /// example, consider `PARTITION BY b, ORDER BY a, c` when the input is
1203    /// sorted by `[a, b, c]`: the mode will be `Linear`, but the last row of
1204    /// the input is the "last" data in terms of `[a, b, c]`, not in terms of
1205    /// the ordering requirement `[a, c]`. Hence, only column `a` can serve
1206    /// as a guarantee of the "last" data across partitions. In the `Sorted`
1207    /// and `PartiallySorted` modes, the leading ordering separates
1208    /// partitions, so finished partitions are pruned eagerly instead and no
1209    /// such bound is needed.
1210    most_recent_row: Option<RecordBatch>,
1211    /// Output partition index this stream serves; passed as the first
1212    /// argument to [`WindowStateObserver::finalize_window_aggregate`].
1213    partition_idx: usize,
1214    /// If set, invoked from [`Self::publish_finalized_states`] with the
1215    /// finalized per-window-expression state for every partition key that is
1216    /// about to be dropped.
1217    state_observer: Option<Arc<dyn WindowStateObserver>>,
1218}
1219
1220impl BoundedWindowAggStream {
1221    /// Fire `observer` once per (window expression, partition key) for every
1222    /// group whose [`WindowAggState::is_end`] is true. Always mutates when
1223    /// called: [`datafusion_expr::Accumulator::state`] requires `&mut`, which
1224    /// propagates up here. The caller is responsible for deciding whether to
1225    /// fire (i.e. checking whether an observer is installed).
1226    ///
1227    /// Exactly-once per group is enforced by [`WindowState::aggregate_state`],
1228    /// which errors on second call; the `published` early-skip below avoids reaching the error.
1229    fn publish_finalized_states(
1230        &mut self,
1231        observer: &dyn WindowStateObserver,
1232    ) -> Result<()> {
1233        let partition_idx = self.partition_idx;
1234        for (expr_idx, per_expr) in self.window_agg_states.iter_mut().enumerate() {
1235            let window_expr = &self.window_expr[expr_idx];
1236            for (key, ws) in per_expr.iter_mut() {
1237                if ws.published || !ws.state.is_end {
1238                    continue;
1239                }
1240                if let Some(state) = ws.aggregate_state()? {
1241                    observer.finalize_window_aggregate(
1242                        partition_idx,
1243                        window_expr,
1244                        key,
1245                        state,
1246                    )?;
1247                }
1248            }
1249        }
1250        Ok(())
1251    }
1252
1253    /// Prunes sections of the state that are no longer needed when calculating
1254    /// results (as determined by window frame boundaries and number of results generated).
1255    // For instance, if first `n` (not necessarily same with `n_out`) elements are no longer needed to
1256    // calculate window expression result (outside the window frame boundary) we retract first `n` elements
1257    // from the corresponding partition's batch in `self.partition_buffers`.
1258    // For instance, if `n_out` number of rows are calculated, we can remove
1259    // first `n_out` rows from `self.input_buffer`.
1260    fn prune_state(&mut self, n_out: usize) -> Result<()> {
1261        // Prune `self.window_agg_states`:
1262        self.prune_out_columns();
1263        // Prune `self.partition_buffers`:
1264        self.prune_partition_batches();
1265        // Prune `self.input_buffer`:
1266        self.prune_input_batch(n_out)?;
1267        // Prune internal state of search algorithm.
1268        self.search_mode.prune(n_out);
1269        Ok(())
1270    }
1271}
1272
1273impl Stream for BoundedWindowAggStream {
1274    type Item = Result<RecordBatch>;
1275
1276    fn poll_next(
1277        mut self: Pin<&mut Self>,
1278        cx: &mut Context<'_>,
1279    ) -> Poll<Option<Self::Item>> {
1280        let poll = self.poll_next_inner(cx);
1281        self.baseline_metrics.record_poll(poll)
1282    }
1283}
1284
1285impl BoundedWindowAggStream {
1286    /// Create a new BoundedWindowAggStream
1287    fn new(
1288        schema: SchemaRef,
1289        window_expr: Vec<Arc<dyn WindowExpr>>,
1290        input: SendableRecordBatchStream,
1291        baseline_metrics: BaselineMetrics,
1292        search_mode: Box<dyn PartitionSearcher>,
1293        partition_idx: usize,
1294        state_observer: Option<Arc<dyn WindowStateObserver>>,
1295    ) -> Result<Self> {
1296        let state = window_expr.iter().map(|_| IndexMap::default()).collect();
1297        let empty_batch = RecordBatch::new_empty(Arc::clone(&schema));
1298        Ok(Self {
1299            schema,
1300            input,
1301            input_buffer: empty_batch,
1302            partition_buffers: IndexMap::default(),
1303            window_agg_states: state,
1304            finished: false,
1305            window_expr,
1306            baseline_metrics,
1307            search_mode,
1308            most_recent_row: None,
1309            partition_idx,
1310            state_observer,
1311        })
1312    }
1313
1314    fn compute_aggregates(&mut self) -> Result<Option<RecordBatch>> {
1315        // calculate window cols
1316        let eval_ctx = WindowEvalContext::default()
1317            .with_most_recent_row(self.most_recent_row.as_ref());
1318        for (cur_window_expr, state) in
1319            self.window_expr.iter().zip(&mut self.window_agg_states)
1320        {
1321            cur_window_expr.evaluate_stateful(
1322                &self.partition_buffers,
1323                state,
1324                &eval_ctx,
1325            )?;
1326        }
1327
1328        // Fire before `calculate_out_columns`: on causal frames every row
1329        // already streamed out, so at EOS that call returns `None` and the
1330        // prune path is skipped — the final partition would otherwise be
1331        // dropped unobserved.
1332        if let Some(observer) = self.state_observer.clone() {
1333            self.publish_finalized_states(observer.as_ref())?;
1334        }
1335
1336        let schema = Arc::clone(&self.schema);
1337        let window_expr_out = self.search_mode.calculate_out_columns(
1338            &self.input_buffer,
1339            &self.window_agg_states,
1340            &mut self.partition_buffers,
1341            &self.window_expr,
1342        )?;
1343        if let Some(window_expr_out) = window_expr_out {
1344            let n_out = window_expr_out[0].len();
1345            // right append new columns to corresponding section in the original input buffer.
1346            let columns_to_show = self
1347                .input_buffer
1348                .columns()
1349                .iter()
1350                .map(|elem| elem.slice(0, n_out))
1351                .chain(window_expr_out)
1352                .collect::<Vec<_>>();
1353            let n_generated = columns_to_show[0].len();
1354            self.prune_state(n_generated)?;
1355            Ok(Some(RecordBatch::try_new(schema, columns_to_show)?))
1356        } else {
1357            Ok(None)
1358        }
1359    }
1360
1361    #[inline]
1362    fn poll_next_inner(
1363        &mut self,
1364        cx: &mut Context<'_>,
1365    ) -> Poll<Option<Result<RecordBatch>>> {
1366        if self.finished {
1367            return Poll::Ready(None);
1368        }
1369
1370        let elapsed_compute = self.baseline_metrics.elapsed_compute().clone();
1371        match ready!(self.input.poll_next_unpin(cx)) {
1372            Some(Ok(batch)) => {
1373                // Start the timer for compute time within this operator. It will be
1374                // stopped when dropped.
1375                let _timer = elapsed_compute.timer();
1376
1377                if self.search_mode.is_mode_linear() && batch.num_rows() > 0 {
1378                    self.most_recent_row = Some(get_last_row_batch(&batch)?);
1379                }
1380                self.search_mode.update_partition_batch(
1381                    &mut self.input_buffer,
1382                    batch,
1383                    &self.window_expr,
1384                    &mut self.partition_buffers,
1385                )?;
1386                if let Some(batch) = self.compute_aggregates()? {
1387                    return Poll::Ready(Some(Ok(batch)));
1388                }
1389                self.poll_next_inner(cx)
1390            }
1391            Some(Err(e)) => Poll::Ready(Some(Err(e))),
1392            None => {
1393                let _timer = elapsed_compute.timer();
1394
1395                self.finished = true;
1396                // Release the input pipeline's resources before computing the
1397                // final aggregates.
1398                let input_schema = self.input.schema();
1399                self.input = Box::pin(EmptyRecordBatchStream::new(input_schema));
1400                for (_, partition_batch_state) in self.partition_buffers.iter_mut() {
1401                    partition_batch_state.is_end = true;
1402                }
1403                if let Some(batch) = self.compute_aggregates()? {
1404                    return Poll::Ready(Some(Ok(batch)));
1405                }
1406                Poll::Ready(None)
1407            }
1408        }
1409    }
1410
1411    /// Removes partitions that have ended. For the remaining partitions,
1412    /// drops buffered rows that no window expression will need again.
1413    fn prune_partition_batches(&mut self) {
1414        // Check that per-state and per-partition end-flags are consistent;
1415        // otherwise, the pruning code below might produce inconsistent state.
1416        #[cfg(debug_assertions)]
1417        for window_agg_state in self.window_agg_states.iter() {
1418            for (partition_row, WindowState { state, .. }) in window_agg_state.iter() {
1419                debug_assert_eq!(
1420                    state.is_end, self.partition_buffers[partition_row].is_end,
1421                    "window state's recorded end flag is out of sync with its partition"
1422                );
1423            }
1424        }
1425
1426        // Remove partitions which we know already ended (is_end flag is true).
1427        // Since the retain method preserves insertion order, we still have
1428        // ordering in between partitions after removal.
1429        self.partition_buffers
1430            .retain(|_, partition_batch_state| !partition_batch_state.is_end);
1431        // Likewise, drop per-window-expression state for ended partitions.
1432        for window_agg_state in self.window_agg_states.iter_mut() {
1433            window_agg_state.retain(|_, WindowState { state, .. }| !state.is_end);
1434        }
1435
1436        // Calculate how many rows to prune from each partition's batch. For a
1437        // single window expression, rows before min(window_frame_range.start,
1438        // last_calculated_index) are prunable: their results are already
1439        // calculated, and frame boundaries never move backwards, so no future
1440        // frame can include them. All window expressions share the partition
1441        // batch, so a row can only be pruned once every expression is done with
1442        // it: the count to prune is the minimum across expressions. A partition
1443        // missing from the map has nothing to prune.
1444        let mut n_prune_each_partition = HashMap::new();
1445        if let Some((first, rest)) = self.window_agg_states.split_first() {
1446            // First window expression seeds the prune-count map
1447            for (partition_row, WindowState { state, .. }) in first.iter() {
1448                let n_prune =
1449                    min(state.window_frame_range.start, state.last_calculated_index);
1450                if n_prune > 0 {
1451                    n_prune_each_partition.insert(partition_row.clone(), n_prune);
1452                }
1453            }
1454            // Take the per-partition min of the prune-count for each
1455            // additional window expression
1456            for window_agg_state in rest {
1457                n_prune_each_partition.retain(|partition_row, current| {
1458                    let Some(WindowState { state, .. }) =
1459                        window_agg_state.get(partition_row)
1460                    else {
1461                        return false;
1462                    };
1463                    let n_prune =
1464                        min(state.window_frame_range.start, state.last_calculated_index);
1465                    *current = min(*current, n_prune);
1466                    *current > 0
1467                });
1468            }
1469        }
1470
1471        // Drop the prunable prefix of each partition's buffered batch:
1472        for (partition_row, n_prune) in n_prune_each_partition.iter() {
1473            debug_assert!(
1474                *n_prune > 0,
1475                "prune-count map must only contain positive entries"
1476            );
1477            let pb_state = &mut self.partition_buffers[partition_row];
1478
1479            let batch = &pb_state.record_batch;
1480            pb_state.record_batch = batch.slice(*n_prune, batch.num_rows() - n_prune);
1481
1482            // Update state indices since we have pruned some rows from the beginning:
1483            for window_agg_state in self.window_agg_states.iter_mut() {
1484                window_agg_state[partition_row].state.prune_state(*n_prune);
1485            }
1486        }
1487    }
1488
1489    /// Prunes the section of the input batch whose aggregate results
1490    /// are calculated and emitted.
1491    fn prune_input_batch(&mut self, n_out: usize) -> Result<()> {
1492        // Prune first n_out rows from the input_buffer
1493        let n_to_keep = self.input_buffer.num_rows() - n_out;
1494        let batch_to_keep = self
1495            .input_buffer
1496            .columns()
1497            .iter()
1498            .map(|elem| elem.slice(n_out, n_to_keep))
1499            .collect::<Vec<_>>();
1500        self.input_buffer = RecordBatch::try_new_with_options(
1501            self.input_buffer.schema(),
1502            batch_to_keep,
1503            &RecordBatchOptions::new().with_row_count(Some(n_to_keep)),
1504        )?;
1505        Ok(())
1506    }
1507
1508    /// Prunes emitted parts from WindowAggState `out_col` field.
1509    fn prune_out_columns(&mut self) {
1510        // We store generated columns for each window expression in the `out_col`
1511        // field of `WindowAggState`. Given how many rows are emitted, we remove
1512        // these sections from state.
1513        for partition_window_agg_states in self.window_agg_states.iter_mut() {
1514            // If `is_end` is set, directly remove the entry; this shrinks the
1515            // hash map.
1516            partition_window_agg_states
1517                .retain(|_, partition_batch_state| !partition_batch_state.state.is_end);
1518        }
1519        // Only partitions that emitted rows since the previous pruning pass
1520        // have output columns to shrink. Their emitted-row counts are
1521        // consumed and reset here, so partitions that emitted nothing keep
1522        // a count of zero and are passed over without any hash lookups.
1523        for (partition_key, partition_batch) in self.partition_buffers.iter_mut() {
1524            let n_emitted = partition_batch.n_out_row;
1525            if n_emitted == 0 {
1526                continue;
1527            }
1528            partition_batch.n_out_row = 0;
1529            for partition_window_agg_states in self.window_agg_states.iter_mut() {
1530                if let Some(WindowState { state, .. }) =
1531                    partition_window_agg_states.get_mut(partition_key)
1532                {
1533                    let out_col = &mut state.out_col;
1534                    let n_to_keep = out_col.len() - n_emitted;
1535                    *out_col = out_col.slice(n_emitted, n_to_keep);
1536                }
1537            }
1538        }
1539    }
1540}
1541
1542impl RecordBatchStream for BoundedWindowAggStream {
1543    /// Get the schema
1544    fn schema(&self) -> SchemaRef {
1545        Arc::clone(&self.schema)
1546    }
1547}
1548
1549// Gets the index of minimum entry, returns None if empty.
1550fn argmin<T: PartialOrd>(data: impl Iterator<Item = T>) -> Option<(usize, T)> {
1551    data.enumerate()
1552        .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Equal))
1553}
1554
1555/// Calculates the section we can show results for expression
1556fn get_aggregate_result_out_column(
1557    partition_window_agg_states: &PartitionWindowAggStates,
1558    len_to_show: usize,
1559) -> Result<ArrayRef> {
1560    let mut result = None;
1561    let mut running_length = 0;
1562    let mut batches_to_concat = vec![];
1563    // We assume that iteration order is according to insertion order
1564    for (
1565        _,
1566        WindowState {
1567            state: WindowAggState { out_col, .. },
1568            ..
1569        },
1570    ) in partition_window_agg_states
1571    {
1572        if running_length < len_to_show {
1573            let n_to_use = min(len_to_show - running_length, out_col.len());
1574            let slice_to_use = if n_to_use == out_col.len() {
1575                // avoid slice when the entire column is used
1576                Arc::clone(out_col)
1577            } else {
1578                out_col.slice(0, n_to_use)
1579            };
1580            batches_to_concat.push(slice_to_use);
1581            running_length += n_to_use;
1582        } else {
1583            break;
1584        }
1585    }
1586
1587    if !batches_to_concat.is_empty() {
1588        let array_refs: Vec<&dyn Array> =
1589            batches_to_concat.iter().map(|a| a.as_ref()).collect();
1590        result = Some(concat(&array_refs)?);
1591    }
1592
1593    if running_length != len_to_show {
1594        return exec_err!(
1595            "Generated row number should be {len_to_show}, it is {running_length}"
1596        );
1597    }
1598    result.ok_or_else(|| exec_datafusion_err!("Should contain something"))
1599}
1600
1601/// Constructs a batch from the last row of batch in the argument.
1602pub(crate) fn get_last_row_batch(batch: &RecordBatch) -> Result<RecordBatch> {
1603    if batch.num_rows() == 0 {
1604        return exec_err!("Latest batch should have at least 1 row");
1605    }
1606    Ok(batch.slice(batch.num_rows() - 1, 1))
1607}
1608
1609#[cfg(test)]
1610mod tests {
1611    use std::pin::Pin;
1612    use std::sync::Arc;
1613    use std::task::{Context, Poll};
1614    use std::time::Duration;
1615
1616    use crate::common::collect;
1617    use crate::execution_plan::CardinalityEffect;
1618    use crate::expressions::PhysicalSortExpr;
1619    use crate::projection::{ProjectionExec, ProjectionExpr};
1620    use crate::streaming::{PartitionStream, StreamingTableExec};
1621    use crate::test::TestMemoryExec;
1622    use crate::windows::bounded_window_agg_exec::WindowStateObserver;
1623    use crate::windows::{
1624        BoundedWindowAggExec, InputOrderMode, create_udwf_window_expr, create_window_expr,
1625    };
1626    use crate::{ExecutionPlan, WindowExpr, displayable, execute_stream};
1627
1628    use arrow::array::{
1629        RecordBatch,
1630        builder::{Int64Builder, UInt64Builder},
1631    };
1632    use arrow::compute::SortOptions;
1633    use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
1634    use datafusion_common::test_util::batches_to_string;
1635    use datafusion_common::{Result, ScalarValue, exec_datafusion_err};
1636    use datafusion_execution::config::SessionConfig;
1637    use datafusion_execution::{
1638        RecordBatchStream, SendableRecordBatchStream, TaskContext,
1639    };
1640    use datafusion_expr::{
1641        WindowFrame, WindowFrameBound, WindowFrameUnits, WindowFunctionDefinition,
1642    };
1643    use datafusion_functions_aggregate::count::count_udaf;
1644    use datafusion_functions_aggregate::sum::sum_udaf;
1645    use datafusion_functions_window::nth_value::last_value_udwf;
1646    use datafusion_functions_window::nth_value::nth_value_udwf;
1647    use datafusion_physical_expr::expressions::{Column, Literal, col};
1648    use datafusion_physical_expr::window::{PartitionKey, StandardWindowExpr};
1649    use datafusion_physical_expr::{LexOrdering, PhysicalExpr};
1650
1651    use futures::future::Shared;
1652    use futures::{FutureExt, Stream, StreamExt, pin_mut, ready};
1653    use insta::assert_snapshot;
1654    use itertools::Itertools;
1655    use tokio::time::timeout;
1656
1657    #[derive(Debug, Clone)]
1658    struct TestStreamPartition {
1659        schema: SchemaRef,
1660        batches: Vec<RecordBatch>,
1661        idx: usize,
1662        state: PolingState,
1663        sleep_duration: Duration,
1664        send_exit: bool,
1665    }
1666
1667    impl PartitionStream for TestStreamPartition {
1668        fn schema(&self) -> &SchemaRef {
1669            &self.schema
1670        }
1671
1672        fn execute(&self, _ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
1673            // We create an iterator from the record batches and map them into Ok values,
1674            // converting the iterator into a futures::stream::Stream
1675            Box::pin(self.clone())
1676        }
1677    }
1678
1679    impl Stream for TestStreamPartition {
1680        type Item = Result<RecordBatch>;
1681
1682        fn poll_next(
1683            mut self: Pin<&mut Self>,
1684            cx: &mut Context<'_>,
1685        ) -> Poll<Option<Self::Item>> {
1686            self.poll_next_inner(cx)
1687        }
1688    }
1689
1690    #[derive(Debug, Clone)]
1691    enum PolingState {
1692        Sleep(Shared<futures::future::BoxFuture<'static, ()>>),
1693        BatchReturn,
1694    }
1695
1696    impl TestStreamPartition {
1697        fn poll_next_inner(
1698            self: &mut Pin<&mut Self>,
1699            cx: &mut Context<'_>,
1700        ) -> Poll<Option<Result<RecordBatch>>> {
1701            loop {
1702                match &mut self.state {
1703                    PolingState::BatchReturn => {
1704                        // Wait for self.sleep_duration before sending any new data
1705                        let f = tokio::time::sleep(self.sleep_duration).boxed().shared();
1706                        self.state = PolingState::Sleep(f);
1707                        let input_batch = if let Some(batch) =
1708                            self.batches.clone().get(self.idx)
1709                        {
1710                            batch.clone()
1711                        } else if self.send_exit {
1712                            // Send None to signal end of data
1713                            return Poll::Ready(None);
1714                        } else {
1715                            // Go to sleep mode
1716                            let f =
1717                                tokio::time::sleep(self.sleep_duration).boxed().shared();
1718                            self.state = PolingState::Sleep(f);
1719                            continue;
1720                        };
1721                        self.idx += 1;
1722                        return Poll::Ready(Some(Ok(input_batch)));
1723                    }
1724                    PolingState::Sleep(future) => {
1725                        pin_mut!(future);
1726                        ready!(future.poll_unpin(cx));
1727                        self.state = PolingState::BatchReturn;
1728                    }
1729                }
1730            }
1731        }
1732    }
1733
1734    impl RecordBatchStream for TestStreamPartition {
1735        fn schema(&self) -> SchemaRef {
1736            Arc::clone(&self.schema)
1737        }
1738    }
1739
1740    fn bounded_window_exec_pb_latent_range(
1741        input: Arc<dyn ExecutionPlan>,
1742        n_future_range: usize,
1743        hash: &str,
1744        order_by: &str,
1745    ) -> Result<Arc<dyn ExecutionPlan>> {
1746        let schema = input.schema();
1747        let window_fn = WindowFunctionDefinition::AggregateUDF(count_udaf());
1748        let col_expr =
1749            Arc::new(Column::new(schema.fields[0].name(), 0)) as Arc<dyn PhysicalExpr>;
1750        let args = vec![col_expr];
1751        let partitionby_exprs = vec![col(hash, &schema)?];
1752        let orderby_exprs = vec![PhysicalSortExpr {
1753            expr: col(order_by, &schema)?,
1754            options: SortOptions::default(),
1755        }];
1756        let window_frame = WindowFrame::new_bounds(
1757            WindowFrameUnits::Range,
1758            WindowFrameBound::CurrentRow,
1759            WindowFrameBound::Following(ScalarValue::UInt64(Some(n_future_range as u64))),
1760        );
1761        let fn_name = format!(
1762            "{window_fn}({args:?}) PARTITION BY: [{partitionby_exprs:?}], ORDER BY: [{orderby_exprs:?}]"
1763        );
1764        let input_order_mode = InputOrderMode::Linear;
1765        Ok(Arc::new(BoundedWindowAggExec::try_new(
1766            vec![create_window_expr(
1767                &window_fn,
1768                fn_name,
1769                &args,
1770                &partitionby_exprs,
1771                &orderby_exprs,
1772                Arc::new(window_frame),
1773                input.schema(),
1774                false,
1775                false,
1776                None,
1777            )?],
1778            input,
1779            input_order_mode,
1780            true,
1781        )?))
1782    }
1783
1784    fn projection_exec(input: Arc<dyn ExecutionPlan>) -> Result<Arc<dyn ExecutionPlan>> {
1785        let schema = input.schema();
1786        let exprs = input
1787            .schema()
1788            .fields
1789            .iter()
1790            .enumerate()
1791            .map(|(idx, field)| {
1792                let name = if field.name().len() > 20 {
1793                    format!("col_{idx}")
1794                } else {
1795                    field.name().clone()
1796                };
1797                let expr = col(field.name(), &schema).unwrap();
1798                (expr, name)
1799            })
1800            .collect::<Vec<_>>();
1801        let proj_exprs: Vec<ProjectionExpr> = exprs
1802            .into_iter()
1803            .map(|(expr, alias)| ProjectionExpr { expr, alias })
1804            .collect();
1805        Ok(Arc::new(ProjectionExec::try_new(proj_exprs, input)?))
1806    }
1807
1808    fn task_context_helper() -> TaskContext {
1809        let task_ctx = TaskContext::default();
1810        // Create session context with config
1811        let session_config = SessionConfig::new()
1812            .with_batch_size(1)
1813            .with_target_partitions(2)
1814            .with_round_robin_repartition(false);
1815        task_ctx.with_session_config(session_config)
1816    }
1817
1818    fn task_context() -> Arc<TaskContext> {
1819        Arc::new(task_context_helper())
1820    }
1821
1822    pub async fn collect_stream(
1823        mut stream: SendableRecordBatchStream,
1824        results: &mut Vec<RecordBatch>,
1825    ) -> Result<()> {
1826        while let Some(item) = stream.next().await {
1827            results.push(item?);
1828        }
1829        Ok(())
1830    }
1831
1832    /// Execute the [ExecutionPlan] and collect the results in memory
1833    pub async fn collect_with_timeout(
1834        plan: Arc<dyn ExecutionPlan>,
1835        context: Arc<TaskContext>,
1836        timeout_duration: Duration,
1837    ) -> Result<Vec<RecordBatch>> {
1838        let stream = execute_stream(plan, context)?;
1839        let mut results = vec![];
1840
1841        // Execute the asynchronous operation with a timeout
1842        if timeout(timeout_duration, collect_stream(stream, &mut results))
1843            .await
1844            .is_ok()
1845        {
1846            return Err(exec_datafusion_err!("shouldn't have completed"));
1847        };
1848
1849        Ok(results)
1850    }
1851
1852    fn test_schema() -> SchemaRef {
1853        Arc::new(Schema::new(vec![
1854            Field::new("sn", DataType::UInt64, true),
1855            Field::new("hash", DataType::Int64, true),
1856        ]))
1857    }
1858
1859    fn schema_orders(schema: &SchemaRef) -> Result<Vec<LexOrdering>> {
1860        let orderings = vec![
1861            [PhysicalSortExpr {
1862                expr: col("sn", schema)?,
1863                options: SortOptions {
1864                    descending: false,
1865                    nulls_first: false,
1866                },
1867            }]
1868            .into(),
1869        ];
1870        Ok(orderings)
1871    }
1872
1873    fn is_integer_division_safe(lhs: usize, rhs: usize) -> bool {
1874        let res = lhs / rhs;
1875        res * rhs == lhs
1876    }
1877    fn generate_batches(
1878        schema: &SchemaRef,
1879        n_row: usize,
1880        n_chunk: usize,
1881    ) -> Result<Vec<RecordBatch>> {
1882        let mut batches = vec![];
1883        assert!(n_row > 0);
1884        assert!(n_chunk > 0);
1885        assert!(is_integer_division_safe(n_row, n_chunk));
1886        let hash_replicate = 4;
1887
1888        let chunks = (0..n_row)
1889            .chunks(n_chunk)
1890            .into_iter()
1891            .map(|elem| elem.into_iter().collect::<Vec<_>>())
1892            .collect::<Vec<_>>();
1893
1894        // Send 2 RecordBatches at the source
1895        for sn_values in chunks {
1896            let mut sn1_array = UInt64Builder::with_capacity(sn_values.len());
1897            let mut hash_array = Int64Builder::with_capacity(sn_values.len());
1898
1899            for sn in sn_values {
1900                sn1_array.append_value(sn as u64);
1901                let hash_value = (2 - (sn / hash_replicate)) as i64;
1902                hash_array.append_value(hash_value);
1903            }
1904
1905            let batch = RecordBatch::try_new(
1906                Arc::clone(schema),
1907                vec![Arc::new(sn1_array.finish()), Arc::new(hash_array.finish())],
1908            )?;
1909            batches.push(batch);
1910        }
1911        Ok(batches)
1912    }
1913
1914    fn generate_never_ending_source(
1915        n_rows: usize,
1916        chunk_length: usize,
1917        n_partition: usize,
1918        is_infinite: bool,
1919        send_exit: bool,
1920        per_batch_wait_duration_in_millis: u64,
1921    ) -> Result<Arc<dyn ExecutionPlan>> {
1922        assert!(n_partition > 0);
1923
1924        // We use same hash value in the table. This makes sure that
1925        // After hashing computation will continue in only in one of the output partitions
1926        // In this case, data flow should still continue
1927        let schema = test_schema();
1928        let orderings = schema_orders(&schema)?;
1929
1930        // Source waits per_batch_wait_duration_in_millis ms before sending other batch
1931        let per_batch_wait_duration =
1932            Duration::from_millis(per_batch_wait_duration_in_millis);
1933
1934        let batches = generate_batches(&schema, n_rows, chunk_length)?;
1935
1936        // Source has 2 partitions
1937        let partitions = vec![
1938            Arc::new(TestStreamPartition {
1939                schema: Arc::clone(&schema),
1940                batches,
1941                idx: 0,
1942                state: PolingState::BatchReturn,
1943                sleep_duration: per_batch_wait_duration,
1944                send_exit,
1945            }) as _;
1946            n_partition
1947        ];
1948        let source = Arc::new(StreamingTableExec::try_new(
1949            Arc::clone(&schema),
1950            partitions,
1951            None,
1952            orderings,
1953            is_infinite,
1954            None,
1955        )?) as _;
1956        Ok(source)
1957    }
1958
1959    // Tests NTH_VALUE(negative index) with memoize feature
1960    // To be able to trigger memoize feature for NTH_VALUE we need to
1961    // - feed BoundedWindowAggExec with batch stream data.
1962    // - Window frame should contain UNBOUNDED PRECEDING.
1963    // It hard to ensure these conditions are met, from the sql query.
1964    #[tokio::test]
1965    async fn test_window_nth_value_bounded_memoize() -> Result<()> {
1966        let config = SessionConfig::new().with_target_partitions(1);
1967        let task_ctx = Arc::new(TaskContext::default().with_session_config(config));
1968
1969        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1970        // Create a new batch of data to insert into the table
1971        let batch = RecordBatch::try_new(
1972            Arc::clone(&schema),
1973            vec![Arc::new(arrow::array::Int32Array::from(vec![1, 2, 3]))],
1974        )?;
1975
1976        let memory_exec = TestMemoryExec::try_new_exec(
1977            &[vec![batch.clone(), batch.clone(), batch.clone()]],
1978            Arc::clone(&schema),
1979            None,
1980        )?;
1981        let col_a = col("a", &schema)?;
1982        let nth_value_func1 = create_udwf_window_expr(
1983            &nth_value_udwf(),
1984            &[
1985                Arc::clone(&col_a),
1986                Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
1987            ],
1988            &schema,
1989            "nth_value(-1)".to_string(),
1990            false,
1991        )?
1992        .reverse_expr()
1993        .unwrap();
1994        let nth_value_func2 = create_udwf_window_expr(
1995            &nth_value_udwf(),
1996            &[
1997                Arc::clone(&col_a),
1998                Arc::new(Literal::new(ScalarValue::Int32(Some(2)))),
1999            ],
2000            &schema,
2001            "nth_value(-2)".to_string(),
2002            false,
2003        )?
2004        .reverse_expr()
2005        .unwrap();
2006
2007        let last_value_func = create_udwf_window_expr(
2008            &last_value_udwf(),
2009            &[Arc::clone(&col_a)],
2010            &schema,
2011            "last".to_string(),
2012            false,
2013        )?;
2014
2015        let window_exprs = vec![
2016            // LAST_VALUE(a)
2017            Arc::new(StandardWindowExpr::new(
2018                last_value_func,
2019                &[],
2020                &[],
2021                Arc::new(WindowFrame::new_bounds(
2022                    WindowFrameUnits::Rows,
2023                    WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
2024                    WindowFrameBound::CurrentRow,
2025                )),
2026            )) as _,
2027            // NTH_VALUE(a, -1)
2028            Arc::new(StandardWindowExpr::new(
2029                nth_value_func1,
2030                &[],
2031                &[],
2032                Arc::new(WindowFrame::new_bounds(
2033                    WindowFrameUnits::Rows,
2034                    WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
2035                    WindowFrameBound::CurrentRow,
2036                )),
2037            )) as _,
2038            // NTH_VALUE(a, -2)
2039            Arc::new(StandardWindowExpr::new(
2040                nth_value_func2,
2041                &[],
2042                &[],
2043                Arc::new(WindowFrame::new_bounds(
2044                    WindowFrameUnits::Rows,
2045                    WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
2046                    WindowFrameBound::CurrentRow,
2047                )),
2048            )) as _,
2049        ];
2050        let physical_plan = BoundedWindowAggExec::try_new(
2051            window_exprs,
2052            memory_exec,
2053            InputOrderMode::Sorted,
2054            true,
2055        )
2056        .map(|e| Arc::new(e) as Arc<dyn ExecutionPlan>)?;
2057
2058        let batches = collect(physical_plan.execute(0, task_ctx)?).await?;
2059
2060        // Get string representation of the plan
2061        assert_snapshot!(displayable(physical_plan.as_ref()).indent(true), @r#"
2062        BoundedWindowAggExec: wdw=[last: Field { "last": nullable Int32 }, frame: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, nth_value(-1): Field { "nth_value(-1)": nullable Int32 }, frame: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, nth_value(-2): Field { "nth_value(-2)": nullable Int32 }, frame: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW], mode=[Sorted]
2063          DataSourceExec: partitions=1, partition_sizes=[3]
2064        "#);
2065
2066        assert_snapshot!(batches_to_string(&batches), @r"
2067        +---+------+---------------+---------------+
2068        | a | last | nth_value(-1) | nth_value(-2) |
2069        +---+------+---------------+---------------+
2070        | 1 | 1    | 1             |               |
2071        | 2 | 2    | 2             | 1             |
2072        | 3 | 3    | 3             | 2             |
2073        | 1 | 1    | 1             | 3             |
2074        | 2 | 2    | 2             | 1             |
2075        | 3 | 3    | 3             | 2             |
2076        | 1 | 1    | 1             | 3             |
2077        | 2 | 2    | 2             | 1             |
2078        | 3 | 3    | 3             | 2             |
2079        +---+------+---------------+---------------+
2080        ");
2081        Ok(())
2082    }
2083
2084    // In `Linear` mode, a partition may receive no new rows for several
2085    // input batches while other partitions keep growing. Once all of a
2086    // partition's buffered rows have results, the evaluation sweep skips
2087    // it until it receives rows again, so this test drives a partition
2088    // through quiet batches and then resumes it: the results after the
2089    // gap must continue from the retained accumulator state. Both frames
2090    // are causal, so results finalize in the batch their row arrives in
2091    // and the quiet partition is fully calculated while it waits.
2092    #[tokio::test]
2093    async fn bounded_window_linear_quiet_partition_resume() -> Result<()> {
2094        let schema = Arc::new(Schema::new(vec![
2095            Field::new("pk", DataType::UInt64, false),
2096            Field::new("ts", DataType::UInt64, false),
2097        ]));
2098        let make_batch = |rows: &[(u64, u64)]| -> Result<RecordBatch> {
2099            let mut pk = UInt64Builder::with_capacity(rows.len());
2100            let mut ts = UInt64Builder::with_capacity(rows.len());
2101            for (p, t) in rows {
2102                pk.append_value(*p);
2103                ts.append_value(*t);
2104            }
2105            Ok(RecordBatch::try_new(
2106                Arc::clone(&schema),
2107                vec![Arc::new(pk.finish()), Arc::new(ts.finish())],
2108            )?)
2109        };
2110        // `ts` ascends globally; partition 0 is absent from the middle batches.
2111        let batches = vec![
2112            make_batch(&[(0, 0), (0, 1), (1, 2)])?,
2113            make_batch(&[(1, 3), (1, 4)])?,
2114            make_batch(&[(1, 5)])?,
2115            make_batch(&[(0, 6), (1, 7)])?,
2116        ];
2117        let memory_exec =
2118            TestMemoryExec::try_new_exec(&[batches], Arc::clone(&schema), None)?;
2119
2120        let partition_by = vec![col("pk", &schema)?];
2121        let order_by = [PhysicalSortExpr {
2122            expr: col("ts", &schema)?,
2123            options: SortOptions::default(),
2124        }];
2125        // A running COUNT (plain aggregate) and a SUM over the previous and
2126        // current row (sliding aggregate).
2127        let count_expr = create_window_expr(
2128            &WindowFunctionDefinition::AggregateUDF(count_udaf()),
2129            "count".to_string(),
2130            &[col("ts", &schema)?],
2131            &partition_by,
2132            &order_by,
2133            Arc::new(WindowFrame::new_bounds(
2134                WindowFrameUnits::Rows,
2135                WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
2136                WindowFrameBound::CurrentRow,
2137            )),
2138            Arc::clone(&schema),
2139            false,
2140            false,
2141            None,
2142        )?;
2143        let sum_expr = create_window_expr(
2144            &WindowFunctionDefinition::AggregateUDF(sum_udaf()),
2145            "sum".to_string(),
2146            &[col("ts", &schema)?],
2147            &partition_by,
2148            &order_by,
2149            Arc::new(WindowFrame::new_bounds(
2150                WindowFrameUnits::Rows,
2151                WindowFrameBound::Preceding(ScalarValue::UInt64(Some(1))),
2152                WindowFrameBound::CurrentRow,
2153            )),
2154            Arc::clone(&schema),
2155            false,
2156            false,
2157            None,
2158        )?;
2159        let physical_plan = BoundedWindowAggExec::try_new(
2160            vec![count_expr, sum_expr],
2161            memory_exec,
2162            InputOrderMode::Linear,
2163            true,
2164        )
2165        .map(|e| Arc::new(e) as Arc<dyn ExecutionPlan>)?;
2166
2167        let batches = collect(physical_plan.execute(0, task_context())?).await?;
2168
2169        assert_snapshot!(batches_to_string(&batches), @r"
2170        +----+----+-------+-----+
2171        | pk | ts | count | sum |
2172        +----+----+-------+-----+
2173        | 0  | 0  | 1     | 0   |
2174        | 0  | 1  | 2     | 1   |
2175        | 1  | 2  | 1     | 2   |
2176        | 1  | 3  | 2     | 5   |
2177        | 1  | 4  | 3     | 7   |
2178        | 1  | 5  | 4     | 9   |
2179        | 0  | 6  | 3     | 7   |
2180        | 1  | 7  | 5     | 12  |
2181        +----+----+-------+-----+
2182        ");
2183        Ok(())
2184    }
2185
2186    // This test, tests whether most recent row guarantee by the input batch of the `BoundedWindowAggExec`
2187    // helps `BoundedWindowAggExec` to generate low latency result in the `Linear` mode.
2188    // Input data generated at the source is
2189    //       "+----+------+",
2190    //       "| sn | hash |",
2191    //       "+----+------+",
2192    //       "| 0  | 2    |",
2193    //       "| 1  | 2    |",
2194    //       "| 2  | 2    |",
2195    //       "| 3  | 2    |",
2196    //       "| 4  | 1    |",
2197    //       "| 5  | 1    |",
2198    //       "| 6  | 1    |",
2199    //       "| 7  | 1    |",
2200    //       "| 8  | 0    |",
2201    //       "| 9  | 0    |",
2202    //       "+----+------+",
2203    //
2204    // Effectively following query is run on this data
2205    //
2206    //   SELECT *, count(*) OVER(PARTITION BY duplicated_hash ORDER BY sn RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING)
2207    //   FROM test;
2208    //
2209    // partition `duplicated_hash=2` receives following data from the input
2210    //
2211    //       "+----+------+",
2212    //       "| sn | hash |",
2213    //       "+----+------+",
2214    //       "| 0  | 2    |",
2215    //       "| 1  | 2    |",
2216    //       "| 2  | 2    |",
2217    //       "| 3  | 2    |",
2218    //       "+----+------+",
2219    // normally `BoundedWindowExec` can only generate following result from the input above
2220    //
2221    //       "+----+------+---------+",
2222    //       "| sn | hash |  count  |",
2223    //       "+----+------+---------+",
2224    //       "| 0  | 2    |  2      |",
2225    //       "| 1  | 2    |  2      |",
2226    //       "| 2  | 2    |<not yet>|",
2227    //       "| 3  | 2    |<not yet>|",
2228    //       "+----+------+---------+",
2229    // where result of last 2 row is missing. Since window frame end is not may change with future data
2230    // since window frame end is determined by 1 following (To generate result for row=3[where sn=2] we
2231    // need to received sn=4 to make sure window frame end bound won't change with future data).
2232    //
2233    // With the ability of different partitions to use global ordering at the input (where most up-to date
2234    //   row is
2235    //      "| 9  | 0    |",
2236    //   )
2237    //
2238    // `BoundedWindowExec` should be able to generate following result in the test
2239    //
2240    //       "+----+------+-------+",
2241    //       "| sn | hash | col_2 |",
2242    //       "+----+------+-------+",
2243    //       "| 0  | 2    | 2     |",
2244    //       "| 1  | 2    | 2     |",
2245    //       "| 2  | 2    | 2     |",
2246    //       "| 3  | 2    | 1     |",
2247    //       "| 4  | 1    | 2     |",
2248    //       "| 5  | 1    | 2     |",
2249    //       "| 6  | 1    | 2     |",
2250    //       "| 7  | 1    | 1     |",
2251    //       "+----+------+-------+",
2252    //
2253    // where result for all rows except last 2 is calculated (To calculate result for row 9 where sn=8
2254    //   we need to receive sn=10 value to calculate it result.).
2255    // In this test, out aim is to test for which portion of the input data `BoundedWindowExec` can generate
2256    // a result. To test this behaviour, we generated the data at the source infinitely (no `None` signal
2257    //    is sent to output from source). After, row:
2258    //
2259    //       "| 9  | 0    |",
2260    //
2261    // is sent. Source stops sending data to output. We collect, result emitted by the `BoundedWindowExec` at the
2262    // end of the pipeline with a timeout (Since no `None` is sent from source. Collection never ends otherwise).
2263    #[tokio::test]
2264    async fn bounded_window_exec_linear_mode_range_information() -> Result<()> {
2265        let n_rows = 10;
2266        let chunk_length = 2;
2267        let n_future_range = 1;
2268
2269        let timeout_duration = Duration::from_millis(2000);
2270
2271        let source =
2272            generate_never_ending_source(n_rows, chunk_length, 1, true, false, 5)?;
2273
2274        let window =
2275            bounded_window_exec_pb_latent_range(source, n_future_range, "hash", "sn")?;
2276
2277        let plan = projection_exec(window)?;
2278
2279        // Get string representation of the plan
2280        assert_snapshot!(displayable(plan.as_ref()).indent(true), @r#"
2281        ProjectionExec: expr=[sn@0 as sn, hash@1 as hash, count([Column { name: "sn", index: 0 }]) PARTITION BY: [[Column { name: "hash", index: 1 }]], ORDER BY: [[PhysicalSortExpr { expr: Column { name: "sn", index: 0 }, options: SortOptions { descending: false, nulls_first: true } }]]@2 as col_2]
2282          BoundedWindowAggExec: wdw=[count([Column { name: "sn", index: 0 }]) PARTITION BY: [[Column { name: "hash", index: 1 }]], ORDER BY: [[PhysicalSortExpr { expr: Column { name: "sn", index: 0 }, options: SortOptions { descending: false, nulls_first: true } }]]: Field { "count([Column { name: \"sn\", index: 0 }]) PARTITION BY: [[Column { name: \"hash\", index: 1 }]], ORDER BY: [[PhysicalSortExpr { expr: Column { name: \"sn\", index: 0 }, options: SortOptions { descending: false, nulls_first: true } }]]": Int64 }, frame: RANGE BETWEEN CURRENT ROW AND 1 FOLLOWING], mode=[Linear]
2283            StreamingTableExec: partition_sizes=1, projection=[sn, hash], infinite_source=true, output_ordering=[sn@0 ASC NULLS LAST]
2284        "#);
2285
2286        let task_ctx = task_context();
2287        let batches = collect_with_timeout(plan, task_ctx, timeout_duration).await?;
2288
2289        assert_snapshot!(batches_to_string(&batches), @r"
2290        +----+------+-------+
2291        | sn | hash | col_2 |
2292        +----+------+-------+
2293        | 0  | 2    | 2     |
2294        | 1  | 2    | 2     |
2295        | 2  | 2    | 2     |
2296        | 3  | 2    | 1     |
2297        | 4  | 1    | 2     |
2298        | 5  | 1    | 2     |
2299        | 6  | 1    | 2     |
2300        | 7  | 1    | 1     |
2301        +----+------+-------+
2302        ");
2303
2304        Ok(())
2305    }
2306
2307    type Observation = (usize, PartitionKey, Vec<ScalarValue>);
2308
2309    /// Test [`WindowStateObserver`] that records every callback into a shared
2310    /// `Vec` for later assertion.
2311    struct RecordingObserver {
2312        sink: Arc<std::sync::Mutex<Vec<Observation>>>,
2313    }
2314
2315    impl WindowStateObserver for RecordingObserver {
2316        fn finalize_window_aggregate(
2317            &self,
2318            partition_idx: usize,
2319            _window_expr: &Arc<dyn WindowExpr>,
2320            partition_key: &PartitionKey,
2321            state: Vec<ScalarValue>,
2322        ) -> Result<()> {
2323            self.sink
2324                .lock()
2325                .unwrap()
2326                .push((partition_idx, partition_key.clone(), state));
2327            Ok(())
2328        }
2329    }
2330
2331    /// Build a `BoundedWindowAggExec` for `count(sn) OVER (PARTITION BY hash
2332    /// ORDER BY sn <frame>)` over a fixed two-group source (hash=1 × 3,
2333    /// hash=2 × 3, sorted by (hash, sn)). Returns the plan pre-observer so
2334    /// callers can decide how to install it.
2335    fn build_partition_close_plan(frame: WindowFrame) -> Result<BoundedWindowAggExec> {
2336        let schema = test_schema();
2337
2338        let mut sn_b = UInt64Builder::with_capacity(6);
2339        let mut hash_b = Int64Builder::with_capacity(6);
2340        for (sn, hash) in [(1u64, 1i64), (2, 1), (3, 1), (4, 2), (5, 2), (6, 2)] {
2341            sn_b.append_value(sn);
2342            hash_b.append_value(hash);
2343        }
2344        let batch = RecordBatch::try_new(
2345            Arc::clone(&schema),
2346            vec![Arc::new(sn_b.finish()), Arc::new(hash_b.finish())],
2347        )?;
2348        let ordering: LexOrdering = [
2349            PhysicalSortExpr {
2350                expr: col("hash", &schema)?,
2351                options: SortOptions::default(),
2352            },
2353            PhysicalSortExpr {
2354                expr: col("sn", &schema)?,
2355                options: SortOptions::default(),
2356            },
2357        ]
2358        .into();
2359        let source_raw =
2360            TestMemoryExec::try_new(&[vec![batch]], Arc::clone(&schema), None)?
2361                .try_with_sort_information(vec![ordering])?;
2362        let source: Arc<dyn ExecutionPlan> =
2363            Arc::new(TestMemoryExec::update_cache(&Arc::new(source_raw)));
2364
2365        let expr = create_window_expr(
2366            &WindowFunctionDefinition::AggregateUDF(count_udaf()),
2367            "cnt".to_string(),
2368            &[col("sn", &schema)?],
2369            &[col("hash", &schema)?],
2370            &[PhysicalSortExpr {
2371                expr: col("sn", &schema)?,
2372                options: SortOptions::default(),
2373            }],
2374            Arc::new(frame),
2375            source.schema(),
2376            false,
2377            false,
2378            None,
2379        )?;
2380
2381        BoundedWindowAggExec::try_new(vec![expr], source, InputOrderMode::Sorted, false)
2382    }
2383
2384    // Two PARTITION BY groups: hash=1 [sn=1,2,3] then hash=2 [sn=4,5,6].
2385    // Input is sorted by (hash, sn) so we can run in Sorted mode; in that
2386    // mode `mark_partition_end` closes the leading group mid-stream and
2387    // EOS closes the tail — both fire the observer for an ever-expanding
2388    // frame. Sliding frames are rejected at install time.
2389
2390    #[tokio::test]
2391    async fn test_state_observer_rejects_sliding_frame() -> Result<()> {
2392        // `CURRENT ROW → UNBOUNDED FOLLOWING` is not ever-expanding, so this
2393        // maps to `SlidingAggregateWindowExpr` whose accumulator retracts as
2394        // rows leave the frame — at partition close the accumulator holds
2395        // only the last frame's rows, not the partition aggregate.
2396        // `with_state_observer` refuses this configuration.
2397        use std::sync::Mutex;
2398
2399        let plan = build_partition_close_plan(WindowFrame::new_bounds(
2400            WindowFrameUnits::Rows,
2401            WindowFrameBound::CurrentRow,
2402            WindowFrameBound::Following(ScalarValue::UInt64(None)),
2403        ))?;
2404        let observer: Arc<dyn WindowStateObserver> = Arc::new(RecordingObserver {
2405            sink: Arc::new(Mutex::new(vec![])),
2406        });
2407        let err = plan.with_state_observer(Some(observer)).unwrap_err();
2408        let msg = err.to_string();
2409        assert!(
2410            msg.contains("sliding aggregate window frame"),
2411            "expected sliding-frame rejection, got: {msg}"
2412        );
2413        Ok(())
2414    }
2415
2416    #[tokio::test]
2417    async fn test_finalized_state_observer_fires_on_causal_frame() -> Result<()> {
2418        // `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` — ever-expanding,
2419        // `PlainAggregateWindowExpr` under the hood. At partition close the
2420        // accumulator holds the partition aggregate. Both mid-stream close
2421        // (hash=1 as hash=2 rows arrive) and EOS (hash=2 at drain) fire.
2422        use std::sync::Mutex;
2423
2424        let task_ctx = Arc::new(TaskContext::default());
2425        let plan = build_partition_close_plan(WindowFrame::new_bounds(
2426            WindowFrameUnits::Rows,
2427            WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
2428            WindowFrameBound::CurrentRow,
2429        ))?;
2430
2431        let observations: Arc<Mutex<Vec<Observation>>> = Arc::new(Mutex::new(vec![]));
2432        let observer: Arc<dyn WindowStateObserver> = Arc::new(RecordingObserver {
2433            sink: Arc::clone(&observations),
2434        });
2435        let plan = plan.with_state_observer(Some(observer))?;
2436
2437        let _ = collect(Arc::new(plan).execute(0, task_ctx)?).await?;
2438
2439        // count(sn) over each of hash=1 (3 rows) and hash=2 (3 rows), in
2440        // close order — hash=1 first (mid-stream close), hash=2 second (EOS).
2441        let observed: Vec<(usize, i64, Vec<ScalarValue>)> = observations
2442            .lock()
2443            .unwrap()
2444            .iter()
2445            .map(|(idx, key, state)| {
2446                let hash = match &key[0] {
2447                    ScalarValue::Int64(Some(v)) => *v,
2448                    other => panic!("unexpected partition-key element: {other:?}"),
2449                };
2450                (*idx, hash, state.clone())
2451            })
2452            .collect();
2453        assert_eq!(
2454            observed,
2455            vec![
2456                (0, 1, vec![ScalarValue::Int64(Some(3))]),
2457                (0, 2, vec![ScalarValue::Int64(Some(3))]),
2458            ]
2459        );
2460        Ok(())
2461    }
2462
2463    #[tokio::test]
2464    async fn test_finalized_state_observer_fires_exactly_once_across_batches()
2465    -> Result<()> {
2466        // Regression guard for the exactly-once observer contract when
2467        // partition close and pruning happen on different `compute_aggregates`
2468        // calls.
2469        //
2470        // The observer fires from `publish_finalized_states`, called at the
2471        // top of every `compute_aggregates`. Entries are only cleared by
2472        // `prune_state`, which runs only when `calculate_out_columns` returns
2473        // `Some`. Nothing in the type system ties the two together, so a
2474        // group whose state was published on batch N must not be re-published
2475        // on batch N+1 or at EOS.
2476        //
2477        // Layout: three PARTITION BY groups streamed across two input
2478        // batches, so each group closes on a distinct `compute_aggregates`
2479        // call:
2480        //   batch 1 = [hash=1 × 2]                — no close (single group).
2481        //   batch 2 = [hash=2 × 2, hash=3 × 2]    — `mark_partition_end`
2482        //                                            closes hash=1 and hash=2.
2483        //   EOS                                    — closes hash=3.
2484        //
2485        // Assertion: each key appears exactly once across all observations.
2486        use std::sync::Mutex;
2487
2488        let task_ctx = Arc::new(TaskContext::default());
2489        let schema = test_schema();
2490
2491        // Two batches, same output partition.
2492        let make_batch = |rows: &[(u64, i64)]| -> Result<RecordBatch> {
2493            let mut sn_b = UInt64Builder::with_capacity(rows.len());
2494            let mut hash_b = Int64Builder::with_capacity(rows.len());
2495            for &(sn, hash) in rows {
2496                sn_b.append_value(sn);
2497                hash_b.append_value(hash);
2498            }
2499            Ok(RecordBatch::try_new(
2500                Arc::clone(&schema),
2501                vec![Arc::new(sn_b.finish()), Arc::new(hash_b.finish())],
2502            )?)
2503        };
2504        let batch1 = make_batch(&[(1, 1), (2, 1)])?;
2505        let batch2 = make_batch(&[(3, 2), (4, 2), (5, 3), (6, 3)])?;
2506
2507        let ordering: LexOrdering = [
2508            PhysicalSortExpr {
2509                expr: col("hash", &schema)?,
2510                options: SortOptions::default(),
2511            },
2512            PhysicalSortExpr {
2513                expr: col("sn", &schema)?,
2514                options: SortOptions::default(),
2515            },
2516        ]
2517        .into();
2518        let source_raw =
2519            TestMemoryExec::try_new(&[vec![batch1, batch2]], Arc::clone(&schema), None)?
2520                .try_with_sort_information(vec![ordering])?;
2521        let source: Arc<dyn ExecutionPlan> =
2522            Arc::new(TestMemoryExec::update_cache(&Arc::new(source_raw)));
2523
2524        let expr = create_window_expr(
2525            &WindowFunctionDefinition::AggregateUDF(count_udaf()),
2526            "cnt".to_string(),
2527            &[col("sn", &schema)?],
2528            &[col("hash", &schema)?],
2529            &[PhysicalSortExpr {
2530                expr: col("sn", &schema)?,
2531                options: SortOptions::default(),
2532            }],
2533            Arc::new(WindowFrame::new_bounds(
2534                WindowFrameUnits::Rows,
2535                WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
2536                WindowFrameBound::CurrentRow,
2537            )),
2538            source.schema(),
2539            false,
2540            false,
2541            None,
2542        )?;
2543
2544        let observations: Arc<Mutex<Vec<Observation>>> = Arc::new(Mutex::new(vec![]));
2545        let observer: Arc<dyn WindowStateObserver> = Arc::new(RecordingObserver {
2546            sink: Arc::clone(&observations),
2547        });
2548
2549        let plan = BoundedWindowAggExec::try_new(
2550            vec![expr],
2551            source,
2552            InputOrderMode::Sorted,
2553            false,
2554        )?
2555        .with_state_observer(Some(observer))?;
2556
2557        let _ = collect(Arc::new(plan).execute(0, task_ctx)?).await?;
2558
2559        let fired: Vec<i64> = observations
2560            .lock()
2561            .unwrap()
2562            .iter()
2563            .map(|(_, key, _)| match &key[0] {
2564                ScalarValue::Int64(Some(v)) => *v,
2565                other => panic!("unexpected partition-key element: {other:?}"),
2566            })
2567            .collect();
2568        // Each group closes on a distinct `compute_aggregates` call — hash=1
2569        // and hash=2 on batch 2's `mark_partition_end`, hash=3 at EOS — and
2570        // each appears exactly once, in close order.
2571        assert_eq!(fired, vec![1, 2, 3]);
2572        Ok(())
2573    }
2574
2575    /// Run one task's local BWAG for `SUM(sn) OVER (ORDER BY sn ROWS
2576    /// UNBOUNDED PRECEDING TO CURRENT ROW)` with no PARTITION BY, over
2577    /// `input` sorted ascending. Returns the per-row output values and the
2578    /// observed finalized state total (which the caller uses as a carry-in
2579    /// for the next task).
2580    async fn run_running_sum_task(
2581        input: &[u64],
2582        task_ctx: Arc<TaskContext>,
2583    ) -> Result<(Vec<u64>, u64)> {
2584        use arrow::array::UInt64Array;
2585        use datafusion_functions_aggregate::sum::sum_udaf;
2586        use std::sync::Mutex;
2587
2588        /// Observer for `run_running_sum_task`: captures the single running
2589        /// SUM total published at EOS. Asserts exactly-one fire and rejects
2590        /// non-empty partition keys (this helper is no-PARTITION-BY only).
2591        struct RunningSumObserver {
2592            sink: Arc<Mutex<Option<u64>>>,
2593        }
2594
2595        impl WindowStateObserver for RunningSumObserver {
2596            fn finalize_window_aggregate(
2597                &self,
2598                _partition_idx: usize,
2599                _window_expr: &Arc<dyn WindowExpr>,
2600                partition_key: &PartitionKey,
2601                state: Vec<ScalarValue>,
2602            ) -> Result<()> {
2603                assert!(
2604                    partition_key.is_empty(),
2605                    "empty PartitionKey for no-PARTITION-BY plan"
2606                );
2607                let total = match &state[0] {
2608                    ScalarValue::UInt64(Some(v)) => *v,
2609                    ScalarValue::Int64(Some(v)) => *v as u64,
2610                    other => panic!("unexpected sum state element: {other:?}"),
2611                };
2612                let prev = self.sink.lock().unwrap().replace(total);
2613                assert!(prev.is_none(), "observer must fire exactly once per task");
2614                Ok(())
2615            }
2616        }
2617
2618        let schema = test_schema();
2619        let mut sn_b = UInt64Builder::with_capacity(input.len());
2620        let mut hash_b = Int64Builder::with_capacity(input.len());
2621        for &sn in input {
2622            sn_b.append_value(sn);
2623            hash_b.append_value(0);
2624        }
2625        let batch = RecordBatch::try_new(
2626            Arc::clone(&schema),
2627            vec![Arc::new(sn_b.finish()), Arc::new(hash_b.finish())],
2628        )?;
2629        let ordering: LexOrdering = [PhysicalSortExpr {
2630            expr: col("sn", &schema)?,
2631            options: SortOptions::default(),
2632        }]
2633        .into();
2634        let source_raw =
2635            TestMemoryExec::try_new(&[vec![batch]], Arc::clone(&schema), None)?
2636                .try_with_sort_information(vec![ordering])?;
2637        let source: Arc<dyn ExecutionPlan> =
2638            Arc::new(TestMemoryExec::update_cache(&Arc::new(source_raw)));
2639
2640        let window_fn = WindowFunctionDefinition::AggregateUDF(sum_udaf());
2641        let args = vec![col("sn", &schema)?];
2642        let partition_by: Vec<Arc<dyn PhysicalExpr>> = vec![];
2643        let order_by = vec![PhysicalSortExpr {
2644            expr: col("sn", &schema)?,
2645            options: SortOptions::default(),
2646        }];
2647        let frame = WindowFrame::new_bounds(
2648            WindowFrameUnits::Rows,
2649            WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
2650            WindowFrameBound::CurrentRow,
2651        );
2652        let expr = create_window_expr(
2653            &window_fn,
2654            "running_sum".to_string(),
2655            &args,
2656            &partition_by,
2657            &order_by,
2658            Arc::new(frame),
2659            source.schema(),
2660            false,
2661            false,
2662            None,
2663        )?;
2664
2665        let total_sink: Arc<Mutex<Option<u64>>> = Arc::new(Mutex::new(None));
2666        let observer: Arc<dyn WindowStateObserver> = Arc::new(RunningSumObserver {
2667            sink: Arc::clone(&total_sink),
2668        });
2669
2670        let plan = BoundedWindowAggExec::try_new(
2671            vec![expr],
2672            source,
2673            InputOrderMode::Sorted,
2674            false,
2675        )?
2676        .with_state_observer(Some(observer))?;
2677        let batches = collect(Arc::new(plan).execute(0, task_ctx)?).await?;
2678
2679        let mut out = Vec::with_capacity(input.len());
2680        for batch in &batches {
2681            let col = batch
2682                .column_by_name("running_sum")
2683                .expect("running_sum column present");
2684            let arr = col
2685                .as_any()
2686                .downcast_ref::<UInt64Array>()
2687                .expect("SUM(UInt64) → UInt64Array");
2688            for i in 0..arr.len() {
2689                out.push(arr.value(i));
2690            }
2691        }
2692        let total = total_sink
2693            .lock()
2694            .unwrap()
2695            .expect("observer must have fired at EOS");
2696        Ok((out, total))
2697    }
2698
2699    /// Run one task's local BWAG for `approx_distinct(sn) OVER (ORDER BY sn
2700    /// ROWS UNBOUNDED PRECEDING TO CURRENT ROW)` with no PARTITION BY, and
2701    /// return the single EOS-observed [`Accumulator::state`] Vec.
2702    async fn run_approx_distinct_task(
2703        input: &[u64],
2704        task_ctx: Arc<TaskContext>,
2705    ) -> Result<Vec<ScalarValue>> {
2706        use datafusion_functions_aggregate::approx_distinct::approx_distinct_udaf;
2707        use std::sync::Mutex;
2708
2709        /// Observer for `run_approx_distinct_task`: capture the single EOS
2710        /// state. Asserts exactly-one fire and rejects non-empty partition
2711        /// keys (helper is no-PARTITION-BY only).
2712        struct ApproxDistinctObserver {
2713            sink: Arc<Mutex<Option<Vec<ScalarValue>>>>,
2714        }
2715
2716        impl WindowStateObserver for ApproxDistinctObserver {
2717            fn finalize_window_aggregate(
2718                &self,
2719                _partition_idx: usize,
2720                _window_expr: &Arc<dyn WindowExpr>,
2721                partition_key: &PartitionKey,
2722                state: Vec<ScalarValue>,
2723            ) -> Result<()> {
2724                assert!(
2725                    partition_key.is_empty(),
2726                    "empty PartitionKey for no-PARTITION-BY plan"
2727                );
2728                let prev = self.sink.lock().unwrap().replace(state);
2729                assert!(prev.is_none(), "observer must fire exactly once per task");
2730                Ok(())
2731            }
2732        }
2733
2734        let schema = test_schema();
2735        let mut sn_b = UInt64Builder::with_capacity(input.len());
2736        let mut hash_b = Int64Builder::with_capacity(input.len());
2737        for &sn in input {
2738            sn_b.append_value(sn);
2739            hash_b.append_value(0);
2740        }
2741        let batch = RecordBatch::try_new(
2742            Arc::clone(&schema),
2743            vec![Arc::new(sn_b.finish()), Arc::new(hash_b.finish())],
2744        )?;
2745        let ordering: LexOrdering = [PhysicalSortExpr {
2746            expr: col("sn", &schema)?,
2747            options: SortOptions::default(),
2748        }]
2749        .into();
2750        let source_raw =
2751            TestMemoryExec::try_new(&[vec![batch]], Arc::clone(&schema), None)?
2752                .try_with_sort_information(vec![ordering])?;
2753        let source: Arc<dyn ExecutionPlan> =
2754            Arc::new(TestMemoryExec::update_cache(&Arc::new(source_raw)));
2755
2756        let expr = create_window_expr(
2757            &WindowFunctionDefinition::AggregateUDF(approx_distinct_udaf()),
2758            "approx_distinct_sn".to_string(),
2759            &[col("sn", &schema)?],
2760            &[],
2761            &[PhysicalSortExpr {
2762                expr: col("sn", &schema)?,
2763                options: SortOptions::default(),
2764            }],
2765            Arc::new(WindowFrame::new_bounds(
2766                WindowFrameUnits::Rows,
2767                WindowFrameBound::Preceding(ScalarValue::UInt64(None)),
2768                WindowFrameBound::CurrentRow,
2769            )),
2770            source.schema(),
2771            false,
2772            false,
2773            None,
2774        )?;
2775
2776        let state_sink: Arc<Mutex<Option<Vec<ScalarValue>>>> = Arc::new(Mutex::new(None));
2777        let observer: Arc<dyn WindowStateObserver> = Arc::new(ApproxDistinctObserver {
2778            sink: Arc::clone(&state_sink),
2779        });
2780
2781        let plan = BoundedWindowAggExec::try_new(
2782            vec![expr],
2783            source,
2784            InputOrderMode::Sorted,
2785            false,
2786        )?
2787        .with_state_observer(Some(observer))?;
2788        let _ = collect(Arc::new(plan).execute(0, task_ctx)?).await?;
2789
2790        state_sink
2791            .lock()
2792            .unwrap()
2793            .take()
2794            .ok_or_else(|| exec_datafusion_err!("observer never fired"))
2795    }
2796
2797    #[tokio::test]
2798    async fn test_prefix_scan_across_tasks_matches_single_bwag() -> Result<()> {
2799        // Demonstrates the parallel-window shape reviewers asked about:
2800        // range-shuffle `SUM(sn) OVER (ORDER BY sn UNBOUNDED PRECEDING TO
2801        // CURRENT ROW)` across two tasks, then prefix-scan each task's
2802        // finalized state (from the observer) to carry-in the next task's
2803        // rows. Result must match a single BWAG over the concatenated input.
2804        let task_ctx = Arc::new(TaskContext::default());
2805
2806        // Two tasks under range partition on sn:
2807        let (task1_out, task1_total) =
2808            run_running_sum_task(&[1, 1, 2, 2, 3, 3, 4, 4], Arc::clone(&task_ctx))
2809                .await?;
2810        let (task2_out, task2_total) =
2811            run_running_sum_task(&[5, 5, 6, 6, 7, 7, 8, 8], Arc::clone(&task_ctx))
2812                .await?;
2813
2814        // Local (uncorrected) outputs and totals — first pass.
2815        assert_eq!(task1_out, vec![1, 2, 4, 6, 9, 12, 16, 20]);
2816        assert_eq!(task1_total, 20);
2817        assert_eq!(task2_out, vec![5, 10, 16, 22, 29, 36, 44, 52]);
2818        assert_eq!(task2_total, 52);
2819
2820        // Prefix scan over per-task totals → carry-in for each task. Task 0's
2821        // carry-in is 0; task N's carry-in is the sum of tasks [0, N).
2822        let carry_ins = [0u64, task1_total];
2823
2824        // Second pass: shift each task's local values by its carry-in.
2825        let task1_final: Vec<u64> = task1_out.iter().map(|v| v + carry_ins[0]).collect();
2826        let task2_final: Vec<u64> = task2_out.iter().map(|v| v + carry_ins[1]).collect();
2827        let parallel_result: Vec<u64> = task1_final
2828            .iter()
2829            .chain(task2_final.iter())
2830            .copied()
2831            .collect();
2832
2833        // Oracle: single BWAG over the full concatenated input.
2834        let (single_result, single_total) = run_running_sum_task(
2835            &[1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8],
2836            task_ctx,
2837        )
2838        .await?;
2839
2840        assert_eq!(
2841            parallel_result, single_result,
2842            "two-task prefix-scan must match single-BWAG oracle"
2843        );
2844        // And matches the sequence in the design discussion.
2845        assert_eq!(
2846            single_result,
2847            vec![1, 2, 4, 6, 9, 12, 16, 20, 25, 30, 36, 42, 49, 56, 64, 72]
2848        );
2849        assert_eq!(single_total, 72);
2850        Ok(())
2851    }
2852
2853    #[tokio::test]
2854    async fn test_prefix_merge_across_tasks_approx_distinct() -> Result<()> {
2855        // Load-bearing contract for the parallel-window use case: the state
2856        // exposed by `WindowStateObserver::finalize_window_aggregate` must be
2857        // compatible with `Accumulator::merge_batch` on a fresh accumulator
2858        // of the same UDAF. This is what allows non-decomposable aggregates
2859        // like `approx_distinct` (HLL sketch state) to be prefix-merged
2860        // across shard tasks — the reason we exposed accumulator state at
2861        // all. If this ever breaks, downstream parallel-window work has to
2862        // wait for a public API change.
2863        use arrow::array::{ArrayRef, BinaryArray};
2864        use arrow::datatypes::FieldRef;
2865        use datafusion_expr::function::AccumulatorArgs;
2866        use datafusion_functions_aggregate::approx_distinct::approx_distinct_udaf;
2867
2868        let task_ctx = Arc::new(TaskContext::default());
2869
2870        // Two tasks with overlapping inputs; concatenated distinct universe
2871        // is {1,2,3,4,5}.
2872        let state1 =
2873            run_approx_distinct_task(&[1, 1, 2, 3], Arc::clone(&task_ctx)).await?;
2874        let state2 = run_approx_distinct_task(&[3, 4, 5], Arc::clone(&task_ctx)).await?;
2875        let state_single =
2876            run_approx_distinct_task(&[1, 1, 2, 3, 3, 4, 5], Arc::clone(&task_ctx))
2877                .await?;
2878
2879        // approx_distinct state is a single serialized-HLL Binary field.
2880        assert_eq!(state1.len(), 1, "single state field");
2881        assert_eq!(state2.len(), 1, "single state field");
2882        assert_eq!(state_single.len(), 1, "single state field");
2883
2884        // Seed a fresh accumulator with the given serialized HLL states via
2885        // `merge_batch` and return its distinct-count evaluation.
2886        fn evaluate_merged(states: &[&ScalarValue]) -> Result<ScalarValue> {
2887            let udaf = approx_distinct_udaf();
2888            let input_schema =
2889                Arc::new(Schema::new(vec![Field::new("sn", DataType::UInt64, true)]));
2890            let return_field: FieldRef =
2891                Arc::new(Field::new("approx_distinct_sn", DataType::UInt64, true));
2892            let expr_field: FieldRef = Arc::new(Field::new("sn", DataType::UInt64, true));
2893            let physical_col: Arc<dyn PhysicalExpr> = col("sn", &input_schema)?;
2894            let args = AccumulatorArgs {
2895                return_field: Arc::clone(&return_field),
2896                schema: &input_schema,
2897                ignore_nulls: false,
2898                order_bys: &[],
2899                is_reversed: false,
2900                name: "approx_distinct",
2901                is_distinct: false,
2902                exprs: std::slice::from_ref(&physical_col),
2903                expr_fields: std::slice::from_ref(&expr_field),
2904            };
2905            let mut acc = udaf.accumulator(args)?;
2906            let byte_slices: Vec<&[u8]> = states
2907                .iter()
2908                .map(|s| match s {
2909                    ScalarValue::Binary(Some(v)) => v.as_slice(),
2910                    other => panic!("expected Binary state, got {other:?}"),
2911                })
2912                .collect();
2913            let bin: ArrayRef = Arc::new(BinaryArray::from_iter_values(byte_slices));
2914            acc.merge_batch(std::slice::from_ref(&bin))?;
2915            acc.evaluate()
2916        }
2917
2918        let merged = evaluate_merged(&[&state1[0], &state2[0]])?;
2919        let oracle = evaluate_merged(&[&state_single[0]])?;
2920
2921        assert_eq!(
2922            merged, oracle,
2923            "merged task states must match single-BWAG oracle — parallel prefix-merge contract"
2924        );
2925        // HLL is approximate but exact for a 5-element universe.
2926        assert_eq!(merged, ScalarValue::UInt64(Some(5)));
2927        Ok(())
2928    }
2929
2930    #[test]
2931    fn test_bounded_window_agg_cardinality_effect() -> Result<()> {
2932        let schema = test_schema();
2933        let input: Arc<dyn ExecutionPlan> =
2934            Arc::new(TestMemoryExec::try_new(&[], Arc::clone(&schema), None)?);
2935        let plan = bounded_window_exec_pb_latent_range(input, 1, "hash", "sn")?;
2936        let plan = plan
2937            .downcast_ref::<BoundedWindowAggExec>()
2938            .expect("expected BoundedWindowAggExec");
2939
2940        assert!(matches!(
2941            plan.cardinality_effect(),
2942            CardinalityEffect::Equal
2943        ));
2944        Ok(())
2945    }
2946
2947    /// Checks the per-partition batches that `LinearSearch` splits an input
2948    /// batch into: partitions appear in first-appearance order, rows within a
2949    /// partition keep their stream order, NULL keys form their own partition,
2950    /// and a single-partition batch is passed through without copying.
2951    #[test]
2952    fn test_linear_search_evaluate_partition_batches() -> Result<()> {
2953        use super::{LinearSearch, PartitionSearcher};
2954        use arrow::array::{Int32Array, Int64Array};
2955
2956        let schema = Arc::new(Schema::new(vec![
2957            Field::new("a", DataType::Int32, true),
2958            Field::new("b", DataType::Int64, false),
2959        ]));
2960        let window_expr = create_window_expr(
2961            &WindowFunctionDefinition::AggregateUDF(count_udaf()),
2962            "count".to_string(),
2963            &[col("b", &schema)?],
2964            &[col("a", &schema)?],
2965            &[],
2966            Arc::new(WindowFrame::new(None)),
2967            Arc::clone(&schema),
2968            false,
2969            false,
2970            None,
2971        )?;
2972        let mut searcher = LinearSearch::new(vec![], Arc::clone(&schema));
2973
2974        let batch = RecordBatch::try_new(
2975            Arc::clone(&schema),
2976            vec![
2977                Arc::new(Int32Array::from(vec![
2978                    Some(1),
2979                    Some(2),
2980                    Some(1),
2981                    None,
2982                    Some(2),
2983                    Some(1),
2984                ])),
2985                Arc::new(Int64Array::from(vec![10, 20, 11, 30, 21, 12])),
2986            ],
2987        )?;
2988        let result =
2989            searcher.evaluate_partition_batches(&batch, &[Arc::clone(&window_expr)])?;
2990        assert_eq!(result.len(), 3);
2991        let expected = [
2992            (
2993                ScalarValue::Int32(Some(1)),
2994                vec![Some(1); 3],
2995                vec![10i64, 11, 12],
2996            ),
2997            (ScalarValue::Int32(Some(2)), vec![Some(2); 2], vec![20, 21]),
2998            (ScalarValue::Int32(None), vec![None], vec![30]),
2999        ];
3000        for ((key, partition_batch), (exp_key, exp_a, exp_b)) in
3001            result.iter().zip(expected)
3002        {
3003            assert_eq!(key, &vec![exp_key]);
3004            let exp_batch = RecordBatch::try_new(
3005                Arc::clone(&schema),
3006                vec![
3007                    Arc::new(Int32Array::from(exp_a)),
3008                    Arc::new(Int64Array::from(exp_b)),
3009                ],
3010            )?;
3011            assert_eq!(partition_batch, &exp_batch);
3012        }
3013
3014        let single = RecordBatch::try_new(
3015            Arc::clone(&schema),
3016            vec![
3017                Arc::new(Int32Array::from(vec![Some(7), Some(7)])),
3018                Arc::new(Int64Array::from(vec![70, 71])),
3019            ],
3020        )?;
3021        let result = searcher.evaluate_partition_batches(&single, &[window_expr])?;
3022        assert_eq!(result.len(), 1);
3023        assert_eq!(result[0].0, vec![ScalarValue::Int32(Some(7))]);
3024        assert_eq!(result[0].1, single);
3025        // The whole batch belongs to one partition, so its columns are reused
3026        // rather than gathered into a new batch.
3027        assert!(Arc::ptr_eq(result[0].1.column(0), single.column(0)));
3028        Ok(())
3029    }
3030}