Skip to main content

oxirs_arq/
window_function.rs

1// SPARQL-style window functions (ROW_NUMBER, RANK, DENSE_RANK, NTILE, LAG, LEAD, CumSum, CumCount)
2// Added in v1.1.0 Round 7
3
4use std::collections::HashMap;
5
6/// A value that can appear in a window function computation.
7#[derive(Debug, Clone, PartialEq)]
8pub enum WindowValue {
9    Integer(i64),
10    Float(f64),
11    Text(String),
12    Null,
13}
14
15impl PartialOrd for WindowValue {
16    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
17        Some(self.cmp_window(other))
18    }
19}
20
21impl WindowValue {
22    fn cmp_window(&self, other: &Self) -> std::cmp::Ordering {
23        use std::cmp::Ordering;
24        match (self, other) {
25            (WindowValue::Null, WindowValue::Null) => Ordering::Equal,
26            (WindowValue::Null, _) => Ordering::Less,
27            (_, WindowValue::Null) => Ordering::Greater,
28            (WindowValue::Integer(a), WindowValue::Integer(b)) => a.cmp(b),
29            (WindowValue::Float(a), WindowValue::Float(b)) => {
30                a.partial_cmp(b).unwrap_or(Ordering::Equal)
31            }
32            (WindowValue::Integer(a), WindowValue::Float(b)) => {
33                (*a as f64).partial_cmp(b).unwrap_or(Ordering::Equal)
34            }
35            (WindowValue::Float(a), WindowValue::Integer(b)) => {
36                a.partial_cmp(&(*b as f64)).unwrap_or(Ordering::Equal)
37            }
38            (WindowValue::Text(a), WindowValue::Text(b)) => a.cmp(b),
39            (WindowValue::Integer(_) | WindowValue::Float(_), WindowValue::Text(_)) => {
40                Ordering::Less
41            }
42            (WindowValue::Text(_), WindowValue::Integer(_) | WindowValue::Float(_)) => {
43                Ordering::Greater
44            }
45        }
46    }
47
48    /// Try to extract a numeric f64 value.
49    #[allow(dead_code)]
50    fn as_f64(&self) -> Option<f64> {
51        match self {
52            WindowValue::Integer(i) => Some(*i as f64),
53            WindowValue::Float(f) => Some(*f),
54            _ => None,
55        }
56    }
57}
58
59/// A single row in a window computation, mapping column names to values.
60#[derive(Debug, Clone)]
61pub struct WindowRow {
62    pub values: HashMap<String, WindowValue>,
63}
64
65impl WindowRow {
66    pub fn new(values: HashMap<String, WindowValue>) -> Self {
67        Self { values }
68    }
69
70    pub fn get(&self, column: &str) -> Option<&WindowValue> {
71        self.values.get(column)
72    }
73
74    pub fn set(&mut self, column: String, value: WindowValue) {
75        self.values.insert(column, value);
76    }
77}
78
79/// Window function variants.
80#[derive(Debug, Clone)]
81pub enum WindowFunc {
82    RowNumber,
83    Rank,
84    DenseRank,
85    Ntile(usize),
86    Lag {
87        column: String,
88        offset: usize,
89        default: Option<WindowValue>,
90    },
91    Lead {
92        column: String,
93        offset: usize,
94        default: Option<WindowValue>,
95    },
96    CumSum {
97        column: String,
98    },
99    CumCount,
100}
101
102/// Full window specification: partition + order + function.
103#[derive(Debug, Clone)]
104pub struct WindowSpec {
105    pub partition_by: Vec<String>,
106    pub order_by: Vec<(String, bool)>, // (column_name, ascending)
107    pub func: WindowFunc,
108    pub output_column: String,
109}
110
111/// Errors that can occur during window function application.
112#[derive(Debug)]
113pub enum WindowError {
114    ColumnNotFound(String),
115    InvalidNtile(String),
116    InvalidOffset(String),
117    NonNumericColumn(String),
118}
119
120impl std::fmt::Display for WindowError {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            WindowError::ColumnNotFound(c) => write!(f, "Column not found: {c}"),
124            WindowError::InvalidNtile(m) => write!(f, "Invalid NTILE: {m}"),
125            WindowError::InvalidOffset(m) => write!(f, "Invalid offset: {m}"),
126            WindowError::NonNumericColumn(c) => write!(f, "Non-numeric column: {c}"),
127        }
128    }
129}
130
131impl std::error::Error for WindowError {}
132
133/// Compute a partition key string for a row.
134fn partition_key(row: &WindowRow, partition_by: &[String]) -> String {
135    partition_by
136        .iter()
137        .map(|col| match row.values.get(col) {
138            Some(WindowValue::Integer(i)) => format!("i:{i}"),
139            Some(WindowValue::Float(f)) => format!("f:{f}"),
140            Some(WindowValue::Text(t)) => format!("t:{t}"),
141            Some(WindowValue::Null) | None => "null".to_string(),
142        })
143        .collect::<Vec<_>>()
144        .join("|")
145}
146
147/// Compare two rows by the order_by specification.
148fn cmp_rows(a: &WindowRow, b: &WindowRow, order_by: &[(String, bool)]) -> std::cmp::Ordering {
149    for (col, ascending) in order_by {
150        let av = a.values.get(col).unwrap_or(&WindowValue::Null);
151        let bv = b.values.get(col).unwrap_or(&WindowValue::Null);
152        let ord = av.cmp_window(bv);
153        if ord != std::cmp::Ordering::Equal {
154            return if *ascending { ord } else { ord.reverse() };
155        }
156    }
157    std::cmp::Ordering::Equal
158}
159
160/// Apply a single window function over all rows, returning (row, computed_value) pairs.
161pub fn apply_window(
162    rows: &[WindowRow],
163    spec: &WindowSpec,
164) -> Result<Vec<(WindowRow, WindowValue)>, WindowError> {
165    if rows.is_empty() {
166        return Ok(vec![]);
167    }
168
169    // Collect original indices grouped by partition key
170    let mut partitions: HashMap<String, Vec<usize>> = HashMap::new();
171    for (idx, row) in rows.iter().enumerate() {
172        let key = partition_key(row, &spec.partition_by);
173        partitions.entry(key).or_default().push(idx);
174    }
175
176    // Sort indices within each partition by order_by
177    for indices in partitions.values_mut() {
178        indices.sort_by(|&a, &b| cmp_rows(&rows[a], &rows[b], &spec.order_by));
179    }
180
181    let mut results: Vec<Option<WindowValue>> = vec![None; rows.len()];
182
183    for indices in partitions.values() {
184        compute_partition_values(rows, indices, &spec.func, &spec.order_by, &mut results)?;
185    }
186
187    Ok(rows
188        .iter()
189        .enumerate()
190        .map(|(i, row)| (row.clone(), results[i].take().unwrap_or(WindowValue::Null)))
191        .collect())
192}
193
194fn compute_partition_values(
195    rows: &[WindowRow],
196    sorted_indices: &[usize],
197    func: &WindowFunc,
198    order_by: &[(String, bool)],
199    results: &mut [Option<WindowValue>],
200) -> Result<(), WindowError> {
201    let n = sorted_indices.len();
202    match func {
203        WindowFunc::RowNumber => {
204            for (rank, &orig_idx) in sorted_indices.iter().enumerate() {
205                results[orig_idx] = Some(WindowValue::Integer((rank + 1) as i64));
206            }
207        }
208        WindowFunc::Rank => {
209            let mut current_rank = 1usize;
210            let mut i = 0;
211            while i < n {
212                // Find the extent of the tie group
213                let mut j = i + 1;
214                while j < n
215                    && cmp_rows(&rows[sorted_indices[i]], &rows[sorted_indices[j]], order_by)
216                        == std::cmp::Ordering::Equal
217                {
218                    j += 1;
219                }
220                // All rows from i..j get current_rank
221                for &orig_idx in &sorted_indices[i..j] {
222                    results[orig_idx] = Some(WindowValue::Integer(current_rank as i64));
223                }
224                current_rank += j - i; // skip ranks for ties
225                i = j;
226            }
227        }
228        WindowFunc::DenseRank => {
229            let mut current_rank = 1usize;
230            let mut i = 0;
231            while i < n {
232                let mut j = i + 1;
233                while j < n
234                    && cmp_rows(&rows[sorted_indices[i]], &rows[sorted_indices[j]], order_by)
235                        == std::cmp::Ordering::Equal
236                {
237                    j += 1;
238                }
239                for &orig_idx in &sorted_indices[i..j] {
240                    results[orig_idx] = Some(WindowValue::Integer(current_rank as i64));
241                }
242                current_rank += 1; // dense: no gaps
243                i = j;
244            }
245        }
246        WindowFunc::Ntile(buckets) => {
247            if *buckets == 0 {
248                return Err(WindowError::InvalidNtile(
249                    "NTILE bucket count must be > 0".to_string(),
250                ));
251            }
252            let total = n;
253            let buckets = *buckets;
254            for (pos, &orig_idx) in sorted_indices.iter().enumerate() {
255                // Standard NTILE formula
256                let bucket = (pos * buckets / total) + 1;
257                results[orig_idx] = Some(WindowValue::Integer(bucket as i64));
258            }
259        }
260        WindowFunc::Lag {
261            column,
262            offset,
263            default,
264        } => {
265            for (pos, &orig_idx) in sorted_indices.iter().enumerate() {
266                let value = if pos >= *offset {
267                    let source_idx = sorted_indices[pos - offset];
268                    rows[source_idx]
269                        .values
270                        .get(column)
271                        .cloned()
272                        .unwrap_or(WindowValue::Null)
273                } else {
274                    default.clone().unwrap_or(WindowValue::Null)
275                };
276                results[orig_idx] = Some(value);
277            }
278        }
279        WindowFunc::Lead {
280            column,
281            offset,
282            default,
283        } => {
284            for (pos, &orig_idx) in sorted_indices.iter().enumerate() {
285                let value = if pos + offset < n {
286                    let source_idx = sorted_indices[pos + offset];
287                    rows[source_idx]
288                        .values
289                        .get(column)
290                        .cloned()
291                        .unwrap_or(WindowValue::Null)
292                } else {
293                    default.clone().unwrap_or(WindowValue::Null)
294                };
295                results[orig_idx] = Some(value);
296            }
297        }
298        WindowFunc::CumSum { column } => {
299            let mut running_sum = 0.0f64;
300            let mut running_int: Option<i64> = Some(0);
301            for &orig_idx in sorted_indices.iter() {
302                let val = rows[orig_idx]
303                    .values
304                    .get(column)
305                    .unwrap_or(&WindowValue::Null);
306                match val {
307                    WindowValue::Integer(i) => {
308                        running_sum += *i as f64;
309                        running_int = running_int.and_then(|s| s.checked_add(*i));
310                    }
311                    WindowValue::Float(f) => {
312                        running_sum += f;
313                        running_int = None;
314                    }
315                    WindowValue::Null => {
316                        // NULLs contribute 0
317                    }
318                    WindowValue::Text(_) => {
319                        return Err(WindowError::NonNumericColumn(column.clone()));
320                    }
321                }
322                let result_val = if let Some(int_sum) = running_int {
323                    WindowValue::Integer(int_sum)
324                } else {
325                    WindowValue::Float(running_sum)
326                };
327                results[orig_idx] = Some(result_val);
328            }
329        }
330        WindowFunc::CumCount => {
331            for (pos, &orig_idx) in sorted_indices.iter().enumerate() {
332                results[orig_idx] = Some(WindowValue::Integer((pos + 1) as i64));
333            }
334        }
335    }
336    Ok(())
337}
338
339/// Apply multiple window functions, adding their output columns to each row.
340pub fn apply_windows(
341    mut rows: Vec<WindowRow>,
342    specs: &[WindowSpec],
343) -> Result<Vec<WindowRow>, WindowError> {
344    for spec in specs {
345        let result_pairs = apply_window(&rows, spec)?;
346        for (idx, (_row, value)) in result_pairs.into_iter().enumerate() {
347            rows[idx].values.insert(spec.output_column.clone(), value);
348        }
349    }
350    Ok(rows)
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    fn make_row(pairs: &[(&str, WindowValue)]) -> WindowRow {
358        let mut values = HashMap::new();
359        for (k, v) in pairs {
360            values.insert(k.to_string(), v.clone());
361        }
362        WindowRow { values }
363    }
364
365    fn int_spec(func: WindowFunc, order_col: &str, out: &str) -> WindowSpec {
366        WindowSpec {
367            partition_by: vec![],
368            order_by: vec![(order_col.to_string(), true)],
369            func,
370            output_column: out.to_string(),
371        }
372    }
373
374    fn partitioned_spec(
375        func: WindowFunc,
376        partition_col: &str,
377        order_col: &str,
378        out: &str,
379    ) -> WindowSpec {
380        WindowSpec {
381            partition_by: vec![partition_col.to_string()],
382            order_by: vec![(order_col.to_string(), true)],
383            func,
384            output_column: out.to_string(),
385        }
386    }
387
388    // ---- ROW_NUMBER ----
389
390    #[test]
391    fn test_row_number_sequential() {
392        let rows = vec![
393            make_row(&[("n", WindowValue::Integer(3))]),
394            make_row(&[("n", WindowValue::Integer(1))]),
395            make_row(&[("n", WindowValue::Integer(2))]),
396        ];
397        let spec = int_spec(WindowFunc::RowNumber, "n", "rn");
398        let result = apply_window(&rows, &spec).unwrap();
399        // Collect (n_value, rn_value)
400        let pairs: Vec<(i64, i64)> = result
401            .iter()
402            .map(|(row, val)| {
403                let n = match row.values["n"] {
404                    WindowValue::Integer(i) => i,
405                    _ => 0,
406                };
407                let rn = match val {
408                    WindowValue::Integer(i) => *i,
409                    _ => 0,
410                };
411                (n, rn)
412            })
413            .collect();
414        // Sorted by n: 1->1, 2->2, 3->3
415        for (n, rn) in &pairs {
416            assert_eq!(*rn, *n, "Expected rn == n for n={n}");
417        }
418        let mut nums: Vec<i64> = pairs.iter().map(|(_, rn)| *rn).collect();
419        nums.sort();
420        assert_eq!(nums, vec![1, 2, 3]);
421    }
422
423    #[test]
424    fn test_row_number_empty() {
425        let rows: Vec<WindowRow> = vec![];
426        let spec = int_spec(WindowFunc::RowNumber, "n", "rn");
427        let result = apply_window(&rows, &spec).unwrap();
428        assert!(result.is_empty());
429    }
430
431    #[test]
432    fn test_row_number_single() {
433        let rows = vec![make_row(&[("n", WindowValue::Integer(42))])];
434        let spec = int_spec(WindowFunc::RowNumber, "n", "rn");
435        let result = apply_window(&rows, &spec).unwrap();
436        assert_eq!(result.len(), 1);
437        assert_eq!(result[0].1, WindowValue::Integer(1));
438    }
439
440    // ---- RANK ----
441
442    #[test]
443    fn test_rank_no_ties() {
444        let rows = vec![
445            make_row(&[("v", WindowValue::Integer(10))]),
446            make_row(&[("v", WindowValue::Integer(20))]),
447            make_row(&[("v", WindowValue::Integer(30))]),
448        ];
449        let spec = int_spec(WindowFunc::Rank, "v", "r");
450        let result = apply_window(&rows, &spec).unwrap();
451        let mut ranks: Vec<i64> = result
452            .iter()
453            .map(|(_, v)| match v {
454                WindowValue::Integer(i) => *i,
455                _ => 0,
456            })
457            .collect();
458        ranks.sort();
459        assert_eq!(ranks, vec![1, 2, 3]);
460    }
461
462    #[test]
463    fn test_rank_with_ties() {
464        let rows = vec![
465            make_row(&[("v", WindowValue::Integer(10))]),
466            make_row(&[("v", WindowValue::Integer(10))]),
467            make_row(&[("v", WindowValue::Integer(20))]),
468        ];
469        let spec = int_spec(WindowFunc::Rank, "v", "r");
470        let result = apply_window(&rows, &spec).unwrap();
471        let mut ranks: Vec<i64> = result
472            .iter()
473            .map(|(_, v)| match v {
474                WindowValue::Integer(i) => *i,
475                _ => 0,
476            })
477            .collect();
478        ranks.sort();
479        // Two tied at rank 1, then gap → rank 3
480        assert_eq!(ranks, vec![1, 1, 3]);
481    }
482
483    #[test]
484    fn test_rank_all_tied() {
485        let rows = vec![
486            make_row(&[("v", WindowValue::Integer(5))]),
487            make_row(&[("v", WindowValue::Integer(5))]),
488            make_row(&[("v", WindowValue::Integer(5))]),
489        ];
490        let spec = int_spec(WindowFunc::Rank, "v", "r");
491        let result = apply_window(&rows, &spec).unwrap();
492        for (_, val) in &result {
493            assert_eq!(*val, WindowValue::Integer(1));
494        }
495    }
496
497    // ---- DENSE_RANK ----
498
499    #[test]
500    fn test_dense_rank_with_ties() {
501        let rows = vec![
502            make_row(&[("v", WindowValue::Integer(10))]),
503            make_row(&[("v", WindowValue::Integer(10))]),
504            make_row(&[("v", WindowValue::Integer(20))]),
505        ];
506        let spec = int_spec(WindowFunc::DenseRank, "v", "dr");
507        let result = apply_window(&rows, &spec).unwrap();
508        let mut ranks: Vec<i64> = result
509            .iter()
510            .map(|(_, v)| match v {
511                WindowValue::Integer(i) => *i,
512                _ => 0,
513            })
514            .collect();
515        ranks.sort();
516        // No gaps: 1, 1, 2
517        assert_eq!(ranks, vec![1, 1, 2]);
518    }
519
520    #[test]
521    fn test_dense_rank_no_ties() {
522        let rows = vec![
523            make_row(&[("v", WindowValue::Integer(1))]),
524            make_row(&[("v", WindowValue::Integer(2))]),
525            make_row(&[("v", WindowValue::Integer(3))]),
526        ];
527        let spec = int_spec(WindowFunc::DenseRank, "v", "dr");
528        let result = apply_window(&rows, &spec).unwrap();
529        let mut ranks: Vec<i64> = result
530            .iter()
531            .map(|(_, v)| match v {
532                WindowValue::Integer(i) => *i,
533                _ => 0,
534            })
535            .collect();
536        ranks.sort();
537        assert_eq!(ranks, vec![1, 2, 3]);
538    }
539
540    #[test]
541    fn test_dense_rank_multiple_ties() {
542        let rows = vec![
543            make_row(&[("v", WindowValue::Integer(10))]),
544            make_row(&[("v", WindowValue::Integer(10))]),
545            make_row(&[("v", WindowValue::Integer(20))]),
546            make_row(&[("v", WindowValue::Integer(20))]),
547            make_row(&[("v", WindowValue::Integer(30))]),
548        ];
549        let spec = int_spec(WindowFunc::DenseRank, "v", "dr");
550        let result = apply_window(&rows, &spec).unwrap();
551        let mut ranks: Vec<i64> = result
552            .iter()
553            .map(|(_, v)| match v {
554                WindowValue::Integer(i) => *i,
555                _ => 0,
556            })
557            .collect();
558        ranks.sort();
559        assert_eq!(ranks, vec![1, 1, 2, 2, 3]);
560    }
561
562    // ---- NTILE ----
563
564    #[test]
565    fn test_ntile_4_even() {
566        let rows: Vec<WindowRow> = (1i64..=8)
567            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
568            .collect();
569        let spec = int_spec(WindowFunc::Ntile(4), "v", "nt");
570        let result = apply_window(&rows, &spec).unwrap();
571        let mut buckets: Vec<i64> = result
572            .iter()
573            .map(|(_, v)| match v {
574                WindowValue::Integer(i) => *i,
575                _ => 0,
576            })
577            .collect();
578        buckets.sort();
579        // 8 rows, 4 buckets → 2 per bucket: 1,1,2,2,3,3,4,4
580        assert_eq!(buckets, vec![1, 1, 2, 2, 3, 3, 4, 4]);
581    }
582
583    #[test]
584    fn test_ntile_3_uneven() {
585        let rows: Vec<WindowRow> = (1i64..=7)
586            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
587            .collect();
588        let spec = int_spec(WindowFunc::Ntile(3), "v", "nt");
589        let result = apply_window(&rows, &spec).unwrap();
590        let mut buckets: Vec<i64> = result
591            .iter()
592            .map(|(_, v)| match v {
593                WindowValue::Integer(i) => *i,
594                _ => 0,
595            })
596            .collect();
597        buckets.sort();
598        assert_eq!(buckets.len(), 7);
599        // All values 1, 2, or 3
600        for b in &buckets {
601            assert!(*b >= 1 && *b <= 3, "bucket out of range: {b}");
602        }
603    }
604
605    #[test]
606    fn test_ntile_zero_error() {
607        let rows = vec![make_row(&[("v", WindowValue::Integer(1))])];
608        let spec = int_spec(WindowFunc::Ntile(0), "v", "nt");
609        assert!(matches!(
610            apply_window(&rows, &spec),
611            Err(WindowError::InvalidNtile(_))
612        ));
613    }
614
615    #[test]
616    fn test_ntile_1() {
617        let rows: Vec<WindowRow> = (1i64..=5)
618            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
619            .collect();
620        let spec = int_spec(WindowFunc::Ntile(1), "v", "nt");
621        let result = apply_window(&rows, &spec).unwrap();
622        for (_, val) in &result {
623            assert_eq!(*val, WindowValue::Integer(1));
624        }
625    }
626
627    // ---- LAG ----
628
629    #[test]
630    fn test_lag_offset_1_no_default() {
631        let rows: Vec<WindowRow> = (1i64..=4)
632            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
633            .collect();
634        let spec = int_spec(
635            WindowFunc::Lag {
636                column: "v".to_string(),
637                offset: 1,
638                default: None,
639            },
640            "v",
641            "lag",
642        );
643        let result = apply_window(&rows, &spec).unwrap();
644        // Sorted by v: 1,2,3,4. LAG(1): null,1,2,3
645        let mut sorted_pairs: Vec<(i64, &WindowValue)> = result
646            .iter()
647            .map(|(row, val)| {
648                let n = match row.values["v"] {
649                    WindowValue::Integer(i) => i,
650                    _ => 0,
651                };
652                (n, val)
653            })
654            .collect();
655        sorted_pairs.sort_by_key(|(n, _)| *n);
656        assert_eq!(sorted_pairs[0].1, &WindowValue::Null);
657        assert_eq!(sorted_pairs[1].1, &WindowValue::Integer(1));
658        assert_eq!(sorted_pairs[2].1, &WindowValue::Integer(2));
659        assert_eq!(sorted_pairs[3].1, &WindowValue::Integer(3));
660    }
661
662    #[test]
663    fn test_lag_with_default() {
664        let rows: Vec<WindowRow> = (1i64..=3)
665            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
666            .collect();
667        let spec = int_spec(
668            WindowFunc::Lag {
669                column: "v".to_string(),
670                offset: 1,
671                default: Some(WindowValue::Integer(-1)),
672            },
673            "v",
674            "lag",
675        );
676        let result = apply_window(&rows, &spec).unwrap();
677        let mut sorted_pairs: Vec<(i64, WindowValue)> = result
678            .iter()
679            .map(|(row, val)| {
680                let n = match row.values["v"] {
681                    WindowValue::Integer(i) => i,
682                    _ => 0,
683                };
684                (n, val.clone())
685            })
686            .collect();
687        sorted_pairs.sort_by_key(|(n, _)| *n);
688        assert_eq!(sorted_pairs[0].1, WindowValue::Integer(-1));
689        assert_eq!(sorted_pairs[1].1, WindowValue::Integer(1));
690        assert_eq!(sorted_pairs[2].1, WindowValue::Integer(2));
691    }
692
693    #[test]
694    fn test_lag_offset_2() {
695        let rows: Vec<WindowRow> = (1i64..=5)
696            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
697            .collect();
698        let spec = int_spec(
699            WindowFunc::Lag {
700                column: "v".to_string(),
701                offset: 2,
702                default: Some(WindowValue::Integer(0)),
703            },
704            "v",
705            "lag",
706        );
707        let result = apply_window(&rows, &spec).unwrap();
708        let mut sorted_pairs: Vec<(i64, WindowValue)> = result
709            .iter()
710            .map(|(row, val)| {
711                let n = match row.values["v"] {
712                    WindowValue::Integer(i) => i,
713                    _ => 0,
714                };
715                (n, val.clone())
716            })
717            .collect();
718        sorted_pairs.sort_by_key(|(n, _)| *n);
719        assert_eq!(sorted_pairs[0].1, WindowValue::Integer(0)); // default
720        assert_eq!(sorted_pairs[1].1, WindowValue::Integer(0)); // default
721        assert_eq!(sorted_pairs[2].1, WindowValue::Integer(1));
722        assert_eq!(sorted_pairs[3].1, WindowValue::Integer(2));
723        assert_eq!(sorted_pairs[4].1, WindowValue::Integer(3));
724    }
725
726    // ---- LEAD ----
727
728    #[test]
729    fn test_lead_offset_1_no_default() {
730        let rows: Vec<WindowRow> = (1i64..=4)
731            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
732            .collect();
733        let spec = int_spec(
734            WindowFunc::Lead {
735                column: "v".to_string(),
736                offset: 1,
737                default: None,
738            },
739            "v",
740            "lead",
741        );
742        let result = apply_window(&rows, &spec).unwrap();
743        let mut sorted_pairs: Vec<(i64, WindowValue)> = result
744            .iter()
745            .map(|(row, val)| {
746                let n = match row.values["v"] {
747                    WindowValue::Integer(i) => i,
748                    _ => 0,
749                };
750                (n, val.clone())
751            })
752            .collect();
753        sorted_pairs.sort_by_key(|(n, _)| *n);
754        assert_eq!(sorted_pairs[0].1, WindowValue::Integer(2));
755        assert_eq!(sorted_pairs[1].1, WindowValue::Integer(3));
756        assert_eq!(sorted_pairs[2].1, WindowValue::Integer(4));
757        assert_eq!(sorted_pairs[3].1, WindowValue::Null);
758    }
759
760    #[test]
761    fn test_lead_with_default() {
762        let rows: Vec<WindowRow> = (1i64..=3)
763            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
764            .collect();
765        let spec = int_spec(
766            WindowFunc::Lead {
767                column: "v".to_string(),
768                offset: 1,
769                default: Some(WindowValue::Integer(99)),
770            },
771            "v",
772            "lead",
773        );
774        let result = apply_window(&rows, &spec).unwrap();
775        let mut sorted_pairs: Vec<(i64, WindowValue)> = result
776            .iter()
777            .map(|(row, val)| {
778                let n = match row.values["v"] {
779                    WindowValue::Integer(i) => i,
780                    _ => 0,
781                };
782                (n, val.clone())
783            })
784            .collect();
785        sorted_pairs.sort_by_key(|(n, _)| *n);
786        assert_eq!(sorted_pairs[2].1, WindowValue::Integer(99));
787    }
788
789    // ---- CumSum ----
790
791    #[test]
792    fn test_cum_sum_integers() {
793        let rows: Vec<WindowRow> = (1i64..=5)
794            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
795            .collect();
796        let spec = int_spec(
797            WindowFunc::CumSum {
798                column: "v".to_string(),
799            },
800            "v",
801            "cs",
802        );
803        let result = apply_window(&rows, &spec).unwrap();
804        let mut sorted_pairs: Vec<(i64, WindowValue)> = result
805            .iter()
806            .map(|(row, val)| {
807                let n = match row.values["v"] {
808                    WindowValue::Integer(i) => i,
809                    _ => 0,
810                };
811                (n, val.clone())
812            })
813            .collect();
814        sorted_pairs.sort_by_key(|(n, _)| *n);
815        assert_eq!(sorted_pairs[0].1, WindowValue::Integer(1));
816        assert_eq!(sorted_pairs[1].1, WindowValue::Integer(3));
817        assert_eq!(sorted_pairs[2].1, WindowValue::Integer(6));
818        assert_eq!(sorted_pairs[3].1, WindowValue::Integer(10));
819        assert_eq!(sorted_pairs[4].1, WindowValue::Integer(15));
820    }
821
822    #[test]
823    fn test_cum_sum_floats() {
824        let rows = vec![
825            make_row(&[
826                ("v", WindowValue::Float(1.5)),
827                ("k", WindowValue::Integer(1)),
828            ]),
829            make_row(&[
830                ("v", WindowValue::Float(2.5)),
831                ("k", WindowValue::Integer(2)),
832            ]),
833            make_row(&[
834                ("v", WindowValue::Float(1.0)),
835                ("k", WindowValue::Integer(3)),
836            ]),
837        ];
838        let spec = WindowSpec {
839            partition_by: vec![],
840            order_by: vec![("k".to_string(), true)],
841            func: WindowFunc::CumSum {
842                column: "v".to_string(),
843            },
844            output_column: "cs".to_string(),
845        };
846        let result = apply_window(&rows, &spec).unwrap();
847        let mut sorted_pairs: Vec<(i64, f64)> = result
848            .iter()
849            .map(|(row, val)| {
850                let k = match row.values["k"] {
851                    WindowValue::Integer(i) => i,
852                    _ => 0,
853                };
854                let cs = match val {
855                    WindowValue::Float(f) => *f,
856                    WindowValue::Integer(i) => *i as f64,
857                    _ => 0.0,
858                };
859                (k, cs)
860            })
861            .collect();
862        sorted_pairs.sort_by_key(|(k, _)| *k);
863        assert!((sorted_pairs[0].1 - 1.5).abs() < 1e-10);
864        assert!((sorted_pairs[1].1 - 4.0).abs() < 1e-10);
865        assert!((sorted_pairs[2].1 - 5.0).abs() < 1e-10);
866    }
867
868    #[test]
869    fn test_cum_sum_text_error() {
870        let rows = vec![make_row(&[("v", WindowValue::Text("hello".to_string()))])];
871        let spec = int_spec(
872            WindowFunc::CumSum {
873                column: "v".to_string(),
874            },
875            "v",
876            "cs",
877        );
878        assert!(matches!(
879            apply_window(&rows, &spec),
880            Err(WindowError::NonNumericColumn(_))
881        ));
882    }
883
884    // ---- CumCount ----
885
886    #[test]
887    fn test_cum_count() {
888        let rows: Vec<WindowRow> = (1i64..=5)
889            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
890            .collect();
891        let spec = int_spec(WindowFunc::CumCount, "v", "cc");
892        let result = apply_window(&rows, &spec).unwrap();
893        let mut sorted_pairs: Vec<(i64, i64)> = result
894            .iter()
895            .map(|(row, val)| {
896                let n = match row.values["v"] {
897                    WindowValue::Integer(i) => i,
898                    _ => 0,
899                };
900                let cc = match val {
901                    WindowValue::Integer(i) => *i,
902                    _ => 0,
903                };
904                (n, cc)
905            })
906            .collect();
907        sorted_pairs.sort_by_key(|(n, _)| *n);
908        for (i, (_, cc)) in sorted_pairs.iter().enumerate() {
909            assert_eq!(*cc, (i + 1) as i64);
910        }
911    }
912
913    // ---- Partitioned ----
914
915    #[test]
916    fn test_row_number_partitioned() {
917        let rows = vec![
918            make_row(&[
919                ("g", WindowValue::Text("A".into())),
920                ("v", WindowValue::Integer(2)),
921            ]),
922            make_row(&[
923                ("g", WindowValue::Text("A".into())),
924                ("v", WindowValue::Integer(1)),
925            ]),
926            make_row(&[
927                ("g", WindowValue::Text("B".into())),
928                ("v", WindowValue::Integer(3)),
929            ]),
930            make_row(&[
931                ("g", WindowValue::Text("B".into())),
932                ("v", WindowValue::Integer(1)),
933            ]),
934        ];
935        let spec = partitioned_spec(WindowFunc::RowNumber, "g", "v", "rn");
936        let result = apply_window(&rows, &spec).unwrap();
937        let mut pairs: Vec<(String, i64, i64)> = result
938            .iter()
939            .map(|(row, val)| {
940                let g = match &row.values["g"] {
941                    WindowValue::Text(s) => s.clone(),
942                    _ => String::new(),
943                };
944                let v = match row.values["v"] {
945                    WindowValue::Integer(i) => i,
946                    _ => 0,
947                };
948                let rn = match val {
949                    WindowValue::Integer(i) => *i,
950                    _ => 0,
951                };
952                (g, v, rn)
953            })
954            .collect();
955        pairs.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
956        // Group A: v=1->rn=1, v=2->rn=2
957        let group_a: Vec<_> = pairs.iter().filter(|(g, _, _)| g == "A").collect();
958        assert_eq!(group_a.len(), 2);
959        assert_eq!(group_a[0].2, 1);
960        assert_eq!(group_a[1].2, 2);
961        // Group B: v=1->rn=1, v=3->rn=2
962        let group_b: Vec<_> = pairs.iter().filter(|(g, _, _)| g == "B").collect();
963        assert_eq!(group_b.len(), 2);
964        assert_eq!(group_b[0].2, 1);
965        assert_eq!(group_b[1].2, 2);
966    }
967
968    #[test]
969    fn test_rank_partitioned_with_ties() {
970        let rows = vec![
971            make_row(&[
972                ("g", WindowValue::Text("A".into())),
973                ("v", WindowValue::Integer(5)),
974            ]),
975            make_row(&[
976                ("g", WindowValue::Text("A".into())),
977                ("v", WindowValue::Integer(5)),
978            ]),
979            make_row(&[
980                ("g", WindowValue::Text("A".into())),
981                ("v", WindowValue::Integer(10)),
982            ]),
983        ];
984        let spec = partitioned_spec(WindowFunc::Rank, "g", "v", "r");
985        let result = apply_window(&rows, &spec).unwrap();
986        let mut ranks: Vec<i64> = result
987            .iter()
988            .map(|(_, v)| match v {
989                WindowValue::Integer(i) => *i,
990                _ => 0,
991            })
992            .collect();
993        ranks.sort();
994        assert_eq!(ranks, vec![1, 1, 3]);
995    }
996
997    #[test]
998    fn test_dense_rank_partitioned() {
999        let rows = vec![
1000            make_row(&[
1001                ("g", WindowValue::Text("X".into())),
1002                ("v", WindowValue::Integer(5)),
1003            ]),
1004            make_row(&[
1005                ("g", WindowValue::Text("X".into())),
1006                ("v", WindowValue::Integer(5)),
1007            ]),
1008            make_row(&[
1009                ("g", WindowValue::Text("X".into())),
1010                ("v", WindowValue::Integer(10)),
1011            ]),
1012        ];
1013        let spec = partitioned_spec(WindowFunc::DenseRank, "g", "v", "dr");
1014        let result = apply_window(&rows, &spec).unwrap();
1015        let mut ranks: Vec<i64> = result
1016            .iter()
1017            .map(|(_, v)| match v {
1018                WindowValue::Integer(i) => *i,
1019                _ => 0,
1020            })
1021            .collect();
1022        ranks.sort();
1023        assert_eq!(ranks, vec![1, 1, 2]);
1024    }
1025
1026    // ---- ORDER_BY descending ----
1027
1028    #[test]
1029    fn test_row_number_descending() {
1030        let rows: Vec<WindowRow> = (1i64..=4)
1031            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
1032            .collect();
1033        let spec = WindowSpec {
1034            partition_by: vec![],
1035            order_by: vec![("v".to_string(), false)], // descending
1036            func: WindowFunc::RowNumber,
1037            output_column: "rn".to_string(),
1038        };
1039        let result = apply_window(&rows, &spec).unwrap();
1040        let mut pairs: Vec<(i64, i64)> = result
1041            .iter()
1042            .map(|(row, val)| {
1043                let v = match row.values["v"] {
1044                    WindowValue::Integer(i) => i,
1045                    _ => 0,
1046                };
1047                let rn = match val {
1048                    WindowValue::Integer(i) => *i,
1049                    _ => 0,
1050                };
1051                (v, rn)
1052            })
1053            .collect();
1054        pairs.sort_by_key(|(v, _)| *v);
1055        // v=1 gets rn=4 (last in descending order), v=4 gets rn=1
1056        assert_eq!(pairs[0].1, 4); // v=1
1057        assert_eq!(pairs[3].1, 1); // v=4
1058    }
1059
1060    #[test]
1061    fn test_rank_descending() {
1062        let rows = vec![
1063            make_row(&[("v", WindowValue::Integer(30))]),
1064            make_row(&[("v", WindowValue::Integer(20))]),
1065            make_row(&[("v", WindowValue::Integer(10))]),
1066        ];
1067        let spec = WindowSpec {
1068            partition_by: vec![],
1069            order_by: vec![("v".to_string(), false)],
1070            func: WindowFunc::Rank,
1071            output_column: "r".to_string(),
1072        };
1073        let result = apply_window(&rows, &spec).unwrap();
1074        let mut pairs: Vec<(i64, i64)> = result
1075            .iter()
1076            .map(|(row, val)| {
1077                let v = match row.values["v"] {
1078                    WindowValue::Integer(i) => i,
1079                    _ => 0,
1080                };
1081                let r = match val {
1082                    WindowValue::Integer(i) => *i,
1083                    _ => 0,
1084                };
1085                (v, r)
1086            })
1087            .collect();
1088        pairs.sort_by_key(|(v, _)| *v);
1089        // desc order: 30->1, 20->2, 10->3
1090        assert_eq!(pairs[0].1, 3); // v=10 gets rank 3
1091        assert_eq!(pairs[2].1, 1); // v=30 gets rank 1
1092    }
1093
1094    // ---- apply_windows (multiple) ----
1095
1096    #[test]
1097    fn test_apply_windows_multiple_specs() {
1098        let rows: Vec<WindowRow> = (1i64..=3)
1099            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
1100            .collect();
1101        let specs = vec![
1102            int_spec(WindowFunc::RowNumber, "v", "rn"),
1103            int_spec(
1104                WindowFunc::CumSum {
1105                    column: "v".to_string(),
1106                },
1107                "v",
1108                "cs",
1109            ),
1110        ];
1111        let result = apply_windows(rows, &specs).unwrap();
1112        assert_eq!(result.len(), 3);
1113        for row in &result {
1114            assert!(row.values.contains_key("rn"));
1115            assert!(row.values.contains_key("cs"));
1116        }
1117    }
1118
1119    #[test]
1120    fn test_apply_windows_empty() {
1121        let rows: Vec<WindowRow> = vec![];
1122        let specs = vec![int_spec(WindowFunc::RowNumber, "v", "rn")];
1123        let result = apply_windows(rows, &specs).unwrap();
1124        assert!(result.is_empty());
1125    }
1126
1127    // ---- WindowValue ordering ----
1128
1129    #[test]
1130    fn test_window_value_null_smallest() {
1131        assert_eq!(
1132            WindowValue::Null.cmp_window(&WindowValue::Integer(0)),
1133            std::cmp::Ordering::Less
1134        );
1135        assert_eq!(
1136            WindowValue::Integer(0).cmp_window(&WindowValue::Null),
1137            std::cmp::Ordering::Greater
1138        );
1139        assert_eq!(
1140            WindowValue::Null.cmp_window(&WindowValue::Null),
1141            std::cmp::Ordering::Equal
1142        );
1143    }
1144
1145    #[test]
1146    fn test_window_value_int_vs_float() {
1147        assert_eq!(
1148            WindowValue::Integer(1).cmp_window(&WindowValue::Float(2.0)),
1149            std::cmp::Ordering::Less
1150        );
1151        assert_eq!(
1152            WindowValue::Float(1.5).cmp_window(&WindowValue::Integer(1)),
1153            std::cmp::Ordering::Greater
1154        );
1155    }
1156
1157    #[test]
1158    fn test_window_value_text_ordering() {
1159        assert_eq!(
1160            WindowValue::Text("apple".into()).cmp_window(&WindowValue::Text("banana".into())),
1161            std::cmp::Ordering::Less
1162        );
1163    }
1164
1165    // ---- Null handling in data ----
1166
1167    #[test]
1168    fn test_cum_sum_with_nulls() {
1169        let rows = vec![
1170            make_row(&[
1171                ("v", WindowValue::Integer(1)),
1172                ("k", WindowValue::Integer(1)),
1173            ]),
1174            make_row(&[("v", WindowValue::Null), ("k", WindowValue::Integer(2))]),
1175            make_row(&[
1176                ("v", WindowValue::Integer(3)),
1177                ("k", WindowValue::Integer(3)),
1178            ]),
1179        ];
1180        let spec = WindowSpec {
1181            partition_by: vec![],
1182            order_by: vec![("k".to_string(), true)],
1183            func: WindowFunc::CumSum {
1184                column: "v".to_string(),
1185            },
1186            output_column: "cs".to_string(),
1187        };
1188        let result = apply_window(&rows, &spec).unwrap();
1189        let mut sorted: Vec<(i64, WindowValue)> = result
1190            .iter()
1191            .map(|(row, val)| {
1192                let k = match row.values["k"] {
1193                    WindowValue::Integer(i) => i,
1194                    _ => 0,
1195                };
1196                (k, val.clone())
1197            })
1198            .collect();
1199        sorted.sort_by_key(|(k, _)| *k);
1200        assert_eq!(sorted[0].1, WindowValue::Integer(1));
1201        assert_eq!(sorted[1].1, WindowValue::Integer(1)); // null contributes 0
1202        assert_eq!(sorted[2].1, WindowValue::Integer(4));
1203    }
1204
1205    #[test]
1206    fn test_lag_empty_partition() {
1207        let rows: Vec<WindowRow> = vec![];
1208        let spec = int_spec(
1209            WindowFunc::Lag {
1210                column: "v".to_string(),
1211                offset: 1,
1212                default: None,
1213            },
1214            "v",
1215            "lag",
1216        );
1217        let result = apply_window(&rows, &spec).unwrap();
1218        assert!(result.is_empty());
1219    }
1220
1221    #[test]
1222    fn test_lead_large_offset() {
1223        let rows: Vec<WindowRow> = (1i64..=3)
1224            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
1225            .collect();
1226        let spec = int_spec(
1227            WindowFunc::Lead {
1228                column: "v".to_string(),
1229                offset: 10,
1230                default: Some(WindowValue::Integer(-99)),
1231            },
1232            "v",
1233            "lead",
1234        );
1235        let result = apply_window(&rows, &spec).unwrap();
1236        for (_, val) in &result {
1237            assert_eq!(*val, WindowValue::Integer(-99));
1238        }
1239    }
1240
1241    #[test]
1242    fn test_ntile_more_buckets_than_rows() {
1243        let rows = vec![
1244            make_row(&[("v", WindowValue::Integer(1))]),
1245            make_row(&[("v", WindowValue::Integer(2))]),
1246        ];
1247        let spec = int_spec(WindowFunc::Ntile(5), "v", "nt");
1248        let result = apply_window(&rows, &spec).unwrap();
1249        // 2 rows, 5 buckets: each row gets a different bucket (1 and 2 roughly)
1250        assert_eq!(result.len(), 2);
1251        for (_, val) in &result {
1252            match val {
1253                WindowValue::Integer(b) => assert!(*b >= 1 && *b <= 5, "bucket out of range: {b}"),
1254                _ => panic!("Expected integer bucket"),
1255            }
1256        }
1257    }
1258
1259    #[test]
1260    fn test_as_f64() {
1261        assert_eq!(WindowValue::Integer(5).as_f64(), Some(5.0));
1262        assert_eq!(WindowValue::Float(2.71).as_f64(), Some(2.71));
1263        assert_eq!(WindowValue::Text("x".into()).as_f64(), None);
1264        assert_eq!(WindowValue::Null.as_f64(), None);
1265    }
1266
1267    #[test]
1268    fn test_window_row_get_set() {
1269        let mut row = make_row(&[("a", WindowValue::Integer(1))]);
1270        assert_eq!(row.get("a"), Some(&WindowValue::Integer(1)));
1271        assert_eq!(row.get("b"), None);
1272        row.set("b".to_string(), WindowValue::Float(2.0));
1273        assert_eq!(row.get("b"), Some(&WindowValue::Float(2.0)));
1274    }
1275
1276    #[test]
1277    fn test_cum_count_matches_row_number() {
1278        let rows: Vec<WindowRow> = (1i64..=6)
1279            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
1280            .collect();
1281        let rn_spec = int_spec(WindowFunc::RowNumber, "v", "rn");
1282        let cc_spec = int_spec(WindowFunc::CumCount, "v", "cc");
1283        let rn_result = apply_window(&rows, &rn_spec).unwrap();
1284        let cc_result = apply_window(&rows, &cc_spec).unwrap();
1285        let mut rn_vals: Vec<i64> = rn_result
1286            .iter()
1287            .map(|(_, v)| match v {
1288                WindowValue::Integer(i) => *i,
1289                _ => 0,
1290            })
1291            .collect();
1292        let mut cc_vals: Vec<i64> = cc_result
1293            .iter()
1294            .map(|(_, v)| match v {
1295                WindowValue::Integer(i) => *i,
1296                _ => 0,
1297            })
1298            .collect();
1299        rn_vals.sort();
1300        cc_vals.sort();
1301        assert_eq!(rn_vals, cc_vals);
1302    }
1303
1304    #[test]
1305    fn test_lead_offset_2() {
1306        let rows: Vec<WindowRow> = (1i64..=5)
1307            .map(|i| make_row(&[("v", WindowValue::Integer(i))]))
1308            .collect();
1309        let spec = int_spec(
1310            WindowFunc::Lead {
1311                column: "v".to_string(),
1312                offset: 2,
1313                default: Some(WindowValue::Integer(0)),
1314            },
1315            "v",
1316            "lead",
1317        );
1318        let result = apply_window(&rows, &spec).unwrap();
1319        let mut sorted: Vec<(i64, WindowValue)> = result
1320            .iter()
1321            .map(|(row, val)| {
1322                let v = match row.values["v"] {
1323                    WindowValue::Integer(i) => i,
1324                    _ => 0,
1325                };
1326                (v, val.clone())
1327            })
1328            .collect();
1329        sorted.sort_by_key(|(v, _)| *v);
1330        assert_eq!(sorted[0].1, WindowValue::Integer(3));
1331        assert_eq!(sorted[1].1, WindowValue::Integer(4));
1332        assert_eq!(sorted[2].1, WindowValue::Integer(5));
1333        assert_eq!(sorted[3].1, WindowValue::Integer(0));
1334        assert_eq!(sorted[4].1, WindowValue::Integer(0));
1335    }
1336}