Skip to main content

alopex_sql/executor/query/
window.rs

1//! Execution of SQL window functions.
2
3use std::cmp::Ordering;
4use std::ops::RangeInclusive;
5
6use alopex_core::sql::stream::ByteSized;
7
8use crate::ast::{WindowFrame, WindowFrameBound, WindowFrameUnits};
9use crate::catalog::ColumnMetadata;
10use crate::executor::evaluator::{self, EvalContext};
11use crate::executor::memory::{MemoryPolicy, MemoryTracker, map_core_memory_error};
12use crate::executor::{ExecutorError, Result, Row};
13use crate::planner::logical_plan::{
14    OffsetWindowFunction, ValueWindowFunction, WindowExpr, WindowFunction,
15};
16use crate::planner::typed_expr::SortExpr;
17use crate::storage::SqlValue;
18
19use super::aggregate::{Accumulator, create_accumulator_for_aggregate};
20use super::iterator::{RowIterator, SortIterator, VecIterator, compare_key_values, compare_single};
21
22/// Upper bound on aggregate input visits for one explicit frame expression.
23/// This turns the generic O(partition_rows * average_frame_width) evaluator
24/// into a deterministic resource failure instead of unbounded CPU work.
25const MAX_EXPLICIT_FRAME_VISITS: u64 = 1_000_000;
26
27#[derive(Default)]
28struct ExplicitFrameBudget {
29    aggregate_visits: u64,
30    range_boundary_probes: u64,
31}
32
33impl ExplicitFrameBudget {
34    fn charge_aggregate_visits(&mut self, visits: u64) -> Result<()> {
35        self.aggregate_visits = self
36            .aggregate_visits
37            .checked_add(visits)
38            .ok_or_else(frame_overflow)?;
39        if self.aggregate_visits > MAX_EXPLICIT_FRAME_VISITS {
40            return Err(ExecutorError::ResourceExhausted {
41                message: format!(
42                    "explicit window frame requires more than \
43                     {MAX_EXPLICIT_FRAME_VISITS} aggregate input visits"
44                ),
45            });
46        }
47        Ok(())
48    }
49
50    fn charge_range_boundary_probes(&mut self, probes: u64) -> Result<()> {
51        self.range_boundary_probes = self
52            .range_boundary_probes
53            .checked_add(probes)
54            .ok_or_else(frame_overflow)?;
55        if self.range_boundary_probes > MAX_EXPLICIT_FRAME_VISITS {
56            return Err(ExecutorError::ResourceExhausted {
57                message: format!(
58                    "explicit RANGE frame requires more than \
59                     {MAX_EXPLICIT_FRAME_VISITS} boundary probes"
60                ),
61            });
62        }
63        Ok(())
64    }
65}
66
67/// Materializing window iterator. Input order is restored after each
68/// window-local sort so the outer query controls final ordering independently.
69pub struct WindowIterator {
70    rows: std::vec::IntoIter<Row>,
71    schema: Vec<ColumnMetadata>,
72    memory: WindowMemory,
73}
74
75#[derive(Debug)]
76struct WindowMemory {
77    tracker: Option<MemoryTracker>,
78}
79
80impl WindowMemory {
81    fn new(policy: Option<&MemoryPolicy>) -> Self {
82        Self {
83            tracker: policy.cloned().map(MemoryTracker::new),
84        }
85    }
86
87    fn reserve_bytes(&mut self, bytes: u64) -> Result<()> {
88        let Some(tracker) = &mut self.tracker else {
89            return Ok(());
90        };
91        tracker.add_bytes(bytes).map_err(map_core_memory_error)?;
92        if tracker.over_limit() {
93            let limit = tracker.policy().limit_bytes().unwrap_or(u64::MAX);
94            return Err(ExecutorError::ResourceExhausted {
95                message: format!(
96                    "window materialization requires {} bytes (limit {limit}); spilling this \
97                     operator is not supported",
98                    tracker.used_bytes()
99                ),
100            });
101        }
102        Ok(())
103    }
104
105    fn release_bytes(&mut self, bytes: u64) -> Result<()> {
106        let Some(tracker) = &mut self.tracker else {
107            return Ok(());
108        };
109        let retained = tracker.used_bytes().checked_sub(bytes).ok_or_else(|| {
110            ExecutorError::InvalidOperation {
111                operation: "window memory accounting".into(),
112                reason: "released more bytes than were reserved".into(),
113            }
114        })?;
115        tracker.reset();
116        tracker.add_bytes(retained).map_err(map_core_memory_error)
117    }
118
119    fn clear(&mut self) {
120        if let Some(tracker) = &mut self.tracker {
121            tracker.reset();
122        }
123    }
124
125    #[cfg(test)]
126    fn used_bytes(&self) -> u64 {
127        self.tracker
128            .as_ref()
129            .map(MemoryTracker::used_bytes)
130            .unwrap_or(0)
131    }
132}
133
134impl WindowIterator {
135    pub fn new<I: RowIterator>(
136        mut input: I,
137        windows: Vec<WindowExpr>,
138        memory: Option<&MemoryPolicy>,
139    ) -> Result<Self> {
140        let input_schema = input.schema().to_vec();
141        let memory_policy = memory.cloned();
142        let mut memory = WindowMemory::new(memory);
143        let mut rows = Vec::new();
144        while let Some(row) = input.next_row() {
145            let row = row?;
146            let previous_capacity = rows.capacity();
147            rows.push(row);
148            if rows.capacity() > previous_capacity {
149                memory.reserve_bytes(estimated_slots_bytes::<Row>(
150                    rows.capacity() - previous_capacity,
151                )?)?;
152            }
153            memory.reserve_bytes(estimated_row_payload_bytes(
154                rows.last().expect("row was just pushed"),
155            )?)?;
156        }
157
158        for (window_index, window) in windows.iter().enumerate() {
159            let mut frame_budget = ExplicitFrameBudget::default();
160            let sortable_bytes = estimated_cloned_rows_bytes(&rows)?;
161            let sort_key_bytes = estimated_sort_key_bytes(&rows, window)?;
162            // Reserve the full partition once. `clear` below retains this
163            // allocation between partitions, so there is only one overlapping
164            // row-slot buffer to account for during this window expression.
165            let mut partition = Vec::with_capacity(rows.len());
166            let partition_slots = estimated_slots_bytes::<Row>(partition.capacity())?;
167            let output_slots = estimated_slots_bytes::<SqlValue>(rows.len())?;
168            let range_slots = if window.frame.is_some()
169                && matches!(&window.function, WindowFunction::Aggregate(_))
170            {
171                estimated_slots_bytes::<Option<RangeInclusive<usize>>>(rows.len())?
172            } else {
173                0
174            };
175            let temporary_bytes = sortable_bytes
176                .checked_add(sort_key_bytes)
177                .and_then(|bytes| bytes.checked_add(partition_slots))
178                .and_then(|bytes| bytes.checked_add(output_slots))
179                .and_then(|bytes| bytes.checked_add(range_slots))
180                .ok_or_else(window_memory_overflow)?;
181            memory.reserve_bytes(temporary_bytes)?;
182
183            let mut sortable_rows = rows
184                .iter()
185                .enumerate()
186                .map(|(index, row)| Row::new(index as u64, row.values.clone()))
187                .collect::<Vec<_>>();
188            let mut sort_exprs = window
189                .partition_by
190                .iter()
191                .cloned()
192                .map(SortExpr::asc)
193                .collect::<Vec<_>>();
194            sort_exprs.extend(window.order_by.clone());
195
196            let input = VecIterator::new(std::mem::take(&mut sortable_rows), input_schema.clone());
197            let mut sorted: Box<dyn RowIterator> = if let Some(policy) = &memory_policy {
198                Box::new(SortIterator::new_with_policy(
199                    input,
200                    &sort_exprs,
201                    Some(policy.clone()),
202                )?)
203            } else {
204                Box::new(SortIterator::new(input, &sort_exprs)?)
205            };
206
207            let mut values = vec![SqlValue::Null; rows.len()];
208            let mut current_partition_values: Option<Vec<SqlValue>> = None;
209            while let Some(row) = sorted.next_row() {
210                let row = row?;
211                let partition_values = evaluate_exprs(&row, &window.partition_by)?;
212                if current_partition_values
213                    .as_ref()
214                    .is_some_and(|current| !partition_values_equal(current, &partition_values))
215                {
216                    evaluate_partition(
217                        window,
218                        &partition,
219                        &mut values,
220                        &mut frame_budget,
221                        &mut memory,
222                    )?;
223                    partition.clear();
224                }
225                current_partition_values = Some(partition_values);
226                partition.push(row);
227            }
228            if !partition.is_empty() {
229                evaluate_partition(
230                    window,
231                    &partition,
232                    &mut values,
233                    &mut frame_budget,
234                    &mut memory,
235                )?;
236            }
237
238            for (row, value) in rows.iter_mut().zip(values) {
239                let previous_capacity = row.values.capacity();
240                row.values.push(value);
241                if row.values.capacity() > previous_capacity {
242                    memory.reserve_bytes(estimated_slots_bytes::<SqlValue>(
243                        row.values.capacity() - previous_capacity,
244                    )?)?;
245                }
246            }
247            memory.release_bytes(temporary_bytes)?;
248
249            debug_assert!(
250                rows.iter()
251                    .all(|row| { row.values.len() == input_schema.len() + window_index + 1 })
252            );
253        }
254
255        let mut schema = input_schema;
256        schema.extend(windows.iter().enumerate().map(|(index, window)| {
257            ColumnMetadata::new(format!("__window_{index}"), window.result_type.clone())
258        }));
259        Ok(Self {
260            rows: rows.into_iter(),
261            schema,
262            memory,
263        })
264    }
265
266    #[cfg(test)]
267    fn accounted_memory_bytes(&self) -> u64 {
268        self.memory.used_bytes()
269    }
270}
271
272impl RowIterator for WindowIterator {
273    fn next_row(&mut self) -> Option<Result<Row>> {
274        let row = self.rows.next()?;
275        if self.rows.as_slice().is_empty() {
276            self.rows = Vec::new().into_iter();
277            self.memory.clear();
278        } else {
279            let payload_bytes = match estimated_row_payload_bytes(&row) {
280                Ok(bytes) => bytes,
281                Err(error) => return Some(Err(error)),
282            };
283            if let Err(error) = self.memory.release_bytes(payload_bytes) {
284                return Some(Err(error));
285            }
286        }
287        Some(Ok(row))
288    }
289
290    fn schema(&self) -> &[ColumnMetadata] {
291        &self.schema
292    }
293}
294
295fn estimated_slots_bytes<T>(len: usize) -> Result<u64> {
296    u64::try_from(len)
297        .ok()
298        .and_then(|len| len.checked_mul(std::mem::size_of::<T>() as u64))
299        .ok_or_else(window_memory_overflow)
300}
301
302fn estimated_dynamic_values_bytes(values: &[SqlValue]) -> Result<u64> {
303    values.iter().try_fold(0_u64, |total, value| {
304        total
305            .checked_add(value.estimated_bytes())
306            .ok_or_else(window_memory_overflow)
307    })
308}
309
310fn estimated_row_payload_bytes(row: &Row) -> Result<u64> {
311    estimated_slots_bytes::<SqlValue>(row.values.capacity())?
312        .checked_add(estimated_dynamic_values_bytes(&row.values)?)
313        .ok_or_else(window_memory_overflow)
314}
315
316fn estimated_cloned_rows_bytes(rows: &[Row]) -> Result<u64> {
317    let row_slots = estimated_slots_bytes::<Row>(rows.len())?;
318    rows.iter().try_fold(row_slots, |total, row| {
319        let value_slots = estimated_slots_bytes::<SqlValue>(row.values.len())?;
320        let dynamic = estimated_dynamic_values_bytes(&row.values)?;
321        total
322            .checked_add(value_slots)
323            .and_then(|bytes| bytes.checked_add(dynamic))
324            .ok_or_else(window_memory_overflow)
325    })
326}
327
328fn estimated_sort_key_bytes(rows: &[Row], window: &WindowExpr) -> Result<u64> {
329    let expressions = window
330        .partition_by
331        .iter()
332        .chain(window.order_by.iter().map(|sort| &sort.expr));
333    let expressions = expressions.collect::<Vec<_>>();
334    let key_slots = estimated_slots_bytes::<SqlValue>(expressions.len())?;
335    rows.iter().try_fold(0_u64, |total, row| {
336        let context = EvalContext::new(&row.values);
337        let dynamic = expressions.iter().try_fold(0_u64, |bytes, expr| {
338            let value = evaluator::evaluate(expr, &context)?;
339            bytes
340                .checked_add(value.estimated_bytes())
341                .ok_or_else(window_memory_overflow)
342        })?;
343        total
344            .checked_add(key_slots)
345            .and_then(|bytes| bytes.checked_add(dynamic))
346            .ok_or_else(window_memory_overflow)
347    })
348}
349
350fn window_memory_overflow() -> ExecutorError {
351    ExecutorError::ResourceExhausted {
352        message: "window materialization byte estimate overflow".into(),
353    }
354}
355
356fn partition_values_equal(left: &[SqlValue], right: &[SqlValue]) -> bool {
357    left.len() == right.len()
358        && left
359            .iter()
360            .zip(right)
361            .all(|(left, right)| compare_single(left, right, true, false) == Ordering::Equal)
362}
363
364fn evaluate_exprs(row: &Row, exprs: &[crate::planner::TypedExpr]) -> Result<Vec<SqlValue>> {
365    let context = EvalContext::new(&row.values);
366    exprs
367        .iter()
368        .map(|expr| evaluator::evaluate(expr, &context))
369        .collect()
370}
371
372fn evaluate_partition(
373    window: &WindowExpr,
374    partition: &[Row],
375    output: &mut [SqlValue],
376    frame_budget: &mut ExplicitFrameBudget,
377    memory: &mut WindowMemory,
378) -> Result<()> {
379    match &window.function {
380        WindowFunction::RowNumber => {
381            for (position, row) in partition.iter().enumerate() {
382                set_output(output, row, SqlValue::BigInt((position + 1) as i64), memory)?;
383            }
384        }
385        WindowFunction::Rank | WindowFunction::DenseRank => {
386            let mut previous_key: Option<Vec<SqlValue>> = None;
387            let mut rank = 1_i64;
388            let mut dense_rank = 1_i64;
389            for (position, row) in partition.iter().enumerate() {
390                let key = order_values(row, &window.order_by)?;
391                if position > 0
392                    && previous_key.as_ref().is_some_and(|previous| {
393                        compare_key_values(previous, &key, &window.order_by) != Ordering::Equal
394                    })
395                {
396                    rank = (position + 1) as i64;
397                    dense_rank += 1;
398                }
399                previous_key = Some(key);
400                let value = match &window.function {
401                    WindowFunction::Rank => rank,
402                    WindowFunction::DenseRank => dense_rank,
403                    _ => unreachable!(),
404                };
405                set_output(output, row, SqlValue::BigInt(value), memory)?;
406            }
407        }
408        WindowFunction::PercentRank => {
409            evaluate_percent_rank(window, partition, output, memory)?;
410        }
411        WindowFunction::CumeDist => {
412            evaluate_cume_dist(window, partition, output, memory)?;
413        }
414        WindowFunction::Ntile(argument) => {
415            evaluate_ntile(argument, partition, output, memory)?;
416        }
417        WindowFunction::Aggregate(aggregate) => {
418            if aggregate.filter.is_some() || !aggregate.order_by.is_empty() {
419                // The planner rejects FILTER / aggregate ORDER BY with OVER
420                // (issue #148, D2); this guard keeps the window executor from
421                // silently ignoring them if that validation ever regresses.
422                return Err(ExecutorError::InvalidOperation {
423                    operation: "window aggregate".into(),
424                    reason: "FILTER and aggregate ORDER BY are not supported in window \
425                             aggregate calls"
426                        .into(),
427                });
428            }
429            if let Some(frame) = &window.frame
430                && !is_default_ordered_frame(frame)
431            {
432                evaluate_framed_aggregate(
433                    window,
434                    aggregate,
435                    frame,
436                    partition,
437                    output,
438                    frame_budget,
439                    memory,
440                )?;
441            } else {
442                let mut accumulator = create_accumulator_for_aggregate(aggregate);
443                let mut accumulator_bytes = accumulator.retained_bytes();
444                memory.reserve_bytes(accumulator_bytes)?;
445                if window.order_by.is_empty() {
446                    for row in partition {
447                        update_accumulator(
448                            accumulator.as_mut(),
449                            aggregate_values(aggregate, row)?,
450                            memory,
451                            &mut accumulator_bytes,
452                        )?;
453                    }
454                    let value = accumulator.finalize()?;
455                    set_repeated_output(output, partition, value, memory)?;
456                } else {
457                    let mut peer_start = 0;
458                    while peer_start < partition.len() {
459                        let peer_key = order_values(&partition[peer_start], &window.order_by)?;
460                        let mut peer_end = peer_start + 1;
461                        while peer_end < partition.len() {
462                            let candidate = order_values(&partition[peer_end], &window.order_by)?;
463                            if compare_key_values(&candidate, &peer_key, &window.order_by)
464                                != Ordering::Equal
465                            {
466                                break;
467                            }
468                            peer_end += 1;
469                        }
470
471                        for row in &partition[peer_start..peer_end] {
472                            update_accumulator(
473                                accumulator.as_mut(),
474                                aggregate_values(aggregate, row)?,
475                                memory,
476                                &mut accumulator_bytes,
477                            )?;
478                        }
479                        let value = accumulator.finalize()?;
480                        set_repeated_output(
481                            output,
482                            &partition[peer_start..peer_end],
483                            value,
484                            memory,
485                        )?;
486                        peer_start = peer_end;
487                    }
488                }
489                memory.release_bytes(accumulator_bytes)?;
490            }
491        }
492        WindowFunction::Value(function) => {
493            evaluate_value_window(window, function, partition, output, frame_budget, memory)?;
494        }
495        WindowFunction::Lag(function) => evaluate_offset_window(
496            function,
497            OffsetDirection::Preceding,
498            partition,
499            output,
500            memory,
501        )?,
502        WindowFunction::Lead(function) => evaluate_offset_window(
503            function,
504            OffsetDirection::Following,
505            partition,
506            output,
507            memory,
508        )?,
509    }
510    Ok(())
511}
512
513fn evaluate_percent_rank(
514    window: &WindowExpr,
515    partition: &[Row],
516    output: &mut [SqlValue],
517    memory: &mut WindowMemory,
518) -> Result<()> {
519    let denominator = partition.len().saturating_sub(1) as f64;
520    let mut previous_key: Option<Vec<SqlValue>> = None;
521    let mut rank = 1_usize;
522    for (position, row) in partition.iter().enumerate() {
523        let key = order_values(row, &window.order_by)?;
524        if position > 0
525            && previous_key.as_ref().is_some_and(|previous| {
526                compare_key_values(previous, &key, &window.order_by) != Ordering::Equal
527            })
528        {
529            rank = position + 1;
530        }
531        previous_key = Some(key);
532        let value = if denominator == 0.0 {
533            0.0
534        } else {
535            rank.saturating_sub(1) as f64 / denominator
536        };
537        set_output(output, row, SqlValue::Double(value), memory)?;
538    }
539    Ok(())
540}
541
542fn evaluate_cume_dist(
543    window: &WindowExpr,
544    partition: &[Row],
545    output: &mut [SqlValue],
546    memory: &mut WindowMemory,
547) -> Result<()> {
548    let denominator = partition.len() as f64;
549    let mut peer_start = 0;
550    while peer_start < partition.len() {
551        let peer_key = order_values(&partition[peer_start], &window.order_by)?;
552        let mut peer_end = peer_start + 1;
553        while peer_end < partition.len() {
554            let candidate = order_values(&partition[peer_end], &window.order_by)?;
555            if compare_key_values(&candidate, &peer_key, &window.order_by) != Ordering::Equal {
556                break;
557            }
558            peer_end += 1;
559        }
560        let value = SqlValue::Double(peer_end as f64 / denominator);
561        set_repeated_output(output, &partition[peer_start..peer_end], value, memory)?;
562        peer_start = peer_end;
563    }
564    Ok(())
565}
566
567fn evaluate_ntile(
568    argument: &crate::planner::TypedExpr,
569    partition: &[Row],
570    output: &mut [SqlValue],
571    memory: &mut WindowMemory,
572) -> Result<()> {
573    let buckets = partition_constant_positive_integer("NTILE", argument, partition)?;
574    let rows = u64::try_from(partition.len()).map_err(|_| window_argument_overflow("NTILE"))?;
575    let larger_bucket_count = rows % buckets;
576    let smaller_bucket_size = rows / buckets;
577    let larger_bucket_size = smaller_bucket_size
578        .checked_add(1)
579        .ok_or_else(|| window_argument_overflow("NTILE"))?;
580    let larger_rows = larger_bucket_count
581        .checked_mul(larger_bucket_size)
582        .ok_or_else(|| window_argument_overflow("NTILE"))?;
583
584    for (position, row) in partition.iter().enumerate() {
585        let position = u64::try_from(position).map_err(|_| window_argument_overflow("NTILE"))?;
586        let bucket = if position < larger_rows {
587            position / larger_bucket_size + 1
588        } else {
589            debug_assert!(smaller_bucket_size > 0);
590            larger_bucket_count + (position - larger_rows) / smaller_bucket_size + 1
591        };
592        let bucket = i64::try_from(bucket).map_err(|_| window_argument_overflow("NTILE"))?;
593        set_output(output, row, SqlValue::BigInt(bucket), memory)?;
594    }
595    Ok(())
596}
597
598fn partition_constant_positive_integer(
599    name: &str,
600    argument: &crate::planner::TypedExpr,
601    partition: &[Row],
602) -> Result<u64> {
603    let first = positive_integer_argument(
604        name,
605        evaluator::evaluate(argument, &EvalContext::new(&partition[0].values))?,
606    )?;
607    for row in &partition[1..] {
608        let current = positive_integer_argument(
609            name,
610            evaluator::evaluate(argument, &EvalContext::new(&row.values))?,
611        )?;
612        if current != first {
613            return Err(ExecutorError::InvalidOperation {
614                operation: format!("{name} window function"),
615                reason: "argument must be constant within a partition".into(),
616            });
617        }
618    }
619    Ok(first)
620}
621
622fn positive_integer_argument(name: &str, value: SqlValue) -> Result<u64> {
623    let value = match value {
624        SqlValue::Integer(value) => i64::from(value),
625        SqlValue::BigInt(value) => value,
626        _ => {
627            return Err(ExecutorError::InvalidOperation {
628                operation: format!("{name} window function"),
629                reason: "argument must be a positive INTEGER".into(),
630            });
631        }
632    };
633    u64::try_from(value)
634        .ok()
635        .filter(|value| *value > 0)
636        .ok_or_else(|| ExecutorError::InvalidOperation {
637            operation: format!("{name} window function"),
638            reason: "argument must be a positive INTEGER".into(),
639        })
640}
641
642fn window_argument_overflow(name: &str) -> ExecutorError {
643    ExecutorError::InvalidOperation {
644        operation: format!("{name} window function"),
645        reason: "partition or argument exceeds supported range".into(),
646    }
647}
648
649fn evaluate_value_window(
650    window: &WindowExpr,
651    function: &ValueWindowFunction,
652    partition: &[Row],
653    output: &mut [SqlValue],
654    frame_budget: &mut ExplicitFrameBudget,
655    memory: &mut WindowMemory,
656) -> Result<()> {
657    if let Some(frame) = &window.frame
658        && !is_default_ordered_frame(frame)
659    {
660        if frame.units == WindowFrameUnits::Range {
661            ensure_range_boundary_budget(partition.len(), frame_budget)?;
662        }
663        for (position, current_row) in partition.iter().enumerate() {
664            let range = match frame.units {
665                WindowFrameUnits::Rows => rows_frame_range(position, partition.len(), frame)?,
666                WindowFrameUnits::Range => {
667                    range_frame_range(position, partition, frame, &window.order_by)?
668                }
669            };
670            let value = value_from_frame(function, current_row, partition, range.as_ref())?;
671            set_output(output, current_row, value, memory)?;
672        }
673        return Ok(());
674    }
675
676    if window.order_by.is_empty() {
677        let range = 0..=partition.len() - 1;
678        for current_row in partition {
679            let value = value_from_frame(function, current_row, partition, Some(&range))?;
680            set_output(output, current_row, value, memory)?;
681        }
682        return Ok(());
683    }
684
685    let mut peer_start = 0;
686    while peer_start < partition.len() {
687        let peer_key = order_values(&partition[peer_start], &window.order_by)?;
688        let mut peer_end = peer_start + 1;
689        while peer_end < partition.len() {
690            let candidate = order_values(&partition[peer_end], &window.order_by)?;
691            if compare_key_values(&candidate, &peer_key, &window.order_by) != Ordering::Equal {
692                break;
693            }
694            peer_end += 1;
695        }
696        let range = 0..=peer_end - 1;
697        for current_row in &partition[peer_start..peer_end] {
698            let value = value_from_frame(function, current_row, partition, Some(&range))?;
699            set_output(output, current_row, value, memory)?;
700        }
701        peer_start = peer_end;
702    }
703    Ok(())
704}
705
706fn value_from_frame(
707    function: &ValueWindowFunction,
708    current_row: &Row,
709    partition: &[Row],
710    range: Option<&RangeInclusive<usize>>,
711) -> Result<SqlValue> {
712    let target = match function {
713        ValueWindowFunction::FirstValue(_) => range.map(|range| *range.start()),
714        ValueWindowFunction::LastValue(_) => range.map(|range| *range.end()),
715        ValueWindowFunction::NthValue { nth, .. } => {
716            let nth = positive_integer_argument(
717                "NTH_VALUE",
718                evaluator::evaluate(nth, &EvalContext::new(&current_row.values))?,
719            )?;
720            range.and_then(|range| {
721                let offset = usize::try_from(nth.checked_sub(1)?).ok()?;
722                range
723                    .start()
724                    .checked_add(offset)
725                    .filter(|target| target <= range.end())
726            })
727        }
728    };
729    let Some(target) = target else {
730        return Ok(SqlValue::Null);
731    };
732    let value = match function {
733        ValueWindowFunction::FirstValue(value)
734        | ValueWindowFunction::LastValue(value)
735        | ValueWindowFunction::NthValue { value, .. } => value,
736    };
737    evaluator::evaluate(value, &EvalContext::new(&partition[target].values))
738}
739
740fn is_default_ordered_frame(frame: &WindowFrame) -> bool {
741    frame.units == WindowFrameUnits::Range
742        && frame.start_bound == WindowFrameBound::UnboundedPreceding
743        && frame.end_bound == WindowFrameBound::CurrentRow
744}
745
746fn evaluate_framed_aggregate(
747    window: &WindowExpr,
748    aggregate: &crate::planner::AggregateExpr,
749    frame: &WindowFrame,
750    partition: &[Row],
751    output: &mut [SqlValue],
752    frame_budget: &mut ExplicitFrameBudget,
753    memory: &mut WindowMemory,
754) -> Result<()> {
755    if frame.units == WindowFrameUnits::Range {
756        ensure_range_boundary_budget(partition.len(), frame_budget)?;
757    }
758    let mut ranges = Vec::with_capacity(partition.len());
759    for position in 0..partition.len() {
760        let range = match frame.units {
761            WindowFrameUnits::Rows => rows_frame_range(position, partition.len(), frame)?,
762            WindowFrameUnits::Range => {
763                range_frame_range(position, partition, frame, &window.order_by)?
764            }
765        };
766        if let Some(range) = &range {
767            let width = range
768                .end()
769                .checked_sub(*range.start())
770                .and_then(|width| width.checked_add(1))
771                .ok_or_else(frame_overflow)?;
772            frame_budget
773                .charge_aggregate_visits(u64::try_from(width).map_err(|_| frame_overflow())?)?;
774        }
775        ranges.push(range);
776    }
777
778    for (current_row, range) in partition.iter().zip(ranges) {
779        let mut accumulator = create_accumulator_for_aggregate(aggregate);
780        let mut accumulator_bytes = accumulator.retained_bytes();
781        memory.reserve_bytes(accumulator_bytes)?;
782        if let Some(range) = range {
783            for row in &partition[range] {
784                update_accumulator(
785                    accumulator.as_mut(),
786                    aggregate_values(aggregate, row)?,
787                    memory,
788                    &mut accumulator_bytes,
789                )?;
790            }
791        }
792        set_output(output, current_row, accumulator.finalize()?, memory)?;
793        memory.release_bytes(accumulator_bytes)?;
794    }
795    Ok(())
796}
797
798fn ensure_range_boundary_budget(
799    partition_len: usize,
800    budget: &mut ExplicitFrameBudget,
801) -> Result<()> {
802    let rows = u64::try_from(partition_len).map_err(|_| frame_overflow())?;
803    let boundary_probes = rows
804        .checked_mul(rows)
805        // Each row can scan its peer range plus both finite boundaries.
806        .and_then(|probes| probes.checked_mul(3))
807        .ok_or_else(frame_overflow)?;
808    budget.charge_range_boundary_probes(boundary_probes)
809}
810
811fn rows_frame_range(
812    position: usize,
813    partition_len: usize,
814    frame: &WindowFrame,
815) -> Result<Option<RangeInclusive<usize>>> {
816    let start = rows_start(position, partition_len, frame.start_bound)?;
817    let end = rows_end(position, partition_len, frame.end_bound)?;
818    Ok(match (start, end) {
819        (Some(start), Some(end)) if start <= end => Some(start..=end),
820        _ => None,
821    })
822}
823
824fn rows_start(
825    position: usize,
826    partition_len: usize,
827    bound: WindowFrameBound,
828) -> Result<Option<usize>> {
829    match bound {
830        WindowFrameBound::UnboundedPreceding => Ok(Some(0)),
831        WindowFrameBound::Preceding(offset) => {
832            let offset = usize::try_from(offset).unwrap_or(usize::MAX);
833            Ok(Some(position.saturating_sub(offset)))
834        }
835        WindowFrameBound::CurrentRow => Ok(Some(position)),
836        WindowFrameBound::Following(offset) => checked_following(position, offset, partition_len),
837        WindowFrameBound::UnboundedFollowing => Ok(Some(partition_len.saturating_sub(1))),
838    }
839}
840
841fn rows_end(
842    position: usize,
843    partition_len: usize,
844    bound: WindowFrameBound,
845) -> Result<Option<usize>> {
846    match bound {
847        WindowFrameBound::UnboundedPreceding => Ok(Some(0)),
848        WindowFrameBound::Preceding(offset) => {
849            let offset = usize::try_from(offset).unwrap_or(usize::MAX);
850            Ok(position.checked_sub(offset))
851        }
852        WindowFrameBound::CurrentRow => Ok(Some(position)),
853        WindowFrameBound::Following(offset) => {
854            Ok(checked_following(position, offset, partition_len)?
855                .or_else(|| partition_len.checked_sub(1)))
856        }
857        WindowFrameBound::UnboundedFollowing => Ok(partition_len.checked_sub(1)),
858    }
859}
860
861fn checked_following(position: usize, offset: u64, partition_len: usize) -> Result<Option<usize>> {
862    let position = u64::try_from(position).map_err(|_| frame_overflow())?;
863    let target = position.checked_add(offset).ok_or_else(frame_overflow)?;
864    let target = usize::try_from(target).map_err(|_| frame_overflow())?;
865    Ok((target < partition_len).then_some(target))
866}
867
868fn range_frame_range(
869    position: usize,
870    partition: &[Row],
871    frame: &WindowFrame,
872    order_by: &[SortExpr],
873) -> Result<Option<RangeInclusive<usize>>> {
874    let sort = order_by
875        .first()
876        .ok_or_else(|| ExecutorError::InvalidOperation {
877            operation: "RANGE window frame".into(),
878            reason: "ORDER BY is required".into(),
879        })?;
880    let current = evaluator::evaluate(&sort.expr, &EvalContext::new(&partition[position].values))?;
881    let peer = peer_range(position, partition, order_by)?;
882    let start = range_boundary(&current, frame.start_bound, true, partition, sort, &peer)?;
883    let end = range_boundary(&current, frame.end_bound, false, partition, sort, &peer)?;
884    Ok(match (start, end) {
885        (Some(start), Some(end)) if start <= end => Some(start..=end),
886        _ => None,
887    })
888}
889
890fn peer_range(
891    position: usize,
892    partition: &[Row],
893    order_by: &[SortExpr],
894) -> Result<RangeInclusive<usize>> {
895    let key = order_values(&partition[position], order_by)?;
896    let mut start = position;
897    while start > 0 {
898        let candidate = order_values(&partition[start - 1], order_by)?;
899        if compare_key_values(&candidate, &key, order_by) != Ordering::Equal {
900            break;
901        }
902        start -= 1;
903    }
904    let mut end = position;
905    while end + 1 < partition.len() {
906        let candidate = order_values(&partition[end + 1], order_by)?;
907        if compare_key_values(&candidate, &key, order_by) != Ordering::Equal {
908            break;
909        }
910        end += 1;
911    }
912    Ok(start..=end)
913}
914
915fn range_boundary(
916    current: &SqlValue,
917    bound: WindowFrameBound,
918    is_start: bool,
919    partition: &[Row],
920    sort: &SortExpr,
921    peer: &RangeInclusive<usize>,
922) -> Result<Option<usize>> {
923    match bound {
924        WindowFrameBound::UnboundedPreceding => Ok(Some(0)),
925        WindowFrameBound::UnboundedFollowing => Ok(partition.len().checked_sub(1)),
926        WindowFrameBound::CurrentRow => {
927            Ok(Some(if is_start { *peer.start() } else { *peer.end() }))
928        }
929        WindowFrameBound::Preceding(_) | WindowFrameBound::Following(_) if current.is_null() => {
930            Ok(Some(if is_start { *peer.start() } else { *peer.end() }))
931        }
932        WindowFrameBound::Preceding(offset) | WindowFrameBound::Following(offset) => {
933            let target = range_target(current, bound, offset, sort.asc)?;
934            let mut matched = None;
935            for (index, row) in partition.iter().enumerate() {
936                let candidate = evaluator::evaluate(&sort.expr, &EvalContext::new(&row.values))?;
937                let Some(ordering) = compare_range_numeric(&candidate, &target)? else {
938                    continue;
939                };
940                let ordering = if sort.asc {
941                    ordering
942                } else {
943                    ordering.reverse()
944                };
945                if is_start {
946                    if ordering != Ordering::Less {
947                        return Ok(Some(index));
948                    }
949                } else if ordering != Ordering::Greater {
950                    matched = Some(index);
951                } else if matched.is_some() {
952                    break;
953                }
954            }
955            Ok(matched)
956        }
957    }
958}
959
960#[derive(Debug, Clone, Copy)]
961enum RangeNumeric {
962    Integer(i128),
963    Float(f64),
964}
965
966fn range_numeric(value: &SqlValue) -> Result<Option<RangeNumeric>> {
967    Ok(match value {
968        SqlValue::Null => None,
969        SqlValue::Integer(value) => Some(RangeNumeric::Integer(i128::from(*value))),
970        SqlValue::BigInt(value) => Some(RangeNumeric::Integer(i128::from(*value))),
971        SqlValue::Float(value) => Some(RangeNumeric::Float(f64::from(*value))),
972        SqlValue::Double(value) => Some(RangeNumeric::Float(*value)),
973        other => {
974            return Err(ExecutorError::InvalidOperation {
975                operation: "RANGE window frame".into(),
976                reason: format!("ORDER BY value must be numeric, got {}", other.type_name()),
977            });
978        }
979    })
980}
981
982fn range_target(
983    current: &SqlValue,
984    bound: WindowFrameBound,
985    offset: u64,
986    asc: bool,
987) -> Result<RangeNumeric> {
988    let value = range_numeric(current)?.ok_or_else(|| ExecutorError::InvalidOperation {
989        operation: "RANGE window frame".into(),
990        reason: "NULL offset target must use its peer group".into(),
991    })?;
992    let preceding = matches!(bound, WindowFrameBound::Preceding(_));
993    let subtract = preceding == asc;
994    match value {
995        RangeNumeric::Integer(value) => {
996            let offset = i128::from(offset);
997            Ok(RangeNumeric::Integer(
998                if subtract {
999                    value.checked_sub(offset)
1000                } else {
1001                    value.checked_add(offset)
1002                }
1003                .ok_or_else(frame_overflow)?,
1004            ))
1005        }
1006        RangeNumeric::Float(value) => {
1007            let target = if subtract {
1008                value - offset as f64
1009            } else {
1010                value + offset as f64
1011            };
1012            if target.is_finite() {
1013                Ok(RangeNumeric::Float(target))
1014            } else {
1015                Err(frame_overflow())
1016            }
1017        }
1018    }
1019}
1020
1021fn compare_range_numeric(left: &SqlValue, right: &RangeNumeric) -> Result<Option<Ordering>> {
1022    let Some(left) = range_numeric(left)? else {
1023        return Ok(None);
1024    };
1025    let ordering = match (left, *right) {
1026        (RangeNumeric::Integer(left), RangeNumeric::Integer(right)) => left.cmp(&right),
1027        (RangeNumeric::Integer(left), RangeNumeric::Float(right)) => (left as f64)
1028            .partial_cmp(&right)
1029            .ok_or_else(frame_overflow)?,
1030        (RangeNumeric::Float(left), RangeNumeric::Integer(right)) => left
1031            .partial_cmp(&(right as f64))
1032            .ok_or_else(frame_overflow)?,
1033        (RangeNumeric::Float(left), RangeNumeric::Float(right)) => {
1034            left.partial_cmp(&right).ok_or_else(frame_overflow)?
1035        }
1036    };
1037    Ok(Some(ordering))
1038}
1039
1040fn frame_overflow() -> ExecutorError {
1041    ExecutorError::InvalidOperation {
1042        operation: "window frame".into(),
1043        reason: "window frame offset or resource count overflow".into(),
1044    }
1045}
1046
1047#[derive(Debug, Clone, Copy)]
1048enum OffsetDirection {
1049    Preceding,
1050    Following,
1051}
1052
1053fn evaluate_offset_window(
1054    function: &OffsetWindowFunction,
1055    direction: OffsetDirection,
1056    partition: &[Row],
1057    output: &mut [SqlValue],
1058    memory: &mut WindowMemory,
1059) -> Result<()> {
1060    for (position, current_row) in partition.iter().enumerate() {
1061        let current_context = EvalContext::new(&current_row.values);
1062        let value = match evaluate_offset(function.offset.as_ref(), &current_context)? {
1063            None => SqlValue::Null,
1064            Some(offset) => {
1065                match addressed_position(position, offset, direction, partition.len()) {
1066                    Some(target) => evaluator::evaluate(
1067                        &function.value,
1068                        &EvalContext::new(&partition[target].values),
1069                    )?,
1070                    None => function
1071                        .default
1072                        .as_ref()
1073                        .map(|default| evaluator::evaluate(default, &current_context))
1074                        .transpose()?
1075                        .unwrap_or(SqlValue::Null),
1076                }
1077            }
1078        };
1079        set_output(output, current_row, value, memory)?;
1080    }
1081    Ok(())
1082}
1083
1084fn evaluate_offset(
1085    offset: Option<&crate::planner::TypedExpr>,
1086    context: &EvalContext<'_>,
1087) -> Result<Option<u64>> {
1088    let value = offset
1089        .map(|expr| evaluator::evaluate(expr, context))
1090        .transpose()?
1091        .unwrap_or(SqlValue::Integer(1));
1092    match value {
1093        SqlValue::Null => Ok(None),
1094        SqlValue::Integer(value) => non_negative_offset(i64::from(value)).map(Some),
1095        SqlValue::BigInt(value) => non_negative_offset(value).map(Some),
1096        other => Err(ExecutorError::InvalidOperation {
1097            operation: "window offset".into(),
1098            reason: format!("offset must be INTEGER, got {}", other.type_name()),
1099        }),
1100    }
1101}
1102
1103fn non_negative_offset(offset: i64) -> Result<u64> {
1104    u64::try_from(offset).map_err(|_| ExecutorError::InvalidOperation {
1105        operation: "window offset".into(),
1106        reason: "offset must be non-negative".into(),
1107    })
1108}
1109
1110fn addressed_position(
1111    position: usize,
1112    offset: u64,
1113    direction: OffsetDirection,
1114    partition_len: usize,
1115) -> Option<usize> {
1116    let position = u64::try_from(position).ok()?;
1117    let target = match direction {
1118        OffsetDirection::Preceding => position.checked_sub(offset)?,
1119        OffsetDirection::Following => position.checked_add(offset)?,
1120    };
1121    let target = usize::try_from(target).ok()?;
1122    (target < partition_len).then_some(target)
1123}
1124
1125fn order_values(row: &Row, order_by: &[SortExpr]) -> Result<Vec<SqlValue>> {
1126    order_by
1127        .iter()
1128        .map(|sort| evaluator::evaluate(&sort.expr, &EvalContext::new(&row.values)))
1129        .collect()
1130}
1131
1132fn aggregate_values(aggregate: &crate::planner::AggregateExpr, row: &Row) -> Result<Vec<SqlValue>> {
1133    aggregate
1134        .arg
1135        .iter()
1136        .chain(&aggregate.extra_args)
1137        .map(|arg| evaluator::evaluate(arg, &EvalContext::new(&row.values)))
1138        .collect()
1139}
1140
1141fn update_accumulator(
1142    accumulator: &mut dyn Accumulator,
1143    values: Vec<SqlValue>,
1144    memory: &mut WindowMemory,
1145    accounted_bytes: &mut u64,
1146) -> Result<()> {
1147    let temporary_bytes = values.iter().map(ByteSized::estimated_bytes).sum();
1148    memory.reserve_bytes(temporary_bytes)?;
1149    accumulator.update_values(&values)?;
1150    let retained_bytes = accumulator.retained_bytes();
1151    if retained_bytes > *accounted_bytes {
1152        memory.reserve_bytes(retained_bytes - *accounted_bytes)?;
1153    } else if retained_bytes < *accounted_bytes {
1154        memory.release_bytes(*accounted_bytes - retained_bytes)?;
1155    }
1156    *accounted_bytes = retained_bytes;
1157    memory.release_bytes(temporary_bytes)?;
1158    Ok(())
1159}
1160
1161fn set_repeated_output(
1162    output: &mut [SqlValue],
1163    rows: &[Row],
1164    value: SqlValue,
1165    memory: &mut WindowMemory,
1166) -> Result<()> {
1167    let template_bytes = value.estimated_bytes();
1168    memory.reserve_bytes(template_bytes)?;
1169    for row in rows {
1170        set_output(output, row, value.clone(), memory)?;
1171    }
1172    memory.release_bytes(template_bytes)
1173}
1174
1175fn set_output(
1176    output: &mut [SqlValue],
1177    row: &Row,
1178    value: SqlValue,
1179    memory: &mut WindowMemory,
1180) -> Result<()> {
1181    let index = usize::try_from(row.row_id).map_err(|_| ExecutorError::InvalidOperation {
1182        operation: "window function".into(),
1183        reason: "input row index exceeds usize".into(),
1184    })?;
1185    let slot = output
1186        .get_mut(index)
1187        .ok_or_else(|| ExecutorError::InvalidOperation {
1188            operation: "window function".into(),
1189            reason: format!("input row index {index} is out of bounds"),
1190        })?;
1191    memory.reserve_bytes(value.estimated_bytes())?;
1192    *slot = value;
1193    Ok(())
1194}
1195
1196#[cfg(test)]
1197mod frame_tests {
1198    use super::*;
1199    use crate::Span;
1200    use crate::executor::memory::SpillPolicy;
1201    use crate::planner::aggregate_expr::AggregateExpr;
1202    use crate::planner::typed_expr::{TypedExpr, TypedExprKind};
1203    use crate::planner::types::ResolvedType;
1204
1205    fn row_number_window() -> WindowExpr {
1206        WindowExpr {
1207            function: WindowFunction::RowNumber,
1208            partition_by: Vec::new(),
1209            order_by: Vec::new(),
1210            frame: None,
1211            result_type: crate::planner::types::ResolvedType::BigInt,
1212        }
1213    }
1214
1215    fn text_rows() -> Vec<Row> {
1216        vec![
1217            Row::new(0, vec![SqlValue::Text("a".repeat(100))]),
1218            Row::new(1, vec![SqlValue::Text("b".repeat(100))]),
1219        ]
1220    }
1221
1222    fn rows(start_bound: WindowFrameBound, end_bound: WindowFrameBound) -> WindowFrame {
1223        WindowFrame {
1224            units: WindowFrameUnits::Rows,
1225            start_bound,
1226            end_bound,
1227        }
1228    }
1229
1230    fn column(index: usize, resolved_type: ResolvedType) -> TypedExpr {
1231        TypedExpr {
1232            kind: TypedExprKind::ColumnRef {
1233                table: "test".into(),
1234                column: format!("column_{index}"),
1235                column_index: index,
1236            },
1237            resolved_type,
1238            span: Span::default(),
1239        }
1240    }
1241
1242    #[test]
1243    fn rows_boundaries_clamp_and_empty_without_crossing_a_partition() {
1244        let frame = rows(
1245            WindowFrameBound::Preceding(2),
1246            WindowFrameBound::Following(1),
1247        );
1248        assert_eq!(rows_frame_range(0, 3, &frame).unwrap(), Some(0..=1));
1249        assert_eq!(rows_frame_range(2, 3, &frame).unwrap(), Some(0..=2));
1250
1251        let empty = rows(
1252            WindowFrameBound::Following(2),
1253            WindowFrameBound::Following(1),
1254        );
1255        assert_eq!(rows_frame_range(0, 3, &empty).unwrap(), None);
1256        assert_eq!(rows_frame_range(2, 3, &empty).unwrap(), None);
1257    }
1258
1259    #[test]
1260    fn following_offset_arithmetic_overflow_is_a_controlled_error() {
1261        let error = rows_frame_range(
1262            1,
1263            3,
1264            &rows(
1265                WindowFrameBound::Following(u64::MAX),
1266                WindowFrameBound::UnboundedFollowing,
1267            ),
1268        )
1269        .expect_err("position + offset must not wrap");
1270        assert!(error.to_string().contains("overflow"));
1271    }
1272
1273    #[test]
1274    fn range_boundary_work_is_rejected_before_quadratic_scanning() {
1275        let mut budget = ExplicitFrameBudget::default();
1276        ensure_range_boundary_budget(577, &mut budget)
1277            .expect("577 rows stay within one million worst-case probes");
1278        let mut budget = ExplicitFrameBudget::default();
1279        let error = ensure_range_boundary_budget(578, &mut budget)
1280            .expect_err("578 rows exceed three times n squared probe budget");
1281        assert!(matches!(error, ExecutorError::ResourceExhausted { .. }));
1282    }
1283
1284    #[test]
1285    fn overlapping_window_materializations_are_fail_closed_by_byte_limit() {
1286        let rows = text_rows();
1287        let single_copy_bytes = estimated_cloned_rows_bytes(&rows).unwrap();
1288        let input = VecIterator::new(
1289            rows,
1290            vec![ColumnMetadata::new(
1291                "payload",
1292                crate::planner::types::ResolvedType::Text,
1293            )],
1294        );
1295        // Each row set fits independently. The original rows, sortable copy,
1296        // partition storage, and output overlap, so the operator as a whole
1297        // must reject a limit just below two standalone copies.
1298        let spill_dir = tempfile::tempdir().unwrap();
1299        let policy = MemoryPolicy::new(
1300            Some(single_copy_bytes.checked_mul(2).unwrap() - 1),
1301            SpillPolicy::SpillToDisk {
1302                directory: spill_dir.path().to_path_buf(),
1303            },
1304        );
1305
1306        let error = match WindowIterator::new(input, vec![row_number_window()], Some(&policy)) {
1307            Ok(_) => panic!("overlapping row copies must share one byte budget"),
1308            Err(error) => error,
1309        };
1310
1311        assert!(matches!(error, ExecutorError::ResourceExhausted { .. }));
1312    }
1313
1314    #[test]
1315    fn window_memory_accounting_is_released_as_output_is_drained() {
1316        let input = VecIterator::new(
1317            text_rows(),
1318            vec![ColumnMetadata::new(
1319                "payload",
1320                crate::planner::types::ResolvedType::Text,
1321            )],
1322        );
1323        let policy = MemoryPolicy::new(Some(10_000), SpillPolicy::FailFast);
1324        let mut window =
1325            WindowIterator::new(input, vec![row_number_window()], Some(&policy)).unwrap();
1326        let materialized_bytes = window.accounted_memory_bytes();
1327
1328        window.next_row().unwrap().unwrap();
1329        let one_row_remaining_bytes = window.accounted_memory_bytes();
1330        window.next_row().unwrap().unwrap();
1331
1332        assert!(materialized_bytes > one_row_remaining_bytes);
1333        assert!(one_row_remaining_bytes > 0);
1334        assert_eq!(window.accounted_memory_bytes(), 0);
1335    }
1336
1337    #[test]
1338    fn explicit_frame_visit_budget_is_shared_across_partitions() {
1339        let input_rows = (0_u64..2_000)
1340            .map(|row_id| {
1341                Row::new(
1342                    row_id,
1343                    vec![
1344                        SqlValue::Integer((row_id / 1_000) as i32),
1345                        SqlValue::Integer(row_id as i32),
1346                    ],
1347                )
1348            })
1349            .collect::<Vec<_>>();
1350        let input = VecIterator::new(
1351            input_rows,
1352            vec![
1353                ColumnMetadata::new("partition", ResolvedType::Integer),
1354                ColumnMetadata::new("value", ResolvedType::Integer),
1355            ],
1356        );
1357        let window = WindowExpr {
1358            function: WindowFunction::Aggregate(AggregateExpr::count_star()),
1359            partition_by: vec![column(0, ResolvedType::Integer)],
1360            order_by: Vec::new(),
1361            frame: Some(rows(
1362                WindowFrameBound::UnboundedPreceding,
1363                WindowFrameBound::CurrentRow,
1364            )),
1365            result_type: ResolvedType::BigInt,
1366        };
1367
1368        let error = match WindowIterator::new(input, vec![window], None) {
1369            Ok(_) => panic!("all partitions of one window expression must share the visit cap"),
1370            Err(error) => error,
1371        };
1372
1373        assert!(matches!(error, ExecutorError::ResourceExhausted { .. }));
1374    }
1375
1376    #[test]
1377    fn distinct_frame_retention_shares_the_window_memory_budget() {
1378        let input_rows = (0_u64..3)
1379            .map(|row_id| {
1380                Row::new(
1381                    row_id,
1382                    vec![SqlValue::Text(format!("{row_id}-{}", "x".repeat(600)))],
1383                )
1384            })
1385            .collect::<Vec<_>>();
1386        let input = VecIterator::new(
1387            input_rows,
1388            vec![ColumnMetadata::new("payload", ResolvedType::Text)],
1389        );
1390        let window = WindowExpr {
1391            function: WindowFunction::Aggregate(AggregateExpr::count(
1392                column(0, ResolvedType::Text),
1393                true,
1394            )),
1395            partition_by: Vec::new(),
1396            order_by: Vec::new(),
1397            frame: Some(rows(
1398                WindowFrameBound::UnboundedPreceding,
1399                WindowFrameBound::CurrentRow,
1400            )),
1401            result_type: ResolvedType::BigInt,
1402        };
1403        let policy = MemoryPolicy::new(Some(5_500), SpillPolicy::FailFast);
1404
1405        let error = match WindowIterator::new(input, vec![window], Some(&policy)) {
1406            Ok(_) => panic!("DISTINCT dedup keys must overlap the materialized window rows"),
1407            Err(error) => error,
1408        };
1409
1410        assert!(matches!(error, ExecutorError::ResourceExhausted { .. }));
1411    }
1412
1413    #[test]
1414    fn min_text_frame_retention_shares_the_window_memory_budget() {
1415        let input_rows = (0_u64..3)
1416            .map(|row_id| {
1417                Row::new(
1418                    row_id,
1419                    vec![SqlValue::Text(format!("{row_id}-{}", "x".repeat(600)))],
1420                )
1421            })
1422            .collect::<Vec<_>>();
1423        let input = VecIterator::new(
1424            input_rows,
1425            vec![ColumnMetadata::new("payload", ResolvedType::Text)],
1426        );
1427        let aggregate = AggregateExpr::min(column(0, ResolvedType::Text));
1428        let window = WindowExpr {
1429            function: WindowFunction::Aggregate(aggregate),
1430            partition_by: Vec::new(),
1431            order_by: Vec::new(),
1432            frame: Some(rows(
1433                WindowFrameBound::UnboundedPreceding,
1434                WindowFrameBound::CurrentRow,
1435            )),
1436            result_type: ResolvedType::Text,
1437        };
1438        let policy = MemoryPolicy::new(Some(6_500), SpillPolicy::FailFast);
1439
1440        let error = match WindowIterator::new(input, vec![window], Some(&policy)) {
1441            Ok(_) => panic!("MIN text state must overlap materialized rows and output values"),
1442            Err(error) => error,
1443        };
1444
1445        assert!(matches!(error, ExecutorError::ResourceExhausted { .. }));
1446    }
1447
1448    #[test]
1449    fn implicit_min_text_temporaries_share_the_window_memory_budget() {
1450        let input_rows = (0_u64..3)
1451            .map(|row_id| {
1452                Row::new(
1453                    row_id,
1454                    vec![SqlValue::Text(format!("{row_id}-{}", "x".repeat(600)))],
1455                )
1456            })
1457            .collect::<Vec<_>>();
1458        let input = VecIterator::new(
1459            input_rows,
1460            vec![ColumnMetadata::new("payload", ResolvedType::Text)],
1461        );
1462        let aggregate = AggregateExpr::min(column(0, ResolvedType::Text));
1463        let window = WindowExpr {
1464            function: WindowFunction::Aggregate(aggregate),
1465            partition_by: Vec::new(),
1466            order_by: Vec::new(),
1467            frame: None,
1468            result_type: ResolvedType::Text,
1469        };
1470        let policy = MemoryPolicy::new(Some(7_000), SpillPolicy::FailFast);
1471
1472        let error = match WindowIterator::new(input, vec![window], Some(&policy)) {
1473            Ok(_) => panic!("MIN input and finalize templates must overlap retained state"),
1474            Err(error) => error,
1475        };
1476
1477        assert!(matches!(error, ExecutorError::ResourceExhausted { .. }));
1478    }
1479
1480    #[test]
1481    fn signed_zero_partition_values_have_sql_sort_equality() {
1482        let exprs = vec![column(0, ResolvedType::Double)];
1483        let negative = Row::new(0, vec![SqlValue::Double(-0.0)]);
1484        let positive = Row::new(1, vec![SqlValue::Double(0.0)]);
1485
1486        let negative = evaluate_exprs(&negative, &exprs).unwrap();
1487        let positive = evaluate_exprs(&positive, &exprs).unwrap();
1488        assert!(partition_values_equal(&negative, &positive));
1489    }
1490
1491    #[test]
1492    fn signed_zero_range_current_row_values_share_one_peer_group() {
1493        let partition = vec![
1494            Row::new(0, vec![SqlValue::Double(-0.0)]),
1495            Row::new(1, vec![SqlValue::Double(0.0)]),
1496        ];
1497        let order_by = vec![SortExpr::asc(column(0, ResolvedType::Double))];
1498
1499        assert_eq!(peer_range(0, &partition, &order_by).unwrap(), 0..=1);
1500        assert_eq!(peer_range(1, &partition, &order_by).unwrap(), 0..=1);
1501    }
1502
1503    #[test]
1504    fn signed_zero_partition_wiring_keeps_one_partition() {
1505        let input = VecIterator::new(
1506            vec![
1507                Row::new(0, vec![SqlValue::Double(-0.0)]),
1508                Row::new(1, vec![SqlValue::Double(0.0)]),
1509            ],
1510            vec![ColumnMetadata::new("value", ResolvedType::Double)],
1511        );
1512        let window = WindowExpr {
1513            function: WindowFunction::Aggregate(AggregateExpr::count_star()),
1514            partition_by: vec![column(0, ResolvedType::Double)],
1515            order_by: Vec::new(),
1516            frame: None,
1517            result_type: ResolvedType::BigInt,
1518        };
1519        let mut iterator = WindowIterator::new(input, vec![window], None).unwrap();
1520        let mut counts = Vec::new();
1521        while let Some(row) = iterator.next_row() {
1522            counts.push(row.unwrap().values[1].clone());
1523        }
1524
1525        assert_eq!(counts, vec![SqlValue::BigInt(2), SqlValue::BigInt(2)]);
1526    }
1527
1528    #[test]
1529    fn incomparable_values_are_not_arbitrary_peers_or_partitions() {
1530        let scalar_exprs = [column(0, ResolvedType::Double)];
1531        assert!(!partition_values_equal(
1532            &[SqlValue::Double(1.0)],
1533            &[SqlValue::Double(f64::NAN)]
1534        ));
1535        assert!(partition_values_equal(
1536            &[SqlValue::Double(f64::NAN)],
1537            &[SqlValue::Double(f64::NAN)]
1538        ));
1539        assert!(!partition_values_equal(
1540            &[SqlValue::Vector(vec![0.0])],
1541            &[SqlValue::Vector(vec![1.0])]
1542        ));
1543
1544        let partition = vec![
1545            Row::new(0, vec![SqlValue::Double(1.0)]),
1546            Row::new(1, vec![SqlValue::Double(f64::NAN)]),
1547            Row::new(2, vec![SqlValue::Double(f64::NAN)]),
1548        ];
1549        let order_by = vec![SortExpr::asc(scalar_exprs[0].clone())];
1550        assert_eq!(peer_range(0, &partition, &order_by).unwrap(), 0..=0);
1551        assert_eq!(peer_range(1, &partition, &order_by).unwrap(), 1..=2);
1552
1553        let rank_window = WindowExpr {
1554            function: WindowFunction::Rank,
1555            partition_by: Vec::new(),
1556            order_by,
1557            frame: None,
1558            result_type: ResolvedType::BigInt,
1559        };
1560        let mut output = vec![SqlValue::Null; partition.len()];
1561        evaluate_partition(
1562            &rank_window,
1563            &partition,
1564            &mut output,
1565            &mut ExplicitFrameBudget::default(),
1566            &mut WindowMemory::new(None),
1567        )
1568        .unwrap();
1569        assert_eq!(
1570            output,
1571            vec![
1572                SqlValue::BigInt(1),
1573                SqlValue::BigInt(2),
1574                SqlValue::BigInt(2)
1575            ]
1576        );
1577    }
1578}