Skip to main content

nodedb_query/window/
value_agg.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Aggregate window functions (sum, count, avg, min, max, first_value, last_value)
4//! and frame-bound resolution for the Value-native evaluator.
5
6use std::collections::HashMap;
7
8use nodedb_types::Value;
9
10use super::extremum::{extremum_direction, value_replaces};
11use super::spec::{FrameBound, WindowFrame, WindowFuncSpec};
12use super::value_eval::{WindowError, cmp_values, eval_arg_for_row, order_keys_equal_v, set_cell};
13use crate::numeric_sum::ExactSum;
14
15pub(super) fn apply_v_aggregate(
16    rows: &mut [Vec<Value>],
17    indices: &[usize],
18    column_index: &HashMap<String, usize>,
19    spec: &WindowFuncSpec,
20    write_col: usize,
21) -> Result<(), WindowError> {
22    let use_running = spec.frame.mode == "range"
23        && matches!(spec.frame.start, FrameBound::UnboundedPreceding)
24        && matches!(spec.frame.end, FrameBound::CurrentRow);
25
26    if use_running {
27        apply_v_running_aggregate(rows, indices, column_index, spec, write_col)
28    } else {
29        apply_v_per_row_aggregate(rows, indices, column_index, spec, write_col)
30    }
31}
32
33fn eval_arg(
34    spec: &WindowFuncSpec,
35    row: &[Value],
36    column_index: &HashMap<String, usize>,
37) -> Result<Value, WindowError> {
38    match spec.args.first() {
39        Some(expr) => Ok(eval_arg_for_row(expr, row, column_index)?),
40        None => Ok(Value::Null),
41    }
42}
43
44fn apply_v_running_aggregate(
45    rows: &mut [Vec<Value>],
46    indices: &[usize],
47    column_index: &HashMap<String, usize>,
48    spec: &WindowFuncSpec,
49    write_col: usize,
50) -> Result<(), WindowError> {
51    let len = indices.len();
52    if len == 0 {
53        return Ok(());
54    }
55
56    // SUM / AVG total exactly per `ExactSum`.
57    let mut running_sum = ExactSum::new();
58    let mut running_count = 0u64;
59    // MIN / MAX hold the original argument value, compared exactly.
60    let extremum_dir = extremum_direction(&spec.func_name);
61    let mut running_extreme: Option<Value> = None;
62    let mut peer_start = 0usize;
63
64    for pos in 0..len {
65        let i = indices[pos];
66        let val = match rows.get(i) {
67            Some(row) => eval_arg(spec, row, column_index)?,
68            None => Value::Null,
69        };
70
71        if is_window_number(&val) {
72            running_sum.add_value(&val);
73            running_count += 1;
74            if let Some(want_max) = extremum_dir
75                && value_replaces(&val, running_extreme.as_ref(), want_max)
76            {
77                running_extreme = Some(val);
78            }
79        } else if spec.func_name == "count" && (spec.args.is_empty() || !val.is_null()) {
80            // `COUNT(*)` counts every row; `COUNT(expr)` counts rows whose
81            // argument is non-NULL, including non-numeric values.
82            running_count += 1;
83        }
84
85        let is_last_in_group = pos + 1 == len
86            || !order_keys_equal_v(rows, i, indices[pos + 1], column_index, &spec.order_by)?;
87
88        if is_last_in_group {
89            let first_val = match rows.get(indices[0]) {
90                Some(row) => eval_arg(spec, row, column_index)?,
91                None => Value::Null,
92            };
93            let last_val = match rows.get(indices[pos]) {
94                Some(row) => eval_arg(spec, row, column_index)?,
95                None => Value::Null,
96            };
97
98            let result = match spec.func_name.as_str() {
99                "sum" => running_sum.sum()?,
100                "count" => Value::Integer(running_count as i64),
101                "avg" => running_sum.avg()?,
102                "min" | "max" => running_extreme.clone().unwrap_or(Value::Null),
103                "first_value" => first_val,
104                "last_value" => last_val,
105                _ => Value::Null,
106            };
107
108            for &peer_idx in &indices[peer_start..=pos] {
109                set_cell(rows, peer_idx, write_col, result.clone());
110            }
111            peer_start = pos + 1;
112        }
113    }
114    Ok(())
115}
116
117fn apply_v_per_row_aggregate(
118    rows: &mut [Vec<Value>],
119    indices: &[usize],
120    column_index: &HashMap<String, usize>,
121    spec: &WindowFuncSpec,
122    write_col: usize,
123) -> Result<(), WindowError> {
124    let len = indices.len();
125    if len == 0 {
126        return Ok(());
127    }
128
129    let order_expr = spec.order_by.first().map(|(expr, _)| expr);
130    let order_values: Vec<Value> = indices
131        .iter()
132        .map(|&i| match (order_expr, rows.get(i)) {
133            (Some(expr), Some(row)) => Ok(eval_arg_for_row(expr, row, column_index)?),
134            _ => Ok(Value::Null),
135        })
136        .collect::<Result<Vec<_>, WindowError>>()?;
137
138    let peer_groups: Vec<usize> = if spec.frame.mode == "groups" {
139        build_v_peer_groups(&order_values)
140    } else {
141        Vec::new()
142    };
143
144    let arg_vals: Vec<Value> = indices
145        .iter()
146        .map(|&i| match rows.get(i) {
147            Some(row) => eval_arg(spec, row, column_index),
148            None => Ok(Value::Null),
149        })
150        .collect::<Result<Vec<_>, WindowError>>()?;
151    let results: Vec<Value> = (0..len)
152        .map(|pos| {
153            let (start_idx, end_idx) =
154                evaluate_v_frame_bounds(&spec.frame, pos, len, &order_values, &peer_groups);
155            aggregate_v_slice(&arg_vals, spec, start_idx, end_idx)
156        })
157        .collect::<Result<_, _>>()?;
158
159    for (pos, result) in results.into_iter().enumerate() {
160        set_cell(rows, indices[pos], write_col, result);
161    }
162    Ok(())
163}
164
165/// A window SUM / AVG / MIN / MAX input: an integer, float, or decimal.
166fn is_window_number(v: &Value) -> bool {
167    matches!(v, Value::Integer(_) | Value::Float(_) | Value::Decimal(_))
168}
169
170/// Aggregate the evaluated argument over the frame slice
171/// `[start_idx, end_idx]` (partition positions). `arg_vals` holds the
172/// argument per partition position.
173fn aggregate_v_slice(
174    arg_vals: &[Value],
175    spec: &WindowFuncSpec,
176    start_idx: usize,
177    end_idx: usize,
178) -> Result<Value, WindowError> {
179    let frame_values = arg_vals.get(start_idx..=end_idx).unwrap_or(&[]);
180    let slice_count = end_idx - start_idx + 1;
181    let frame_sum = || {
182        let mut acc = ExactSum::new();
183        for v in frame_values {
184            acc.add_value(v);
185        }
186        acc
187    };
188
189    Ok(match spec.func_name.as_str() {
190        "sum" => frame_sum().sum()?,
191        // `COUNT(*)` counts frame rows; `COUNT(expr)` counts the rows whose
192        // argument is non-NULL.
193        "count" => {
194            let counted = if spec.args.is_empty() {
195                slice_count
196            } else {
197                frame_values.iter().filter(|v| !v.is_null()).count()
198            };
199            Value::Integer(counted as i64)
200        }
201        "avg" => frame_sum().avg()?,
202        "min" | "max" => {
203            let want_max = spec.func_name == "max";
204            let mut best: Option<&Value> = None;
205            for candidate in frame_values.iter().filter(|v| is_window_number(v)) {
206                if value_replaces(candidate, best, want_max) {
207                    best = Some(candidate);
208                }
209            }
210            best.cloned().unwrap_or(Value::Null)
211        }
212        "first_value" => arg_vals.get(start_idx).cloned().unwrap_or(Value::Null),
213        "last_value" => arg_vals.get(end_idx).cloned().unwrap_or(Value::Null),
214        _ => Value::Null,
215    })
216}
217
218fn build_v_peer_groups(order_values: &[Value]) -> Vec<usize> {
219    let mut groups = Vec::with_capacity(order_values.len());
220    let mut current_group = 0usize;
221    for (i, val) in order_values.iter().enumerate() {
222        if i > 0
223            && !matches!(
224                cmp_values(val, &order_values[i - 1]),
225                std::cmp::Ordering::Equal
226            )
227        {
228            current_group += 1;
229        }
230        groups.push(current_group);
231    }
232    groups
233}
234
235pub(super) fn evaluate_v_frame_bounds(
236    frame: &WindowFrame,
237    pos: usize,
238    len: usize,
239    order_values: &[Value],
240    peer_groups: &[usize],
241) -> (usize, usize) {
242    match frame.mode.as_str() {
243        "rows" => v_rows_bounds(&frame.start, &frame.end, pos, len),
244        "range" => v_range_bounds(&frame.start, &frame.end, pos, len, order_values),
245        "groups" => v_groups_bounds(&frame.start, &frame.end, pos, len, peer_groups),
246        _ => (0, len.saturating_sub(1)),
247    }
248}
249
250fn v_rows_bounds(start: &FrameBound, end: &FrameBound, pos: usize, len: usize) -> (usize, usize) {
251    let s = v_rows_bound_to_idx(start, pos, len);
252    let e = v_rows_bound_to_idx(end, pos, len);
253    (s.min(e), s.max(e))
254}
255
256fn v_rows_bound_to_idx(bound: &FrameBound, pos: usize, len: usize) -> usize {
257    match bound {
258        FrameBound::UnboundedPreceding => 0,
259        FrameBound::Preceding(n) => pos.saturating_sub(*n as usize),
260        FrameBound::CurrentRow => pos,
261        FrameBound::Following(n) => (pos + *n as usize).min(len.saturating_sub(1)),
262        FrameBound::UnboundedFollowing => len.saturating_sub(1),
263    }
264}
265
266fn v_range_bounds(
267    start: &FrameBound,
268    end: &FrameBound,
269    pos: usize,
270    len: usize,
271    order_values: &[Value],
272) -> (usize, usize) {
273    let current_val = order_values.get(pos).and_then(|v| v.as_f64());
274    let s = v_range_bound_to_idx(start, pos, len, order_values, current_val, true);
275    let e = v_range_bound_to_idx(end, pos, len, order_values, current_val, false);
276    (s.min(e), s.max(e))
277}
278
279fn v_range_bound_to_idx(
280    bound: &FrameBound,
281    pos: usize,
282    len: usize,
283    order_values: &[Value],
284    current_val: Option<f64>,
285    is_start: bool,
286) -> usize {
287    match bound {
288        FrameBound::UnboundedPreceding => 0,
289        FrameBound::UnboundedFollowing => len.saturating_sub(1),
290        FrameBound::CurrentRow => {
291            if is_start {
292                let mut idx = pos;
293                while idx > 0
294                    && matches!(
295                        cmp_values(
296                            order_values.get(idx - 1).unwrap_or(&Value::Null),
297                            order_values.get(pos).unwrap_or(&Value::Null),
298                        ),
299                        std::cmp::Ordering::Equal
300                    )
301                {
302                    idx -= 1;
303                }
304                idx
305            } else {
306                let mut idx = pos;
307                while idx + 1 < len
308                    && matches!(
309                        cmp_values(
310                            order_values.get(idx + 1).unwrap_or(&Value::Null),
311                            order_values.get(pos).unwrap_or(&Value::Null),
312                        ),
313                        std::cmp::Ordering::Equal
314                    )
315                {
316                    idx += 1;
317                }
318                idx
319            }
320        }
321        FrameBound::Preceding(n) => {
322            let threshold = match current_val {
323                Some(cv) => cv - *n as f64,
324                None => return pos,
325            };
326            let mut idx = 0;
327            for (i, v) in order_values.iter().enumerate() {
328                if v.as_f64().is_some_and(|fv| fv >= threshold) {
329                    idx = i;
330                    break;
331                }
332                idx = i + 1;
333            }
334            idx.min(len.saturating_sub(1))
335        }
336        FrameBound::Following(n) => {
337            let threshold = match current_val {
338                Some(cv) => cv + *n as f64,
339                None => return pos,
340            };
341            let mut idx = pos;
342            for (i, v) in order_values.iter().enumerate().skip(pos) {
343                if v.as_f64().is_none_or(|fv| fv > threshold) {
344                    break;
345                }
346                idx = i;
347            }
348            idx.min(len.saturating_sub(1))
349        }
350    }
351}
352
353fn v_groups_bounds(
354    start: &FrameBound,
355    end: &FrameBound,
356    pos: usize,
357    len: usize,
358    peer_groups: &[usize],
359) -> (usize, usize) {
360    let current_group = peer_groups.get(pos).copied().unwrap_or(0);
361    let max_group = peer_groups.last().copied().unwrap_or(0);
362    let start_group = v_groups_bound_to_group(start, current_group, max_group);
363    let end_group = v_groups_bound_to_group(end, current_group, max_group);
364    let start_idx = peer_groups
365        .iter()
366        .position(|&g| g == start_group)
367        .unwrap_or(0);
368    let end_idx = peer_groups
369        .iter()
370        .rposition(|&g| g == end_group)
371        .unwrap_or(len.saturating_sub(1));
372    (start_idx, end_idx)
373}
374
375fn v_groups_bound_to_group(bound: &FrameBound, current_group: usize, max_group: usize) -> usize {
376    match bound {
377        FrameBound::UnboundedPreceding => 0,
378        FrameBound::UnboundedFollowing => max_group,
379        FrameBound::CurrentRow => current_group,
380        FrameBound::Preceding(n) => current_group.saturating_sub(*n as usize),
381        FrameBound::Following(n) => (current_group + *n as usize).min(max_group),
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    use crate::expr::types::SqlExpr;
389
390    fn col(name: &str) -> SqlExpr {
391        SqlExpr::Column(name.to_string())
392    }
393
394    fn ci(names: &[&str]) -> HashMap<String, usize> {
395        names
396            .iter()
397            .enumerate()
398            .map(|(i, n)| (n.to_string(), i))
399            .collect()
400    }
401
402    fn rows_v(vals: &[i64]) -> Vec<Vec<Value>> {
403        vals.iter().map(|&v| vec![Value::Integer(v)]).collect()
404    }
405
406    fn agg_spec(func: &str, frame: WindowFrame, order_by: Vec<(SqlExpr, bool)>) -> WindowFuncSpec {
407        WindowFuncSpec {
408            alias: format!("w_{func}"),
409            func_name: func.to_string(),
410            args: vec![col("v")],
411            partition_by: vec![],
412            order_by,
413            frame,
414        }
415    }
416
417    /// Drive `apply_v_aggregate` over the whole single-partition row set the
418    /// same way `evaluate_window_functions_value` does (push a Null cell, then
419    /// fill it), returning the produced column.
420    fn run_agg(rows: &mut [Vec<Value>], cols: &HashMap<String, usize>, spec: &WindowFuncSpec) {
421        let write_col = rows.first().map(|r| r.len()).unwrap_or(0);
422        for row in rows.iter_mut() {
423            row.push(Value::Null);
424        }
425        let indices: Vec<usize> = (0..rows.len()).collect();
426        apply_v_aggregate(rows, &indices, cols, spec, write_col).unwrap();
427    }
428
429    fn frame(mode: &str, start: FrameBound, end: FrameBound) -> WindowFrame {
430        WindowFrame {
431            mode: mode.into(),
432            start,
433            end,
434        }
435    }
436
437    #[test]
438    fn running_sum_is_cumulative() {
439        // Default frame (range, unbounded preceding → current row) with a
440        // strictly increasing order key → cumulative sum.
441        let cols = ci(&["v"]);
442        let mut rows = rows_v(&[1, 2, 3]);
443        let s = agg_spec("sum", WindowFrame::default(), vec![(col("v"), true)]);
444        run_agg(&mut rows, &cols, &s);
445        let got: Vec<f64> = rows.iter().map(|r| r[1].as_f64().unwrap()).collect();
446        assert_eq!(got, vec![1.0, 3.0, 6.0]);
447    }
448
449    #[test]
450    fn running_sum_shares_value_across_peers() {
451        // Tied order keys form a peer group; the running aggregate assigns the
452        // group's running total to every peer.
453        let cols = ci(&["v"]);
454        let mut rows = rows_v(&[5, 5, 9]);
455        let s = agg_spec("sum", WindowFrame::default(), vec![(col("v"), true)]);
456        run_agg(&mut rows, &cols, &s);
457        let got: Vec<f64> = rows.iter().map(|r| r[1].as_f64().unwrap()).collect();
458        assert_eq!(got, vec![10.0, 10.0, 19.0]);
459    }
460
461    #[test]
462    fn rows_frame_sliding_sum() {
463        let cols = ci(&["v"]);
464        let mut rows = rows_v(&[10, 20, 30]);
465        let s = agg_spec(
466            "sum",
467            frame("rows", FrameBound::Preceding(1), FrameBound::CurrentRow),
468            vec![(col("v"), true)],
469        );
470        run_agg(&mut rows, &cols, &s);
471        let got: Vec<f64> = rows.iter().map(|r| r[1].as_f64().unwrap()).collect();
472        assert_eq!(got, vec![10.0, 30.0, 50.0]);
473    }
474
475    #[test]
476    fn rows_frame_count_and_avg() {
477        let cols = ci(&["v"]);
478        let f = frame(
479            "rows",
480            FrameBound::UnboundedPreceding,
481            FrameBound::CurrentRow,
482        );
483
484        let mut rows = rows_v(&[4, 8, 12]);
485        let cnt = agg_spec("count", f.clone(), vec![(col("v"), true)]);
486        run_agg(&mut rows, &cols, &cnt);
487        let counts: Vec<i64> = rows
488            .iter()
489            .map(|r| match r[1] {
490                Value::Integer(n) => n,
491                _ => panic!("count must be integer"),
492            })
493            .collect();
494        assert_eq!(counts, vec![1, 2, 3]);
495
496        let mut rows = rows_v(&[4, 8, 12]);
497        let avg = agg_spec("avg", f, vec![(col("v"), true)]);
498        run_agg(&mut rows, &cols, &avg);
499        let avgs: Vec<f64> = rows.iter().map(|r| r[1].as_f64().unwrap()).collect();
500        assert_eq!(avgs, vec![4.0, 6.0, 8.0]);
501    }
502
503    #[test]
504    fn rows_frame_min_max() {
505        let cols = ci(&["v"]);
506        let f = frame(
507            "rows",
508            FrameBound::UnboundedPreceding,
509            FrameBound::UnboundedFollowing,
510        );
511
512        let mut rows = rows_v(&[3, 1, 2]);
513        let mn = agg_spec("min", f.clone(), vec![]);
514        run_agg(&mut rows, &cols, &mn);
515        assert!((rows[0][1].as_f64().unwrap() - 1.0).abs() < 1e-9);
516
517        let mut rows = rows_v(&[3, 1, 2]);
518        let mx = agg_spec("max", f, vec![]);
519        run_agg(&mut rows, &cols, &mx);
520        assert!((rows[0][1].as_f64().unwrap() - 3.0).abs() < 1e-9);
521    }
522
523    fn whole_partition() -> WindowFrame {
524        frame(
525            "rows",
526            FrameBound::UnboundedPreceding,
527            FrameBound::UnboundedFollowing,
528        )
529    }
530
531    #[test]
532    fn frame_min_max_keep_integers_above_2_pow_53_exact() {
533        let cols = ci(&["v"]);
534        let vals = [9_007_199_254_740_993, 9_007_199_254_740_992];
535
536        let mut rows = rows_v(&vals);
537        run_agg(
538            &mut rows,
539            &cols,
540            &agg_spec("min", whole_partition(), vec![]),
541        );
542        assert_eq!(rows[0][1], Value::Integer(9_007_199_254_740_992));
543
544        let mut rows = rows_v(&vals);
545        run_agg(
546            &mut rows,
547            &cols,
548            &agg_spec("max", whole_partition(), vec![]),
549        );
550        assert_eq!(rows[0][1], Value::Integer(9_007_199_254_740_993));
551    }
552
553    #[test]
554    fn sum_keeps_integers_above_2_pow_53_exact() {
555        let cols = ci(&["v"]);
556        let vals = [9_007_199_254_740_993, 9_007_199_254_740_992];
557
558        let mut rows = rows_v(&vals);
559        run_agg(
560            &mut rows,
561            &cols,
562            &agg_spec("sum", whole_partition(), vec![]),
563        );
564        assert_eq!(rows[0][1], Value::Integer(18_014_398_509_481_985));
565
566        let mut rows = rows_v(&[9_007_199_254_740_993, 1]);
567        let running = agg_spec("sum", WindowFrame::default(), vec![(col("v"), false)]);
568        run_agg(&mut rows, &cols, &running);
569        assert_eq!(rows[0][1], Value::Integer(9_007_199_254_740_993));
570        assert_eq!(rows[1][1], Value::Integer(9_007_199_254_740_994));
571
572        let mut rows = rows_v(&vals);
573        run_agg(
574            &mut rows,
575            &cols,
576            &agg_spec("avg", whole_partition(), vec![]),
577        );
578        assert_eq!(rows[0][1], Value::Float(9_007_199_254_740_992.0));
579    }
580
581    #[test]
582    fn sum_past_i64_is_decimal_and_u64_counts() {
583        let cols = ci(&["v"]);
584        let mut rows = vec![
585            vec![Value::Integer(i64::MAX)],
586            vec![Value::from_u64(u64::MAX)],
587        ];
588        run_agg(
589            &mut rows,
590            &cols,
591            &agg_spec("sum", whole_partition(), vec![]),
592        );
593        assert_eq!(
594            rows[0][1],
595            Value::Decimal(rust_decimal::Decimal::from_i128_with_scale(
596                i128::from(i64::MAX) + i128::from(u64::MAX),
597                0
598            ))
599        );
600
601        let mut rows = vec![
602            vec![Value::Integer(i64::MAX)],
603            vec![Value::from_u64(u64::MAX)],
604        ];
605        run_agg(
606            &mut rows,
607            &cols,
608            &agg_spec("max", whole_partition(), vec![]),
609        );
610        assert_eq!(rows[0][1], Value::from_u64(u64::MAX));
611    }
612
613    #[test]
614    fn sum_mixed_int_float_is_float_and_nan_is_the_largest_extreme() {
615        let cols = ci(&["v"]);
616        let mixed = || vec![vec![Value::Integer(2)], vec![Value::Float(0.5)]];
617        let mut rows = mixed();
618        run_agg(
619            &mut rows,
620            &cols,
621            &agg_spec("sum", whole_partition(), vec![]),
622        );
623        assert_eq!(rows[0][1], Value::Float(2.5));
624
625        let with_nan = || {
626            vec![
627                vec![Value::Float(f64::NAN)],
628                vec![Value::Integer(4)],
629                vec![Value::Integer(9)],
630            ]
631        };
632        let mut rows = with_nan();
633        run_agg(
634            &mut rows,
635            &cols,
636            &agg_spec("min", whole_partition(), vec![]),
637        );
638        assert_eq!(rows[0][1], Value::Integer(4));
639        // NaN sorts above every number, as in PostgreSQL.
640        let mut rows = with_nan();
641        run_agg(
642            &mut rows,
643            &cols,
644            &agg_spec("max", whole_partition(), vec![]),
645        );
646        assert!(matches!(rows[0][1], Value::Float(f) if f.is_nan()));
647    }
648
649    #[test]
650    fn frame_min_max_over_nanosecond_timestamps() {
651        let cols = ci(&["v"]);
652        let ts = [
653            1_700_000_000_000_000_001,
654            1_700_000_000_000_000_003,
655            1_700_000_000_000_000_002,
656        ];
657
658        let mut rows = rows_v(&ts);
659        run_agg(
660            &mut rows,
661            &cols,
662            &agg_spec("min", whole_partition(), vec![]),
663        );
664        assert_eq!(rows[1][1], Value::Integer(1_700_000_000_000_000_001));
665
666        let mut rows = rows_v(&ts);
667        run_agg(
668            &mut rows,
669            &cols,
670            &agg_spec("max", whole_partition(), vec![]),
671        );
672        assert_eq!(rows[1][1], Value::Integer(1_700_000_000_000_000_003));
673    }
674
675    #[test]
676    fn frame_min_max_mixed_int_float_return_original_type() {
677        let cols = ci(&["v"]);
678        let mixed = || {
679            vec![
680                vec![Value::Integer(9_007_199_254_740_993)],
681                vec![Value::Float(9_007_199_254_740_992.0)],
682                vec![Value::Float(1.5)],
683                vec![Value::Integer(2)],
684            ]
685        };
686
687        let mut rows = mixed();
688        run_agg(
689            &mut rows,
690            &cols,
691            &agg_spec("min", whole_partition(), vec![]),
692        );
693        assert_eq!(rows[0][1], Value::Float(1.5));
694
695        let mut rows = mixed();
696        run_agg(
697            &mut rows,
698            &cols,
699            &agg_spec("max", whole_partition(), vec![]),
700        );
701        assert_eq!(rows[0][1], Value::Integer(9_007_199_254_740_993));
702    }
703
704    #[test]
705    fn running_min_max_keep_integers_exact() {
706        // Default frame, distinct order keys `k`: running extreme per row.
707        let cols = ci(&["v", "k"]);
708        let keyed = || {
709            vec![
710                vec![Value::Integer(9_007_199_254_740_993), Value::Integer(1)],
711                vec![Value::Integer(9_007_199_254_740_992), Value::Integer(2)],
712                vec![Value::Integer(9_007_199_254_740_995), Value::Integer(3)],
713            ]
714        };
715        let order = vec![(col("k"), true)];
716
717        let mut rows = keyed();
718        run_agg(
719            &mut rows,
720            &cols,
721            &agg_spec("min", WindowFrame::default(), order.clone()),
722        );
723        let mins: Vec<Value> = rows.iter().map(|r| r[2].clone()).collect();
724        assert_eq!(
725            mins,
726            vec![
727                Value::Integer(9_007_199_254_740_993),
728                Value::Integer(9_007_199_254_740_992),
729                Value::Integer(9_007_199_254_740_992),
730            ]
731        );
732
733        let mut rows = keyed();
734        run_agg(
735            &mut rows,
736            &cols,
737            &agg_spec("max", WindowFrame::default(), order),
738        );
739        let maxes: Vec<Value> = rows.iter().map(|r| r[2].clone()).collect();
740        assert_eq!(
741            maxes,
742            vec![
743                Value::Integer(9_007_199_254_740_993),
744                Value::Integer(9_007_199_254_740_993),
745                Value::Integer(9_007_199_254_740_995),
746            ]
747        );
748    }
749
750    #[test]
751    fn count_expr_skips_null_arguments() {
752        let cols = ci(&["v"]);
753        let rows_with_null = || {
754            vec![
755                vec![Value::Integer(1)],
756                vec![Value::Null],
757                vec![Value::String("x".into())],
758            ]
759        };
760
761        let mut rows = rows_with_null();
762        run_agg(
763            &mut rows,
764            &cols,
765            &agg_spec("count", whole_partition(), vec![]),
766        );
767        assert_eq!(rows[0][1], Value::Integer(2));
768
769        let mut rows = rows_with_null();
770        let mut star = agg_spec("count", whole_partition(), vec![]);
771        star.args.clear();
772        run_agg(&mut rows, &cols, &star);
773        assert_eq!(rows[0][1], Value::Integer(3));
774
775        let mut rows = rows_with_null();
776        run_agg(
777            &mut rows,
778            &cols,
779            &agg_spec("count", WindowFrame::default(), vec![]),
780        );
781        assert_eq!(rows[2][1], Value::Integer(2));
782    }
783
784    #[test]
785    fn first_and_last_value() {
786        let cols = ci(&["v"]);
787        let f = frame(
788            "rows",
789            FrameBound::UnboundedPreceding,
790            FrameBound::UnboundedFollowing,
791        );
792
793        let mut rows = rows_v(&[7, 8, 9]);
794        let fv = agg_spec("first_value", f.clone(), vec![]);
795        run_agg(&mut rows, &cols, &fv);
796        assert_eq!(rows[2][1].as_f64().unwrap() as i64, 7);
797
798        let mut rows = rows_v(&[7, 8, 9]);
799        let lv = agg_spec("last_value", f, vec![]);
800        run_agg(&mut rows, &cols, &lv);
801        assert_eq!(rows[0][1].as_f64().unwrap() as i64, 9);
802    }
803
804    #[test]
805    fn rows_bounds_resolution() {
806        let order = vec![];
807        let groups = vec![];
808        let f = frame("rows", FrameBound::Preceding(1), FrameBound::Following(1));
809        // Middle of a 5-row partition → window [pos-1, pos+1].
810        assert_eq!(evaluate_v_frame_bounds(&f, 2, 5, &order, &groups), (1, 3));
811        // First row clamps the preceding bound to 0.
812        assert_eq!(evaluate_v_frame_bounds(&f, 0, 5, &order, &groups), (0, 1));
813        // Last row clamps the following bound to len-1.
814        assert_eq!(evaluate_v_frame_bounds(&f, 4, 5, &order, &groups), (3, 4));
815    }
816
817    #[test]
818    fn range_bounds_expand_over_peers() {
819        // RANGE with CURRENT ROW spans the whole peer group of equal keys.
820        let order = vec![Value::Integer(10), Value::Integer(10), Value::Integer(20)];
821        let f = frame(
822            "range",
823            FrameBound::UnboundedPreceding,
824            FrameBound::CurrentRow,
825        );
826        // pos 0 (key 10) → end extends across both 10s.
827        assert_eq!(evaluate_v_frame_bounds(&f, 0, 3, &order, &[]), (0, 1));
828        // pos 2 (key 20) → spans everything up to and including itself.
829        assert_eq!(evaluate_v_frame_bounds(&f, 2, 3, &order, &[]), (0, 2));
830    }
831
832    #[test]
833    fn groups_bounds_resolution() {
834        // Peer groups: [g0, g0, g1, g2]. GROUPS 1 PRECEDING → CURRENT ROW at
835        // pos 2 (group 1) covers groups 0..=1 → indices 0..=1.
836        let peer_groups = vec![0usize, 0, 1, 2];
837        let order = vec![
838            Value::Integer(1),
839            Value::Integer(1),
840            Value::Integer(2),
841            Value::Integer(3),
842        ];
843        let f = frame("groups", FrameBound::Preceding(1), FrameBound::CurrentRow);
844        assert_eq!(
845            evaluate_v_frame_bounds(&f, 2, 4, &order, &peer_groups),
846            (0, 2)
847        );
848    }
849}