Skip to main content

datafusion_physical_expr/window/
window_expr.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
18use std::any::Any;
19use std::fmt::Debug;
20use std::ops::Range;
21use std::sync::Arc;
22
23use crate::PhysicalExpr;
24
25use arrow::array::BooleanArray;
26use arrow::array::{Array, ArrayRef, new_empty_array};
27use arrow::compute::SortOptions;
28use arrow::compute::filter as arrow_filter;
29use arrow::compute::kernels::sort::SortColumn;
30use arrow::datatypes::FieldRef;
31use arrow::record_batch::RecordBatch;
32use datafusion_common::cast::as_boolean_array;
33use datafusion_common::hash_utils::RandomState;
34use datafusion_common::utils::compare_rows;
35use datafusion_common::{
36    Result, ScalarValue, arrow_datafusion_err, exec_datafusion_err, exec_err,
37    internal_err,
38};
39use datafusion_expr::window_state::{
40    PartitionBatchState, WindowAggState, WindowFrameContext, WindowFrameStateGroups,
41};
42use datafusion_expr::{Accumulator, PartitionEvaluator, WindowFrame, WindowFrameBound};
43use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
44
45use datafusion_physical_expr_common::utils::evaluate_expressions_to_arrays;
46use indexmap::IndexMap;
47
48/// Common trait for [window function] implementations
49///
50/// # Aggregate Window Expressions
51///
52/// These expressions take the form
53///
54/// ```text
55/// OVER({ROWS | RANGE| GROUPS} BETWEEN UNBOUNDED PRECEDING AND ...)
56/// ```
57///
58/// For example, cumulative window frames uses `PlainAggregateWindowExpr`.
59///
60/// # Non Aggregate Window Expressions
61///
62/// The expressions have the form
63///
64/// ```text
65/// OVER({ROWS | RANGE| GROUPS} BETWEEN M {PRECEDING| FOLLOWING} AND ...)
66/// ```
67///
68/// For example, sliding window frames use [`SlidingAggregateWindowExpr`].
69///
70/// [window function]: https://en.wikipedia.org/wiki/Window_function_(SQL)
71/// [`PlainAggregateWindowExpr`]: crate::window::PlainAggregateWindowExpr
72/// [`SlidingAggregateWindowExpr`]: crate::window::SlidingAggregateWindowExpr
73pub trait WindowExpr: Send + Sync + Debug {
74    /// Returns the window expression as [`Any`] so that it can be
75    /// downcast to a specific implementation.
76    fn as_any(&self) -> &dyn Any;
77
78    /// The field of the final result of this window function.
79    fn field(&self) -> Result<FieldRef>;
80
81    /// Human readable name such as `"MIN(c2)"` or `"RANK()"`. The default
82    /// implementation returns placeholder text.
83    fn name(&self) -> &str {
84        "WindowExpr: default name"
85    }
86
87    /// Expressions that are passed to the WindowAccumulator.
88    /// Functions which take a single input argument, such as `sum`, return a single [`datafusion_expr::expr::Expr`],
89    /// others (e.g. `cov`) return many.
90    fn expressions(&self) -> Vec<Arc<dyn PhysicalExpr>>;
91
92    /// Evaluate the window function arguments against the batch and return
93    /// array ref, normally the resulting `Vec` is a single element one.
94    fn evaluate_args(&self, batch: &RecordBatch) -> Result<Vec<ArrayRef>> {
95        evaluate_expressions_to_arrays(&self.expressions(), batch)
96    }
97
98    /// Evaluate the window function values against the batch
99    fn evaluate(&self, batch: &RecordBatch) -> Result<ArrayRef>;
100
101    /// Evaluate the window function against the batch. This function facilitates
102    /// stateful, bounded-memory implementations.
103    ///
104    /// `eval_ctx` carries stream-level (cross-partition) information; see
105    /// [`WindowEvalContext`].
106    fn evaluate_stateful(
107        &self,
108        _partition_batches: &PartitionBatches,
109        _window_agg_state: &mut PartitionWindowAggStates,
110        _eval_ctx: &WindowEvalContext<'_>,
111    ) -> Result<()> {
112        internal_err!("evaluate_stateful is not implemented for {}", self.name())
113    }
114
115    /// Expressions that's from the window function's partition by clause, empty if absent
116    fn partition_by(&self) -> &[Arc<dyn PhysicalExpr>];
117
118    /// Expressions that's from the window function's order by clause, empty if absent
119    fn order_by(&self) -> &[PhysicalSortExpr];
120
121    /// Get order by columns, empty if absent
122    fn order_by_columns(&self, batch: &RecordBatch) -> Result<Vec<SortColumn>> {
123        self.order_by()
124            .iter()
125            .map(|e| e.evaluate_to_sort_column(batch))
126            .collect()
127    }
128
129    /// Get the window frame of this [WindowExpr].
130    fn get_window_frame(&self) -> &Arc<WindowFrame>;
131
132    /// Return a flag indicating whether this [WindowExpr] can run with
133    /// bounded memory.
134    fn uses_bounded_memory(&self) -> bool;
135
136    /// Get the reverse expression of this [WindowExpr].
137    fn get_reverse_expr(&self) -> Option<Arc<dyn WindowExpr>>;
138
139    /// Creates a new instance of the window function evaluator.
140    ///
141    /// Returns `WindowFn::Builtin` for built-in window functions (e.g., ROW_NUMBER, RANK)
142    /// or `WindowFn::Aggregate` for aggregate window functions (e.g., SUM, AVG).
143    fn create_window_fn(&self) -> Result<WindowFn>;
144
145    /// Returns all expressions used in the [`WindowExpr`].
146    /// These expressions are (1) function arguments, (2) partition by expressions, (3) order by expressions.
147    fn all_expressions(&self) -> WindowPhysicalExpressions {
148        let args = self.expressions();
149        let partition_by_exprs = self.partition_by().to_vec();
150        let order_by_exprs = self
151            .order_by()
152            .iter()
153            .map(|sort_expr| Arc::clone(&sort_expr.expr))
154            .collect();
155        WindowPhysicalExpressions {
156            args,
157            partition_by_exprs,
158            order_by_exprs,
159        }
160    }
161
162    /// Rewrites [`WindowExpr`], with new expressions given. The argument should be consistent
163    /// with the return value of the [`WindowExpr::all_expressions`] method.
164    /// Returns `Some(Arc<dyn WindowExpr>)` if re-write is supported, otherwise returns `None`.
165    fn with_new_expressions(
166        &self,
167        _args: Vec<Arc<dyn PhysicalExpr>>,
168        _partition_bys: Vec<Arc<dyn PhysicalExpr>>,
169        _order_by_exprs: Vec<Arc<dyn PhysicalExpr>>,
170    ) -> Option<Arc<dyn WindowExpr>> {
171        None
172    }
173}
174
175/// Stores the physical expressions used inside the `WindowExpr`.
176pub struct WindowPhysicalExpressions {
177    /// Window function arguments
178    pub args: Vec<Arc<dyn PhysicalExpr>>,
179    /// PARTITION BY expressions
180    pub partition_by_exprs: Vec<Arc<dyn PhysicalExpr>>,
181    /// ORDER BY expressions
182    pub order_by_exprs: Vec<Arc<dyn PhysicalExpr>>,
183}
184
185/// Extension trait that adds common functionality to [`AggregateWindowExpr`]s
186pub trait AggregateWindowExpr: WindowExpr {
187    /// Get the accumulator for the window expression. Note that distinct
188    /// window expressions may return distinct accumulators; e.g. sliding
189    /// (non-sliding) expressions will return sliding (normal) accumulators.
190    fn get_accumulator(&self) -> Result<Box<dyn Accumulator>>;
191
192    /// Optional FILTER (WHERE ...) predicate for this window aggregate.
193    fn filter_expr(&self) -> Option<&Arc<dyn PhysicalExpr>>;
194
195    /// Given current range and the last range, calculates the accumulator
196    /// result for the range of interest.
197    fn get_aggregate_result_inside_range(
198        &self,
199        last_range: &Range<usize>,
200        cur_range: &Range<usize>,
201        value_slice: &[ArrayRef],
202        accumulator: &mut Box<dyn Accumulator>,
203        filter_mask: Option<&BooleanArray>,
204    ) -> Result<ScalarValue>;
205
206    /// Indicates whether this window function always produces the same result
207    /// for all rows in the partition.
208    fn is_constant_in_partition(&self) -> bool;
209
210    /// Evaluates the window function against the batch.
211    fn aggregate_evaluate(&self, batch: &RecordBatch) -> Result<ArrayRef> {
212        let mut accumulator = self.get_accumulator()?;
213        let mut last_range = Range { start: 0, end: 0 };
214        let sort_options = self.order_by().iter().map(|o| o.options).collect();
215        let mut window_frame_ctx =
216            WindowFrameContext::new(Arc::clone(self.get_window_frame()), sort_options);
217        self.get_result_column(
218            &mut accumulator,
219            batch,
220            None,
221            &mut last_range,
222            &mut window_frame_ctx,
223            0,
224            false,
225        )
226    }
227
228    /// Statefully evaluates the window function against the batch. Maintains
229    /// state so that it can work incrementally over multiple chunks.
230    fn aggregate_evaluate_stateful(
231        &self,
232        partition_batches: &PartitionBatches,
233        window_agg_state: &mut PartitionWindowAggStates,
234        eval_ctx: &WindowEvalContext<'_>,
235    ) -> Result<()> {
236        let field = self.field()?;
237        let out_type = field.data_type();
238        // Every partition consults the same most recent input row, so its
239        // ORDER BY values can be evaluated once, outside the per-partition
240        // loop.
241        let most_recent_row_order_bys = eval_ctx
242            .most_recent_row
243            .map(|batch| self.order_by_columns(batch))
244            .transpose()?
245            .map(get_orderby_values);
246        for (partition_row, partition_batch_state) in partition_batches.iter() {
247            if !window_agg_state.contains_key(partition_row) {
248                let accumulator = self.get_accumulator()?;
249                window_agg_state.insert(
250                    partition_row.clone(),
251                    WindowState {
252                        state: WindowAggState::new(out_type)?,
253                        window_fn: WindowFn::Aggregate(accumulator),
254                        published: false,
255                    },
256                );
257            };
258            let window_state = window_agg_state
259                .get_mut(partition_row)
260                .ok_or_else(|| exec_datafusion_err!("Cannot find state"))?;
261            let accumulator = match &mut window_state.window_fn {
262                WindowFn::Aggregate(accumulator) => accumulator,
263                _ => unreachable!(),
264            };
265            let state = &mut window_state.state;
266            let record_batch = &partition_batch_state.record_batch;
267
268            // Skip partitions that cannot produce anything new until they
269            // either receive rows or reach their end.
270            if state.is_up_to_date_with(partition_batch_state) {
271                continue;
272            }
273
274            // If there is no window state context, initialize it.
275            let window_frame_ctx = state.window_frame_ctx.get_or_insert_with(|| {
276                let sort_options = self.order_by().iter().map(|o| o.options).collect();
277                WindowFrameContext::new(Arc::clone(self.get_window_frame()), sort_options)
278            });
279            let out_col = self.get_result_column(
280                accumulator,
281                record_batch,
282                most_recent_row_order_bys.as_deref(),
283                // Start search from the last range
284                &mut state.window_frame_range,
285                window_frame_ctx,
286                state.last_calculated_index,
287                !partition_batch_state.is_end,
288            )?;
289            state.update(&out_col, partition_batch_state)?;
290        }
291        Ok(())
292    }
293
294    /// Calculates the window expression result for the given record batch.
295    /// Assumes that `record_batch` belongs to a single partition.
296    ///
297    /// # Arguments
298    /// * `accumulator`: The accumulator to use for the calculation.
299    /// * `record_batch`: batch belonging to the current partition (see [`PartitionBatchState`]).
300    /// * `most_recent_row_order_bys`: ORDER BY values of the most recent input
301    ///   row, if available (see [`WindowExpr::evaluate_stateful`]).
302    /// * `last_range`: The last range of rows that were processed (see [`WindowAggState`]).
303    /// * `window_frame_ctx`: Details about the window frame (see [`WindowFrameContext`]).
304    /// * `idx`: The index of the current row in the record batch.
305    /// * `not_end`: is the current row not the end of the partition (see [`PartitionBatchState`]).
306    #[expect(clippy::too_many_arguments)]
307    fn get_result_column(
308        &self,
309        accumulator: &mut Box<dyn Accumulator>,
310        record_batch: &RecordBatch,
311        most_recent_row_order_bys: Option<&[ArrayRef]>,
312        last_range: &mut Range<usize>,
313        window_frame_ctx: &mut WindowFrameContext,
314        mut idx: usize,
315        not_end: bool,
316    ) -> Result<ArrayRef> {
317        let values = self.evaluate_args(record_batch)?;
318
319        // Evaluate filter mask once per record batch if present
320        let filter_mask_arr: Option<ArrayRef> = match self.filter_expr() {
321            Some(expr) => {
322                let value = expr.evaluate(record_batch)?;
323                Some(value.into_array(record_batch.num_rows())?)
324            }
325            None => None,
326        };
327
328        // Borrow boolean view from the owned array
329        let filter_mask: Option<&BooleanArray> = match filter_mask_arr.as_deref() {
330            Some(arr) => Some(as_boolean_array(arr)?),
331            None => None,
332        };
333
334        if self.is_constant_in_partition() {
335            if not_end {
336                let field = self.field()?;
337                let out_type = field.data_type();
338                return Ok(new_empty_array(out_type));
339            }
340            let values = if let Some(mask) = filter_mask {
341                // Apply mask to all argument arrays before a single update
342                filter_arrays(&values, mask)?
343            } else {
344                values
345            };
346            accumulator.update_batch(&values)?;
347            let value = accumulator.evaluate()?;
348            return value.to_array_of_size(record_batch.num_rows());
349        }
350        let order_bys = get_orderby_values(self.order_by_columns(record_batch)?);
351
352        // We iterate on each row to perform a running calculation.
353        let length = values[0].len();
354        let mut row_wise_results: Vec<ScalarValue> = vec![];
355        let is_causal = self.get_window_frame().is_causal();
356        while idx < length {
357            // Start search from the last_range. This squeezes searched range.
358            let cur_range =
359                window_frame_ctx.calculate_range(&order_bys, last_range, length, idx)?;
360            // Exit if the range is non-causal and extends all the way:
361            if cur_range.end == length
362                && !is_causal
363                && not_end
364                && !is_end_bound_safe(
365                    window_frame_ctx,
366                    &order_bys,
367                    most_recent_row_order_bys,
368                    self.order_by(),
369                    idx,
370                )?
371            {
372                break;
373            }
374            let value = self.get_aggregate_result_inside_range(
375                last_range,
376                &cur_range,
377                &values,
378                accumulator,
379                filter_mask,
380            )?;
381            // Update last range
382            *last_range = cur_range;
383            row_wise_results.push(value);
384            idx += 1;
385        }
386
387        if row_wise_results.is_empty() {
388            let field = self.field()?;
389            let out_type = field.data_type();
390            Ok(new_empty_array(out_type))
391        } else {
392            ScalarValue::iter_to_array(row_wise_results)
393        }
394    }
395}
396
397/// Filters a single array with the provided boolean mask.
398pub(crate) fn filter_array(array: &ArrayRef, mask: &BooleanArray) -> Result<ArrayRef> {
399    arrow_filter(array.as_ref(), mask)
400        .map(|a| a as ArrayRef)
401        .map_err(|e| arrow_datafusion_err!(e))
402}
403
404/// Filters a list of arrays with the provided boolean mask.
405pub(crate) fn filter_arrays(
406    arrays: &[ArrayRef],
407    mask: &BooleanArray,
408) -> Result<Vec<ArrayRef>> {
409    arrays.iter().map(|arr| filter_array(arr, mask)).collect()
410}
411
412/// Determines whether the end bound calculation for a window frame context is
413/// safe, meaning that the end bound stays the same, regardless of future data,
414/// based on the current sort expressions and ORDER BY columns. This function
415/// delegates work to specific functions for each frame type.
416///
417/// # Parameters
418///
419/// * `window_frame_ctx`: The context of the window frame being evaluated.
420/// * `order_bys`: A slice of `ArrayRef` representing the ORDER BY columns.
421/// * `most_recent_order_bys`: An optional reference to the most recent ORDER BY
422///   columns.
423/// * `sort_exprs`: Defines the lexicographical ordering in question.
424/// * `idx`: The current index in the window frame.
425///
426/// # Returns
427///
428/// A `Result` which is `Ok(true)` if the end bound is safe, `Ok(false)` otherwise.
429pub(crate) fn is_end_bound_safe(
430    window_frame_ctx: &WindowFrameContext,
431    order_bys: &[ArrayRef],
432    most_recent_order_bys: Option<&[ArrayRef]>,
433    sort_exprs: &[PhysicalSortExpr],
434    idx: usize,
435) -> Result<bool> {
436    if sort_exprs.is_empty() {
437        // Early return if no sort expressions are present:
438        return Ok(false);
439    };
440
441    match window_frame_ctx {
442        WindowFrameContext::Rows(window_frame) => {
443            is_end_bound_safe_for_rows(&window_frame.end_bound)
444        }
445        WindowFrameContext::Range { window_frame, .. } => is_end_bound_safe_for_range(
446            &window_frame.end_bound,
447            &order_bys[0],
448            most_recent_order_bys.map(|items| &items[0]),
449            &sort_exprs[0].options,
450            idx,
451        ),
452        WindowFrameContext::Groups {
453            window_frame,
454            state,
455        } => is_end_bound_safe_for_groups(
456            &window_frame.end_bound,
457            state,
458            &order_bys[0],
459            most_recent_order_bys.map(|items| &items[0]),
460            &sort_exprs[0].options,
461        ),
462    }
463}
464
465/// For row-based window frames, determines whether the end bound calculation
466/// is safe, which is trivially the case for `Preceding` and `CurrentRow` bounds.
467/// For 'Following' bounds, it compares the bound value to zero to ensure that
468/// it doesn't extend beyond the current row.
469///
470/// # Parameters
471///
472/// * `end_bound`: Reference to the window frame bound in question.
473///
474/// # Returns
475///
476/// A `Result` indicating whether the end bound is safe for row-based window frames.
477fn is_end_bound_safe_for_rows(end_bound: &WindowFrameBound) -> Result<bool> {
478    if let WindowFrameBound::Following(value) = end_bound {
479        let zero = ScalarValue::new_zero(&value.data_type());
480        Ok(zero.map(|zero| value.eq(&zero)).unwrap_or(false))
481    } else {
482        Ok(true)
483    }
484}
485
486/// For row-based window frames, determines whether the end bound calculation
487/// is safe by comparing it against specific values (zero, current row). It uses
488/// the `is_row_ahead` helper function to determine if the current row is ahead
489/// of the most recent row based on the ORDER BY column and sorting options.
490///
491/// # Parameters
492///
493/// * `end_bound`: Reference to the window frame bound in question.
494/// * `orderby_col`: Reference to the column used for ordering.
495/// * `most_recent_ob_col`: Optional reference to the most recent order-by column.
496/// * `sort_options`: The sorting options used in the window frame.
497/// * `idx`: The current index in the window frame.
498///
499/// # Returns
500///
501/// A `Result` indicating whether the end bound is safe for range-based window frames.
502fn is_end_bound_safe_for_range(
503    end_bound: &WindowFrameBound,
504    orderby_col: &ArrayRef,
505    most_recent_ob_col: Option<&ArrayRef>,
506    sort_options: &SortOptions,
507    idx: usize,
508) -> Result<bool> {
509    match end_bound {
510        WindowFrameBound::Preceding(value) => {
511            let zero = ScalarValue::new_zero(&value.data_type())?;
512            if value.eq(&zero) {
513                is_row_ahead(orderby_col, most_recent_ob_col, sort_options)
514            } else {
515                Ok(true)
516            }
517        }
518        WindowFrameBound::CurrentRow => {
519            is_row_ahead(orderby_col, most_recent_ob_col, sort_options)
520        }
521        WindowFrameBound::Following(delta) => {
522            let Some(most_recent_ob_col) = most_recent_ob_col else {
523                return Ok(false);
524            };
525            let most_recent_row_value =
526                ScalarValue::try_from_array(most_recent_ob_col, 0)?;
527            let current_row_value = ScalarValue::try_from_array(orderby_col, idx)?;
528
529            if sort_options.descending {
530                current_row_value
531                    .sub(delta)
532                    .map(|value| value > most_recent_row_value)
533            } else {
534                current_row_value
535                    .add(delta)
536                    .map(|value| most_recent_row_value > value)
537            }
538        }
539    }
540}
541
542/// For group-based window frames, determines whether the end bound calculation
543/// is safe by considering the group offset and whether the current row is ahead
544/// of the most recent row in terms of sorting. It checks if the end bound is
545/// within the bounds of the current group based on group end indices.
546///
547/// # Parameters
548///
549/// * `end_bound`: Reference to the window frame bound in question.
550/// * `state`: The state of the window frame for group calculations.
551/// * `orderby_col`: Reference to the column used for ordering.
552/// * `most_recent_ob_col`: Optional reference to the most recent order-by column.
553/// * `sort_options`: The sorting options used in the window frame.
554///
555/// # Returns
556///
557/// A `Result` indicating whether the end bound is safe for group-based window frames.
558fn is_end_bound_safe_for_groups(
559    end_bound: &WindowFrameBound,
560    state: &WindowFrameStateGroups,
561    orderby_col: &ArrayRef,
562    most_recent_ob_col: Option<&ArrayRef>,
563    sort_options: &SortOptions,
564) -> Result<bool> {
565    match end_bound {
566        WindowFrameBound::Preceding(value) => {
567            let zero = ScalarValue::new_zero(&value.data_type())?;
568            if value.eq(&zero) {
569                is_row_ahead(orderby_col, most_recent_ob_col, sort_options)
570            } else {
571                Ok(true)
572            }
573        }
574        WindowFrameBound::CurrentRow => {
575            is_row_ahead(orderby_col, most_recent_ob_col, sort_options)
576        }
577        WindowFrameBound::Following(ScalarValue::UInt64(Some(offset))) => {
578            let delta = state.group_end_indices.len() - state.current_group_idx;
579            if delta == (*offset as usize) + 1 {
580                is_row_ahead(orderby_col, most_recent_ob_col, sort_options)
581            } else {
582                Ok(false)
583            }
584        }
585        _ => Ok(false),
586    }
587}
588
589/// This utility function checks whether `current_cols` is ahead of the `old_cols`
590/// in terms of `sort_options`.
591fn is_row_ahead(
592    old_col: &ArrayRef,
593    current_col: Option<&ArrayRef>,
594    sort_options: &SortOptions,
595) -> Result<bool> {
596    let Some(current_col) = current_col else {
597        return Ok(false);
598    };
599    if old_col.is_empty() || current_col.is_empty() {
600        return Ok(false);
601    }
602    let last_value = ScalarValue::try_from_array(old_col, old_col.len() - 1)?;
603    let current_value = ScalarValue::try_from_array(current_col, 0)?;
604    let cmp = compare_rows(&[current_value], &[last_value], &[*sort_options])?;
605    Ok(cmp.is_gt())
606}
607
608/// Get order by expression results inside `order_by_columns`.
609pub(crate) fn get_orderby_values(order_by_columns: Vec<SortColumn>) -> Vec<ArrayRef> {
610    order_by_columns.into_iter().map(|s| s.values).collect()
611}
612
613#[derive(Debug)]
614pub enum WindowFn {
615    Builtin(Box<dyn PartitionEvaluator>),
616    Aggregate(Box<dyn Accumulator>),
617}
618
619/// Key for IndexMap for each unique partition
620///
621/// For instance, if window frame is `OVER(PARTITION BY a,b)`,
622/// PartitionKey would consist of unique `[a,b]` pairs
623pub type PartitionKey = Vec<ScalarValue>;
624
625/// Stream-level context passed to [`WindowExpr::evaluate_stateful`].
626///
627/// This carries information that spans all partitions of the input, as
628/// opposed to the per-partition state in [`PartitionBatches`] and
629/// [`PartitionWindowAggStates`]. It is `non_exhaustive` so that fields can
630/// be added without breaking implementors; construct it with
631/// [`Default::default`] and the `with_*` builder methods.
632#[derive(Debug, Clone, Copy, Default)]
633#[non_exhaustive]
634pub struct WindowEvalContext<'a> {
635    /// A single-row batch containing the most recent input row, whichever
636    /// partition that row belongs to. It is `Some` only when the input is
637    /// ordered by the first ORDER BY column across partitions (`Linear`
638    /// mode), in which case no future input row -- in any partition -- can
639    /// precede it in that column; implementations can use this bound to
640    /// decide whether pending window frames can be finalized before their
641    /// partition receives more data.
642    pub most_recent_row: Option<&'a RecordBatch>,
643}
644
645impl<'a> WindowEvalContext<'a> {
646    /// Sets the most recent input row (see [`Self::most_recent_row`]).
647    pub fn with_most_recent_row(mut self, batch: Option<&'a RecordBatch>) -> Self {
648        self.most_recent_row = batch;
649        self
650    }
651}
652
653#[derive(Debug)]
654pub struct WindowState {
655    pub state: WindowAggState,
656    pub window_fn: WindowFn,
657    /// True once [`Self::aggregate_state`] has been called on this entry.
658    /// Guards against a second destructive [`Accumulator::state`] read: the
659    /// method itself errors on second call, and the observer loop in
660    /// `BoundedWindowAggStream::publish_finalized_states` uses this as an
661    /// early-skip so it doesn't attempt one. Independent of `state.is_end`,
662    /// which is a group-closed signal that the pruning path also reads.
663    pub published: bool,
664}
665
666impl WindowState {
667    /// [`Accumulator::state`] if this window function is an aggregate, `None`
668    /// otherwise (built-in functions like `row_number`, `rank`, `lead`/`lag`
669    /// have no serializable accumulator state).
670    ///
671    /// [`Accumulator::state`] takes `&mut self` and its trait doc calls out
672    /// that "this function should not be called twice, otherwise it will
673    /// result in potentially non-deterministic behavior." Several built-in
674    /// impls (`median`, `percentile_cont`, `string_agg`,
675    /// `min_max_bytes`/`min_max_struct`) `std::mem::take` their internal
676    /// buffers on call — a second call returns *empty* state, not the same
677    /// state, so a downstream prefix-merge would silently lose every value
678    /// the accumulator had ingested.
679    ///
680    /// Enforced at this layer: on first call we set [`Self::published`] and
681    /// return the state; any later call errors rather than performing a
682    /// destructive re-read.
683    pub fn aggregate_state(&mut self) -> Result<Option<Vec<ScalarValue>>> {
684        if self.published {
685            return exec_err!(
686                "WindowState::aggregate_state called more than once; \
687                 Accumulator::state is a destructive read for several \
688                 built-in aggregates and a second call would silently lose data"
689            );
690        }
691        let state = match &mut self.window_fn {
692            WindowFn::Aggregate(accumulator) => Some(accumulator.state()?),
693            WindowFn::Builtin(_) => None,
694        };
695        self.published = true;
696        Ok(state)
697    }
698}
699
700pub type PartitionWindowAggStates = IndexMap<PartitionKey, WindowState, RandomState>;
701
702/// The IndexMap (i.e. an ordered HashMap) where record batches are separated for each partition.
703pub type PartitionBatches = IndexMap<PartitionKey, PartitionBatchState, RandomState>;
704
705#[cfg(test)]
706mod tests {
707    use std::sync::Arc;
708
709    use crate::window::window_expr::{WindowFn, WindowState, is_row_ahead};
710
711    use arrow::array::{ArrayRef, Float64Array};
712    use arrow::compute::SortOptions;
713    use arrow::datatypes::DataType;
714    use datafusion_common::{Result, ScalarValue};
715    use datafusion_expr::{Accumulator, window_state::WindowAggState};
716
717    /// Minimal [`Accumulator`] whose `state()` records how many times it was
718    /// called by returning the count as its single state element. Any second
719    /// call would surface (were it allowed to happen) as `[UInt64(2)]`
720    /// instead of `[UInt64(1)]`.
721    #[derive(Debug)]
722    struct CallCountingAccumulator {
723        calls: usize,
724    }
725
726    impl Accumulator for CallCountingAccumulator {
727        fn update_batch(&mut self, _values: &[ArrayRef]) -> Result<()> {
728            Ok(())
729        }
730        fn evaluate(&mut self) -> Result<ScalarValue> {
731            Ok(ScalarValue::Null)
732        }
733        fn size(&self) -> usize {
734            size_of::<Self>()
735        }
736        fn state(&mut self) -> Result<Vec<ScalarValue>> {
737            self.calls += 1;
738            Ok(vec![ScalarValue::UInt64(Some(self.calls as u64))])
739        }
740        fn merge_batch(&mut self, _states: &[ArrayRef]) -> Result<()> {
741            Ok(())
742        }
743    }
744
745    #[test]
746    fn aggregate_state_errors_on_second_call() -> Result<()> {
747        // `Accumulator::state()` is a destructive read for several built-in
748        // aggregates (median, percentile_cont, string_agg, min_max_bytes/
749        // min_max_struct all `mem::take` their internal buffers). Its trait
750        // doc says "should not be called twice"; `WindowState::aggregate_state`
751        // enforces that at this layer by returning an error rather than
752        // performing the second read.
753        let acc: Box<dyn Accumulator> = Box::new(CallCountingAccumulator { calls: 0 });
754        let mut ws = WindowState {
755            state: WindowAggState::new(&DataType::UInt64)?,
756            window_fn: WindowFn::Aggregate(acc),
757            published: false,
758        };
759        let first = ws.aggregate_state()?;
760        assert_eq!(first, Some(vec![ScalarValue::UInt64(Some(1))]));
761        assert!(ws.published, "published must flip on successful publish");
762        let err = ws.aggregate_state().unwrap_err().to_string();
763        assert!(
764            err.contains("called more than once"),
765            "expected second-call error, got: {err}"
766        );
767        Ok(())
768    }
769
770    #[test]
771    fn test_is_row_ahead() -> Result<()> {
772        let old_values: ArrayRef =
773            Arc::new(Float64Array::from(vec![5.0, 7.0, 8.0, 9., 10.]));
774
775        let new_values1: ArrayRef = Arc::new(Float64Array::from(vec![11.0]));
776        let new_values2: ArrayRef = Arc::new(Float64Array::from(vec![10.0]));
777
778        assert!(is_row_ahead(
779            &old_values,
780            Some(&new_values1),
781            &SortOptions {
782                descending: false,
783                nulls_first: false
784            }
785        )?);
786        assert!(!is_row_ahead(
787            &old_values,
788            Some(&new_values2),
789            &SortOptions {
790                descending: false,
791                nulls_first: false
792            }
793        )?);
794
795        Ok(())
796    }
797}