Skip to main content

vibesql_executor/evaluator/window/
frames.rs

1//! Frame calculation for window functions
2//!
3//! Calculates frame boundaries (ROWS mode) for window function evaluation.
4//! Supports SQL:2011 EXCLUDE clause for excluding rows from the frame.
5
6use std::cmp::Ordering;
7use std::ops::Range;
8
9use vibesql_ast::{
10    Expression, FrameBound, FrameExclude, FrameUnit, NullsOrder, OrderByItem, OrderDirection,
11    UnaryOperator, WindowFrame,
12};
13use vibesql_types::SqlValue;
14
15use vibesql_storage::Row;
16
17use super::partitioning::Partition;
18use super::sorting::{compare_values, compare_values_with_collation};
19
20/// Validate a window frame specification, returning an error if invalid.
21///
22/// This checks that frame offsets are non-negative numbers.
23pub fn validate_frame(frame_spec: &Option<WindowFrame>) -> Result<(), String> {
24    let frame = match frame_spec {
25        Some(f) => f,
26        None => return Ok(()),
27    };
28
29    // Validate start bound
30    validate_frame_bound(&frame.start, true)?;
31
32    // Validate end bound if present
33    if let Some(ref end_bound) = frame.end {
34        validate_frame_bound(end_bound, false)?;
35    }
36
37    Ok(())
38}
39
40/// Validate that RANGE frames with offset PRECEDING/FOLLOWING have exactly one ORDER BY column.
41///
42/// Per the SQL standard and SQLite behavior, RANGE with numeric offsets (N PRECEDING or
43/// N FOLLOWING) requires exactly one ORDER BY expression. Without ORDER BY, or with
44/// multiple ORDER BY columns, an error is returned.
45pub fn validate_range_order_by(
46    frame_spec: &Option<WindowFrame>,
47    order_by: &Option<Vec<OrderByItem>>,
48) -> Result<(), String> {
49    let frame = match frame_spec {
50        Some(f) => f,
51        None => return Ok(()),
52    };
53
54    // Only applies to RANGE frames
55    if !matches!(frame.unit, FrameUnit::Range) {
56        return Ok(());
57    }
58
59    // Check if either bound uses a numeric offset (N PRECEDING or N FOLLOWING)
60    let has_offset_bound =
61        matches!(frame.start, FrameBound::Preceding(_) | FrameBound::Following(_))
62            || frame.end.as_ref().is_some_and(|end| {
63                matches!(end, FrameBound::Preceding(_) | FrameBound::Following(_))
64            });
65
66    if !has_offset_bound {
67        return Ok(());
68    }
69
70    // RANGE with offset requires exactly one ORDER BY column
71    let order_by_count = order_by.as_ref().map_or(0, |items| items.len());
72    if order_by_count != 1 {
73        return Err(
74            "RANGE with offset PRECEDING/FOLLOWING requires one ORDER BY expression".to_string()
75        );
76    }
77
78    Ok(())
79}
80
81/// Validate a single frame bound
82fn validate_frame_bound(bound: &FrameBound, is_start: bool) -> Result<(), String> {
83    match bound {
84        FrameBound::UnboundedPreceding
85        | FrameBound::UnboundedFollowing
86        | FrameBound::CurrentRow => Ok(()),
87
88        FrameBound::Preceding(offset_expr) | FrameBound::Following(offset_expr) => {
89            let bound_type = if is_start { "starting" } else { "ending" };
90            match evaluate_offset_expr(offset_expr) {
91                Err(_) => {
92                    // Non-constant expression (e.g., column ref) used as frame offset
93                    Err(format!("frame {} offset must be a non-negative integer", bound_type))
94                }
95                Ok(Some(offset)) if offset < 0.0 => {
96                    Err(format!("frame {} offset must be a non-negative number", bound_type))
97                }
98                Ok(None) => {
99                    // NULL, invalid string, blob - all treated as invalid numbers
100                    Err(format!("frame {} offset must be a non-negative number", bound_type))
101                }
102                Ok(Some(_)) => Ok(()), // Valid non-negative number
103            }
104        }
105    }
106}
107
108/// Evaluate an offset expression to get its numeric value.
109///
110/// Handles literal integers, floats, strings (parsed as numbers), and unary minus expressions.
111/// Returns None to indicate the value should be treated as invalid (for error reporting).
112fn evaluate_offset_expr(expr: &Expression) -> Result<Option<f64>, String> {
113    match expr {
114        Expression::Literal(SqlValue::Integer(n)) => Ok(Some(*n as f64)),
115        Expression::Literal(SqlValue::Smallint(n)) => Ok(Some(*n as f64)),
116        Expression::Literal(SqlValue::Bigint(n)) => Ok(Some(*n as f64)),
117        Expression::Literal(SqlValue::Unsigned(n)) => Ok(Some(*n as f64)),
118        Expression::Literal(SqlValue::Float(f)) => Ok(Some(*f as f64)),
119        Expression::Literal(SqlValue::Real(f)) => Ok(Some(*f)),
120        Expression::Literal(SqlValue::Double(f)) => Ok(Some(*f)),
121        Expression::Literal(SqlValue::Numeric(f)) => Ok(Some(*f)),
122        Expression::Literal(SqlValue::Null) => {
123            // NULL is treated as invalid (not a number)
124            Ok(None)
125        }
126        Expression::Literal(SqlValue::Character(s)) | Expression::Literal(SqlValue::Varchar(s)) => {
127            // Try to parse string as a number (SQLite-compatible behavior)
128            let s_str = s.as_str();
129            if s_str.is_empty() {
130                // Empty string is invalid
131                Ok(None)
132            } else if let Ok(n) = s_str.parse::<f64>() {
133                Ok(Some(n))
134            } else {
135                // String that can't be parsed as number is invalid
136                Ok(None)
137            }
138        }
139        Expression::Literal(SqlValue::Blob(_)) => {
140            // Blob is invalid
141            Ok(None)
142        }
143        Expression::UnaryOp { op: UnaryOperator::Minus, expr } => {
144            match evaluate_offset_expr(expr)? {
145                Some(inner) => Ok(Some(-inner)),
146                None => Ok(None),
147            }
148        }
149        Expression::UnaryOp { op: UnaryOperator::Plus, expr } => evaluate_offset_expr(expr),
150        _ => Err("frame offset must be a constant expression".to_string()),
151    }
152}
153
154/// Calculate frame boundaries for a given row in a partition
155///
156/// Returns a `Range<usize>` representing the [start, end) indices of rows in the frame.
157/// Supports ROWS, RANGE, and GROUPS frame semantics.
158///
159/// The `eval_fn` closure evaluates ORDER BY expressions against rows,
160/// supporting complex expressions (BinaryOp, Function, etc.).
161pub fn calculate_frame<F>(
162    partition: &Partition,
163    current_row_idx: usize,
164    order_by: &Option<Vec<OrderByItem>>,
165    frame_spec: &Option<WindowFrame>,
166    eval_fn: &F,
167) -> Range<usize>
168where
169    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
170{
171    let partition_size = partition.len();
172
173    // Default frame depends on whether there's an ORDER BY:
174    // - Without ORDER BY: entire partition (all rows)
175    // - With ORDER BY: RANGE UNBOUNDED PRECEDING to CURRENT ROW
176    let frame = match frame_spec {
177        Some(f) => f,
178        None => {
179            // Check if there's an ORDER BY clause
180            let has_order_by = order_by.as_ref().is_some_and(|items| !items.is_empty());
181
182            if has_order_by {
183                // Default with ORDER BY: RANGE UNBOUNDED PRECEDING TO CURRENT ROW
184                // This means all rows from start to current row's peer group (inclusive)
185                return calculate_range_frame(
186                    partition,
187                    current_row_idx,
188                    order_by,
189                    &FrameBound::UnboundedPreceding,
190                    &Some(FrameBound::CurrentRow),
191                    eval_fn,
192                );
193            } else {
194                // Default without ORDER BY: entire partition
195                return 0..partition_size;
196            }
197        }
198    };
199
200    // Dispatch based on frame unit
201    match frame.unit {
202        FrameUnit::Rows => {
203            calculate_rows_frame(partition, current_row_idx, &frame.start, &frame.end)
204        }
205        FrameUnit::Range => calculate_range_frame(
206            partition,
207            current_row_idx,
208            order_by,
209            &frame.start,
210            &frame.end,
211            eval_fn,
212        ),
213        FrameUnit::Groups => calculate_groups_frame(
214            partition,
215            current_row_idx,
216            order_by,
217            &frame.start,
218            &frame.end,
219            eval_fn,
220        ),
221    }
222}
223
224/// Calculate frame boundaries for ROWS mode (physical row-based)
225fn calculate_rows_frame(
226    partition: &Partition,
227    current_row_idx: usize,
228    start: &FrameBound,
229    end: &Option<FrameBound>,
230) -> Range<usize> {
231    let partition_size = partition.len();
232
233    // Calculate start boundary
234    let start_idx = calculate_rows_boundary(start, current_row_idx, partition_size, true);
235
236    // Calculate end boundary
237    let end_idx = match end {
238        Some(end_bound) => {
239            calculate_rows_boundary(end_bound, current_row_idx, partition_size, false)
240        }
241        None => current_row_idx + 1, // Default: CURRENT ROW (inclusive, so +1 for Range)
242    };
243
244    // Ensure valid range
245    let start = start_idx.min(partition_size);
246    let end = end_idx.min(partition_size).max(start);
247
248    start..end
249}
250
251/// Calculate frame boundaries for RANGE mode (value-based)
252///
253/// RANGE frames are based on the ORDER BY expression values:
254/// - UNBOUNDED PRECEDING/FOLLOWING: Start/end of partition
255/// - CURRENT ROW: All rows with same ORDER BY values (peers)
256/// - N PRECEDING: Rows where ORDER BY value >= current_value - N
257/// - N FOLLOWING: Rows where ORDER BY value <= current_value + N
258fn calculate_range_frame<F>(
259    partition: &Partition,
260    current_row_idx: usize,
261    order_by: &Option<Vec<OrderByItem>>,
262    start: &FrameBound,
263    end: &Option<FrameBound>,
264    eval_fn: &F,
265) -> Range<usize>
266where
267    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
268{
269    let partition_size = partition.len();
270
271    if partition_size == 0 || current_row_idx >= partition_size {
272        return 0..0;
273    }
274
275    // Without ORDER BY, RANGE mode treats entire partition as the frame
276    let order_items = match order_by {
277        Some(items) if !items.is_empty() => items,
278        _ => return 0..partition_size,
279    };
280
281    // Get the current row's ORDER BY value (use first ORDER BY expression)
282    let current_row = &partition.rows[current_row_idx];
283    let current_value = eval_fn(&order_items[0].expr, current_row).unwrap_or(SqlValue::Null);
284
285    // Calculate start boundary
286    let start_idx = calculate_range_boundary(
287        partition,
288        current_row_idx,
289        order_items,
290        &current_value,
291        start,
292        true,
293        eval_fn,
294    );
295
296    // Calculate end boundary
297    let end_idx = match end {
298        Some(end_bound) => calculate_range_boundary(
299            partition,
300            current_row_idx,
301            order_items,
302            &current_value,
303            end_bound,
304            false,
305            eval_fn,
306        ),
307        None => {
308            // Default: CURRENT ROW - find last peer
309            find_last_peer(partition, current_row_idx, order_items, eval_fn) + 1
310        }
311    };
312
313    // Ensure valid range
314    let start = start_idx.min(partition_size);
315    let end = end_idx.min(partition_size).max(start);
316
317    start..end
318}
319
320/// Calculate a single RANGE mode boundary
321///
322/// Note: The meaning of PRECEDING/FOLLOWING depends on sort direction:
323/// - ASC: PRECEDING = smaller values, FOLLOWING = larger values
324/// - DESC: PRECEDING = larger values, FOLLOWING = smaller values
325fn calculate_range_boundary<F>(
326    partition: &Partition,
327    current_row_idx: usize,
328    order_items: &[OrderByItem],
329    current_value: &SqlValue,
330    bound: &FrameBound,
331    is_start: bool,
332    eval_fn: &F,
333) -> usize
334where
335    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
336{
337    use vibesql_ast::OrderDirection;
338
339    let partition_size = partition.len();
340
341    // Check if we have DESC ordering (affects PRECEDING/FOLLOWING semantics)
342    let is_desc =
343        order_items.first().is_some_and(|item| matches!(item.direction, OrderDirection::Desc));
344
345    match bound {
346        FrameBound::UnboundedPreceding => 0,
347        FrameBound::UnboundedFollowing => partition_size,
348
349        FrameBound::CurrentRow => {
350            if is_start {
351                // Find first peer (row with same ORDER BY values)
352                find_first_peer(partition, current_row_idx, order_items, eval_fn)
353            } else {
354                // Find last peer + 1 (exclusive end)
355                find_last_peer(partition, current_row_idx, order_items, eval_fn) + 1
356            }
357        }
358
359        FrameBound::Preceding(offset_expr) => {
360            let offset = get_numeric_offset(offset_expr);
361
362            // For DESC order, PRECEDING means larger values (add offset)
363            // For ASC order, PRECEDING means smaller values (subtract offset)
364            // For non-numeric values (text/blob/NULL) the arithmetic is a no-op,
365            // so the target collapses to the current key (SQLite: the frame
366            // degenerates to the current row's peer group).
367            let target_value = if is_desc {
368                add_to_value(current_value, offset)
369            } else {
370                subtract_from_value(current_value, offset)
371            };
372
373            if is_start {
374                // First row at-or-after the target in window order
375                find_first_row_at_or_after(partition, order_items, &target_value, eval_fn)
376            } else {
377                // Last row at-or-before the target in window order (exclusive end).
378                // If no matching row exists, return 0 (empty frame end)
379                find_last_row_at_or_before(partition, order_items, &target_value, eval_fn)
380                    .map_or(0, |i| i + 1)
381            }
382        }
383
384        FrameBound::Following(offset_expr) => {
385            let offset = get_numeric_offset(offset_expr);
386
387            // For DESC order, FOLLOWING means smaller values (subtract offset)
388            // For ASC order, FOLLOWING means larger values (add offset)
389            let target_value = if is_desc {
390                subtract_from_value(current_value, offset)
391            } else {
392                add_to_value(current_value, offset)
393            };
394
395            if is_start {
396                find_first_row_at_or_after(partition, order_items, &target_value, eval_fn)
397            } else {
398                find_last_row_at_or_before(partition, order_items, &target_value, eval_fn)
399                    .map_or(0, |i| i + 1)
400            }
401        }
402    }
403}
404
405/// Whether NULL ORDER BY keys sort before non-NULL keys in the sorted partition.
406///
407/// Honors an explicit NULLS FIRST/LAST modifier; otherwise uses SQLite's
408/// defaults (NULLS FIRST for ASC, NULLS LAST for DESC).
409fn nulls_sort_first(item: &OrderByItem) -> bool {
410    match item.nulls_order {
411        Some(NullsOrder::First) => true,
412        Some(NullsOrder::Last) => false,
413        None => matches!(item.direction, OrderDirection::Asc),
414    }
415}
416
417/// Extract an explicit COLLATE from an ORDER BY expression
418/// (e.g. `ORDER BY a COLLATE nocase`).
419fn explicit_collation(expr: &Expression) -> Option<&str> {
420    match expr {
421        Expression::Collate { collation, .. } => Some(collation),
422        _ => None,
423    }
424}
425
426/// Compare two ORDER BY key values in *window order* — the order in which rows
427/// appear in the sorted partition. Accounts for sort direction, NULLS
428/// FIRST/LAST placement, and explicit collation, so boundary searches stay
429/// consistent with the partition sort (see `sort_partition_with_collations`).
430fn compare_keys_window_order(
431    a: &SqlValue,
432    b: &SqlValue,
433    item: &OrderByItem,
434    collation: Option<&str>,
435) -> Ordering {
436    let a_null = matches!(a, SqlValue::Null);
437    let b_null = matches!(b, SqlValue::Null);
438    match (a_null, b_null) {
439        (true, true) => Ordering::Equal,
440        (true, false) => {
441            if nulls_sort_first(item) {
442                Ordering::Less
443            } else {
444                Ordering::Greater
445            }
446        }
447        (false, true) => {
448            if nulls_sort_first(item) {
449                Ordering::Greater
450            } else {
451                Ordering::Less
452            }
453        }
454        (false, false) => {
455            let cmp = compare_values_with_collation(a, b, collation);
456            match item.direction {
457                OrderDirection::Asc => cmp,
458                OrderDirection::Desc => cmp.reverse(),
459            }
460        }
461    }
462}
463
464/// Calculate frame boundaries for GROUPS mode (peer group-based)
465///
466/// GROUPS frames count peer groups (rows with same ORDER BY values):
467/// - UNBOUNDED PRECEDING/FOLLOWING: Start/end of partition
468/// - CURRENT ROW: Current peer group
469/// - N PRECEDING: N peer groups before current
470/// - N FOLLOWING: N peer groups after current
471fn calculate_groups_frame<F>(
472    partition: &Partition,
473    current_row_idx: usize,
474    order_by: &Option<Vec<OrderByItem>>,
475    start: &FrameBound,
476    end: &Option<FrameBound>,
477    eval_fn: &F,
478) -> Range<usize>
479where
480    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
481{
482    let partition_size = partition.len();
483
484    if partition_size == 0 || current_row_idx >= partition_size {
485        return 0..0;
486    }
487
488    // Without ORDER BY, GROUPS mode treats entire partition as one group
489    let order_items = match order_by {
490        Some(items) if !items.is_empty() => items,
491        _ => return 0..partition_size,
492    };
493
494    // Build peer group boundaries
495    let group_boundaries = build_group_boundaries(partition, order_items, eval_fn);
496    let current_group = find_group_for_row(&group_boundaries, current_row_idx);
497
498    // Calculate start boundary
499    let start_idx =
500        calculate_groups_boundary(&group_boundaries, current_group, start, true, partition_size);
501
502    // Calculate end boundary
503    let end_idx = match end {
504        Some(end_bound) => calculate_groups_boundary(
505            &group_boundaries,
506            current_group,
507            end_bound,
508            false,
509            partition_size,
510        ),
511        None => {
512            // Default: CURRENT ROW - end of current group
513            if current_group + 1 < group_boundaries.len() {
514                group_boundaries[current_group + 1]
515            } else {
516                partition_size
517            }
518        }
519    };
520
521    // Ensure valid range
522    let start = start_idx.min(partition_size);
523    let end = end_idx.min(partition_size).max(start);
524
525    start..end
526}
527
528/// Calculate a single GROUPS mode boundary
529fn calculate_groups_boundary(
530    group_boundaries: &[usize],
531    current_group: usize,
532    bound: &FrameBound,
533    is_start: bool,
534    partition_size: usize,
535) -> usize {
536    let num_groups = group_boundaries.len();
537
538    match bound {
539        FrameBound::UnboundedPreceding => 0,
540        FrameBound::UnboundedFollowing => partition_size,
541
542        FrameBound::CurrentRow => {
543            if is_start {
544                group_boundaries[current_group]
545            } else {
546                if current_group + 1 < num_groups {
547                    group_boundaries[current_group + 1]
548                } else {
549                    partition_size
550                }
551            }
552        }
553
554        FrameBound::Preceding(offset_expr) => {
555            let offset = get_numeric_offset(offset_expr) as usize;
556
557            if offset > current_group {
558                // Target group is before the partition start
559                if is_start {
560                    // Start at beginning of partition (include everything from start)
561                    0
562                } else {
563                    // End bound is before the partition start => empty frame
564                    0
565                }
566            } else {
567                let target_group = current_group - offset;
568                if is_start {
569                    group_boundaries[target_group]
570                } else {
571                    if target_group + 1 < num_groups {
572                        group_boundaries[target_group + 1]
573                    } else {
574                        partition_size
575                    }
576                }
577            }
578        }
579
580        FrameBound::Following(offset_expr) => {
581            let offset = get_numeric_offset(offset_expr) as usize;
582            let target_group = current_group + offset;
583
584            if target_group >= num_groups {
585                // Target group is beyond the partition end
586                if is_start {
587                    // Start bound is beyond partition => empty frame
588                    partition_size
589                } else {
590                    // End at end of partition (include everything to end)
591                    partition_size
592                }
593            } else {
594                if is_start {
595                    group_boundaries[target_group]
596                } else {
597                    if target_group + 1 < num_groups {
598                        group_boundaries[target_group + 1]
599                    } else {
600                        partition_size
601                    }
602                }
603            }
604        }
605    }
606}
607
608/// Build a list of peer group start indices
609fn build_group_boundaries<F>(
610    partition: &Partition,
611    order_items: &[OrderByItem],
612    eval_fn: &F,
613) -> Vec<usize>
614where
615    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
616{
617    let mut boundaries = vec![0];
618
619    for i in 1..partition.len() {
620        let prev_row = &partition.rows[i - 1];
621        let curr_row = &partition.rows[i];
622
623        // Check if this row starts a new group (different ORDER BY values)
624        let mut is_new_group = false;
625        for item in order_items {
626            let prev_val = eval_fn(&item.expr, prev_row).unwrap_or(SqlValue::Null);
627            let curr_val = eval_fn(&item.expr, curr_row).unwrap_or(SqlValue::Null);
628            if compare_values(&prev_val, &curr_val) != Ordering::Equal {
629                is_new_group = true;
630                break;
631            }
632        }
633
634        if is_new_group {
635            boundaries.push(i);
636        }
637    }
638
639    boundaries
640}
641
642/// Find which group a row belongs to
643fn find_group_for_row(group_boundaries: &[usize], row_idx: usize) -> usize {
644    for (i, &boundary) in group_boundaries.iter().enumerate().rev() {
645        if row_idx >= boundary {
646            return i;
647        }
648    }
649    0
650}
651
652/// Find the first peer (row with same ORDER BY values as current)
653fn find_first_peer<F>(
654    partition: &Partition,
655    current_row_idx: usize,
656    order_items: &[OrderByItem],
657    eval_fn: &F,
658) -> usize
659where
660    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
661{
662    let current_row = &partition.rows[current_row_idx];
663
664    for i in (0..current_row_idx).rev() {
665        let row = &partition.rows[i];
666        let mut is_peer = true;
667
668        for item in order_items {
669            let curr_val = eval_fn(&item.expr, current_row).unwrap_or(SqlValue::Null);
670            let row_val = eval_fn(&item.expr, row).unwrap_or(SqlValue::Null);
671            if compare_values(&curr_val, &row_val) != Ordering::Equal {
672                is_peer = false;
673                break;
674            }
675        }
676
677        if !is_peer {
678            return i + 1;
679        }
680    }
681
682    0
683}
684
685/// Find the last peer (row with same ORDER BY values as current)
686fn find_last_peer<F>(
687    partition: &Partition,
688    current_row_idx: usize,
689    order_items: &[OrderByItem],
690    eval_fn: &F,
691) -> usize
692where
693    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
694{
695    let current_row = &partition.rows[current_row_idx];
696
697    for i in (current_row_idx + 1)..partition.len() {
698        let row = &partition.rows[i];
699        let mut is_peer = true;
700
701        for item in order_items {
702            let curr_val = eval_fn(&item.expr, current_row).unwrap_or(SqlValue::Null);
703            let row_val = eval_fn(&item.expr, row).unwrap_or(SqlValue::Null);
704            if compare_values(&curr_val, &row_val) != Ordering::Equal {
705                is_peer = false;
706                break;
707            }
708        }
709
710        if !is_peer {
711            return i - 1;
712        }
713    }
714
715    partition.len() - 1
716}
717
718/// Find the first row whose ORDER BY key sorts at-or-after `target` in window order
719///
720/// Window order accounts for direction (ASC/DESC), NULLS FIRST/LAST placement,
721/// and explicit collation, so this works for any sorted partition layout.
722fn find_first_row_at_or_after<F>(
723    partition: &Partition,
724    order_items: &[OrderByItem],
725    target: &SqlValue,
726    eval_fn: &F,
727) -> usize
728where
729    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
730{
731    let item = &order_items[0];
732    let collation = explicit_collation(&item.expr);
733    for (i, row) in partition.rows.iter().enumerate() {
734        let val = eval_fn(&item.expr, row).unwrap_or(SqlValue::Null);
735        if compare_keys_window_order(&val, target, item, collation) != Ordering::Less {
736            return i;
737        }
738    }
739    partition.len()
740}
741
742/// Find the last row whose ORDER BY key sorts at-or-before `target` in window order
743fn find_last_row_at_or_before<F>(
744    partition: &Partition,
745    order_items: &[OrderByItem],
746    target: &SqlValue,
747    eval_fn: &F,
748) -> Option<usize>
749where
750    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
751{
752    let item = &order_items[0];
753    let collation = explicit_collation(&item.expr);
754    for i in (0..partition.len()).rev() {
755        let val = eval_fn(&item.expr, &partition.rows[i]).unwrap_or(SqlValue::Null);
756        if compare_keys_window_order(&val, target, item, collation) != Ordering::Greater {
757            return Some(i);
758        }
759    }
760    None
761}
762
763/// Get numeric offset from expression
764fn get_numeric_offset(expr: &Expression) -> f64 {
765    match evaluate_offset_expr(expr) {
766        Ok(Some(n)) => n,
767        _ => 0.0,
768    }
769}
770
771/// Subtract a numeric offset from a SqlValue
772fn subtract_from_value(value: &SqlValue, offset: f64) -> SqlValue {
773    match value {
774        SqlValue::Integer(n) => SqlValue::Real(*n as f64 - offset),
775        SqlValue::Smallint(n) => SqlValue::Real(*n as f64 - offset),
776        SqlValue::Bigint(n) => SqlValue::Real(*n as f64 - offset),
777        SqlValue::Unsigned(n) => SqlValue::Real(*n as f64 - offset),
778        SqlValue::Float(f) => SqlValue::Real(*f as f64 - offset),
779        SqlValue::Real(f) => SqlValue::Real(*f - offset),
780        SqlValue::Double(f) => SqlValue::Real(*f - offset),
781        SqlValue::Numeric(f) => SqlValue::Real(*f - offset),
782        _ => value.clone(), // For non-numeric, return as-is
783    }
784}
785
786/// Add a numeric offset to a SqlValue
787fn add_to_value(value: &SqlValue, offset: f64) -> SqlValue {
788    match value {
789        SqlValue::Integer(n) => SqlValue::Real(*n as f64 + offset),
790        SqlValue::Smallint(n) => SqlValue::Real(*n as f64 + offset),
791        SqlValue::Bigint(n) => SqlValue::Real(*n as f64 + offset),
792        SqlValue::Unsigned(n) => SqlValue::Real(*n as f64 + offset),
793        SqlValue::Float(f) => SqlValue::Real(*f as f64 + offset),
794        SqlValue::Real(f) => SqlValue::Real(*f + offset),
795        SqlValue::Double(f) => SqlValue::Real(*f + offset),
796        SqlValue::Numeric(f) => SqlValue::Real(*f + offset),
797        _ => value.clone(), // For non-numeric, return as-is
798    }
799}
800
801/// Calculate a single ROWS mode boundary (physical row-based)
802///
803/// Returns the index for the boundary.
804/// For start boundaries, returns inclusive index.
805/// For end boundaries, returns exclusive index (Range semantics).
806fn calculate_rows_boundary(
807    bound: &FrameBound,
808    current_row_idx: usize,
809    partition_size: usize,
810    is_start: bool,
811) -> usize {
812    match bound {
813        FrameBound::UnboundedPreceding => 0,
814
815        FrameBound::UnboundedFollowing => partition_size,
816
817        FrameBound::CurrentRow => {
818            if is_start {
819                current_row_idx
820            } else {
821                current_row_idx + 1 // Exclusive end
822            }
823        }
824
825        FrameBound::Preceding(offset_expr) => {
826            let offset = get_numeric_offset(offset_expr) as usize;
827            if is_start {
828                current_row_idx.saturating_sub(offset)
829            } else {
830                // For end boundary, +1 for exclusive end (Range semantics)
831                // If offset > current_row_idx, the position is before the partition start,
832                // so return 0 (empty frame when combined with any start boundary)
833                if offset > current_row_idx {
834                    0
835                } else {
836                    current_row_idx - offset + 1
837                }
838            }
839        }
840
841        FrameBound::Following(offset_expr) => {
842            let offset = get_numeric_offset(offset_expr) as usize;
843            let result = current_row_idx + offset;
844
845            if is_start {
846                result.min(partition_size)
847            } else {
848                (result + 1).min(partition_size) // Exclusive end, +1 for inclusive offset
849            }
850        }
851    }
852}
853
854/// Result of frame calculation including exclusion information
855#[derive(Debug, Clone)]
856pub struct FrameResult {
857    /// The frame range [start, end)
858    pub range: Range<usize>,
859    /// Exclusion mode
860    pub exclude: Option<FrameExclude>,
861    /// Index of the current row being evaluated
862    pub current_row_idx: usize,
863}
864
865impl FrameResult {
866    /// Check if a row index should be included in the frame calculation
867    ///
868    /// Takes into account both the frame range and the EXCLUDE clause.
869    pub fn includes<F>(
870        &self,
871        row_idx: usize,
872        partition: &Partition,
873        order_by: &Option<Vec<OrderByItem>>,
874        eval_fn: &F,
875    ) -> bool
876    where
877        F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
878    {
879        // First check if row is in the frame range
880        if !self.range.contains(&row_idx) {
881            return false;
882        }
883
884        // Then apply EXCLUDE logic
885        match self.exclude {
886            None | Some(FrameExclude::NoOthers) => true,
887
888            Some(FrameExclude::CurrentRow) => row_idx != self.current_row_idx,
889
890            Some(FrameExclude::Group) => {
891                // Exclude current row and all its peers (rows with same ORDER BY values)
892                !is_peer(row_idx, self.current_row_idx, partition, order_by, eval_fn)
893            }
894
895            Some(FrameExclude::Ties) => {
896                // Exclude peers of current row, but include current row itself
897                row_idx == self.current_row_idx
898                    || !is_peer(row_idx, self.current_row_idx, partition, order_by, eval_fn)
899            }
900        }
901    }
902
903    /// Get an iterator over all included row indices
904    ///
905    /// This filters out excluded rows based on the EXCLUDE clause.
906    pub fn included_indices<'a, F>(
907        &'a self,
908        partition: &'a Partition,
909        order_by: &'a Option<Vec<OrderByItem>>,
910        eval_fn: &'a F,
911    ) -> impl Iterator<Item = usize> + 'a
912    where
913        F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
914    {
915        self.range.clone().filter(move |&idx| self.includes(idx, partition, order_by, eval_fn))
916    }
917}
918
919/// Calculate frame with exclusion information
920///
921/// Returns a FrameResult that includes both the range and exclusion info.
922pub fn calculate_frame_with_exclusion<F>(
923    partition: &Partition,
924    current_row_idx: usize,
925    order_by: &Option<Vec<OrderByItem>>,
926    frame_spec: &Option<WindowFrame>,
927    eval_fn: &F,
928) -> FrameResult
929where
930    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
931{
932    let range = calculate_frame(partition, current_row_idx, order_by, frame_spec, eval_fn);
933    let exclude = frame_spec.as_ref().and_then(|f| f.exclude);
934
935    FrameResult { range, exclude, current_row_idx }
936}
937
938/// Check if two rows are peers (have same ORDER BY values)
939///
940/// Rows are considered peers if they have identical values for all ORDER BY expressions.
941/// If there's no ORDER BY clause, all rows are considered peers.
942fn is_peer<F>(
943    row_idx_a: usize,
944    row_idx_b: usize,
945    partition: &Partition,
946    order_by: &Option<Vec<OrderByItem>>,
947    eval_fn: &F,
948) -> bool
949where
950    F: Fn(&Expression, &Row) -> Result<SqlValue, String>,
951{
952    // Same row is always a peer of itself
953    if row_idx_a == row_idx_b {
954        return true;
955    }
956
957    // Without ORDER BY, all rows are considered peers
958    let order_items = match order_by {
959        Some(items) if !items.is_empty() => items,
960        _ => return true,
961    };
962
963    // Ensure indices are valid
964    if row_idx_a >= partition.len() || row_idx_b >= partition.len() {
965        return false;
966    }
967
968    let row_a = &partition.rows[row_idx_a];
969    let row_b = &partition.rows[row_idx_b];
970
971    // Compare all ORDER BY expressions
972    for order_item in order_items {
973        let val_a = eval_fn(&order_item.expr, row_a).unwrap_or(SqlValue::Null);
974        let val_b = eval_fn(&order_item.expr, row_b).unwrap_or(SqlValue::Null);
975
976        if compare_values(&val_a, &val_b) != Ordering::Equal {
977            return false;
978        }
979    }
980
981    true
982}