1use 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
22const 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
67pub 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 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 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(¤t_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 .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(¤t, frame.start_bound, true, partition, sort, &peer)?;
883 let end = range_boundary(¤t, 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(¤t_row.values);
1062 let value = match evaluate_offset(function.offset.as_ref(), ¤t_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, ¤t_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 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}