Skip to main content

nodedb_query/window/
value_eval.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Value-native window-function evaluator for the Lite embedded engine.
4//!
5//! Operates on `Vec<Vec<nodedb_types::Value>>` rows directly, without any
6//! serde_json dependency. Each spec appends one `Value` per row; the caller
7//! appends the returned column names to its `columns` vec.
8
9use std::collections::HashMap;
10
11use nodedb_types::Value;
12
13use super::spec::WindowFuncSpec;
14use super::value_agg::apply_v_aggregate;
15use super::value_partition::build_value_partitions;
16use crate::expr::types::SqlExpr;
17use crate::value_ops::compare_values;
18
19/// Error type for Value-mode window evaluation.
20#[derive(Debug, thiserror::Error)]
21pub enum WindowError {
22    #[error("window column '{name}' not found in result columns")]
23    ColumnNotFound { name: String },
24
25    #[error("window function argument error: {detail}")]
26    ArgEval { detail: String },
27
28    #[error("window frame error: {detail}")]
29    BadFrame { detail: String },
30
31    #[error("window expression: {0}")]
32    Eval(#[from] crate::expr::EvalError),
33}
34
35/// Evaluate window functions over a `Vec<Vec<Value>>` result set.
36///
37/// `column_index` maps column name → position in each row slice.
38/// For each spec, one `Value` is appended to every row. Returns the list of
39/// new column names, one per spec in spec order. `rows` keeps its input
40/// order; each spec's partitions are ordered by that spec's own ORDER BY,
41/// independent of the row order.
42pub fn evaluate_window_functions_value(
43    rows: &mut [Vec<Value>],
44    column_index: &HashMap<String, usize>,
45    specs: &[WindowFuncSpec],
46) -> Result<Vec<String>, WindowError> {
47    let mut new_cols: Vec<String> = Vec::with_capacity(specs.len());
48
49    for spec in specs {
50        let partitions = build_value_partitions(rows, column_index, spec)?;
51        let write_col = rows.first().map(|r| r.len()).unwrap_or(0);
52
53        for row in rows.iter_mut() {
54            row.push(Value::Null);
55        }
56
57        for partition_indices in &partitions {
58            match spec.func_name.as_str() {
59                "row_number" => apply_v_row_number(rows, partition_indices, write_col),
60                "rank" => apply_v_rank(rows, partition_indices, column_index, spec, write_col)?,
61                "dense_rank" => {
62                    apply_v_dense_rank(rows, partition_indices, column_index, spec, write_col)?
63                }
64                "ntile" => apply_v_ntile(rows, partition_indices, spec, write_col)?,
65                "percent_rank" => {
66                    apply_v_percent_rank(rows, partition_indices, column_index, spec, write_col)?
67                }
68                "cume_dist" => {
69                    apply_v_cume_dist(rows, partition_indices, column_index, spec, write_col)?
70                }
71                "lag" => apply_v_lag(rows, partition_indices, column_index, spec, write_col)?,
72                "lead" => apply_v_lead(rows, partition_indices, column_index, spec, write_col)?,
73                "nth_value" => {
74                    apply_v_nth_value(rows, partition_indices, column_index, spec, write_col)?
75                }
76                "sum" | "count" | "avg" | "min" | "max" | "first_value" | "last_value" => {
77                    apply_v_aggregate(rows, partition_indices, column_index, spec, write_col)?
78                }
79                other => {
80                    return Err(WindowError::ArgEval {
81                        detail: format!(
82                            "unknown window function '{other}'; valid names: row_number, rank, \
83                             dense_rank, ntile, percent_rank, cume_dist, lag, lead, nth_value, \
84                             sum, count, avg, min, max, first_value, last_value"
85                        ),
86                    });
87                }
88            }
89        }
90
91        new_cols.push(spec.alias.clone());
92    }
93
94    Ok(new_cols)
95}
96
97// ── Value comparison helpers (pub(super) for value_agg, value_partition) ─────
98
99pub(super) fn cmp_values(a: &Value, b: &Value) -> std::cmp::Ordering {
100    match (a, b) {
101        (Value::Null, Value::Null) => std::cmp::Ordering::Equal,
102        (Value::Null, _) => std::cmp::Ordering::Less,
103        (_, Value::Null) => std::cmp::Ordering::Greater,
104        (va, vb) => compare_values(va, vb),
105    }
106}
107
108pub(super) fn order_keys_equal_v(
109    rows: &[Vec<Value>],
110    a: usize,
111    b: usize,
112    column_index: &HashMap<String, usize>,
113    order_by: &[(SqlExpr, bool)],
114) -> Result<bool, WindowError> {
115    for (expr, _) in order_by {
116        let row_a = rows.get(a).map(|r| r.as_slice()).unwrap_or(&[]);
117        let row_b = rows.get(b).map(|r| r.as_slice()).unwrap_or(&[]);
118        let va = eval_arg_for_row(expr, row_a, column_index)?;
119        let vb = eval_arg_for_row(expr, row_b, column_index)?;
120        if !matches!(cmp_values(&va, &vb), std::cmp::Ordering::Equal) {
121            return Ok(false);
122        }
123    }
124    Ok(true)
125}
126
127// ── Argument evaluation (pub(super) for value_agg) ────────────────────────────
128
129/// Evaluate a window-function argument expression against one column-major
130/// row. A division/modulo-by-zero surfaces as
131/// `Err(EvalError::DivisionByZero)` — which the value-path callers convert into
132/// `WindowError` via `?` — rather than being folded to `NULL`.
133pub(super) fn eval_arg_for_row(
134    expr: &SqlExpr,
135    row: &[Value],
136    column_index: &HashMap<String, usize>,
137) -> Result<Value, crate::expr::EvalError> {
138    match expr {
139        SqlExpr::Column(name) => Ok(column_index
140            .get(name.as_str())
141            .and_then(|&idx| row.get(idx))
142            .cloned()
143            .unwrap_or(Value::Null)),
144        SqlExpr::Literal(v) => Ok(v.clone()),
145        other => {
146            let doc = row_to_obj(row, column_index);
147            Ok(other.eval(&doc)?)
148        }
149    }
150}
151
152fn row_to_obj(row: &[Value], column_index: &HashMap<String, usize>) -> Value {
153    let mut map = HashMap::new();
154    for (name, &idx) in column_index {
155        if let Some(v) = row.get(idx) {
156            map.insert(name.clone(), v.clone());
157        }
158    }
159    Value::Object(map)
160}
161
162fn usize_arg(spec: &WindowFuncSpec, idx: usize, default: usize) -> usize {
163    spec.args
164        .get(idx)
165        .and_then(|e| match e {
166            SqlExpr::Literal(v) => v.as_f64().map(|n| n as usize),
167            _ => None,
168        })
169        .unwrap_or(default)
170}
171
172fn default_arg_value(spec: &WindowFuncSpec, idx: usize) -> Value {
173    spec.args
174        .get(idx)
175        .and_then(|e| match e {
176            SqlExpr::Literal(v) => Some(v.clone()),
177            _ => None,
178        })
179        .unwrap_or(Value::Null)
180}
181
182// ── Cell write helper (pub(super) for value_agg) ──────────────────────────────
183
184pub(super) fn set_cell(rows: &mut [Vec<Value>], row_idx: usize, col_idx: usize, val: Value) {
185    if let Some(row) = rows.get_mut(row_idx)
186        && let Some(cell) = row.get_mut(col_idx)
187    {
188        *cell = val;
189    }
190}
191
192// ── Ranking functions ─────────────────────────────────────────────────────────
193
194fn apply_v_row_number(rows: &mut [Vec<Value>], indices: &[usize], write_col: usize) {
195    for (rank, &i) in indices.iter().enumerate() {
196        set_cell(rows, i, write_col, Value::Integer((rank + 1) as i64));
197    }
198}
199
200fn apply_v_rank(
201    rows: &mut [Vec<Value>],
202    indices: &[usize],
203    column_index: &HashMap<String, usize>,
204    spec: &WindowFuncSpec,
205    write_col: usize,
206) -> Result<(), WindowError> {
207    if indices.is_empty() {
208        return Ok(());
209    }
210    let mut current_rank = 1usize;
211    set_cell(rows, indices[0], write_col, Value::Integer(1));
212    for pos in 1..indices.len() {
213        if !order_keys_equal_v(
214            rows,
215            indices[pos - 1],
216            indices[pos],
217            column_index,
218            &spec.order_by,
219        )? {
220            current_rank = pos + 1;
221        }
222        set_cell(
223            rows,
224            indices[pos],
225            write_col,
226            Value::Integer(current_rank as i64),
227        );
228    }
229    Ok(())
230}
231
232fn apply_v_dense_rank(
233    rows: &mut [Vec<Value>],
234    indices: &[usize],
235    column_index: &HashMap<String, usize>,
236    spec: &WindowFuncSpec,
237    write_col: usize,
238) -> Result<(), WindowError> {
239    if indices.is_empty() {
240        return Ok(());
241    }
242    let mut current_rank = 1usize;
243    set_cell(rows, indices[0], write_col, Value::Integer(1));
244    for pos in 1..indices.len() {
245        if !order_keys_equal_v(
246            rows,
247            indices[pos - 1],
248            indices[pos],
249            column_index,
250            &spec.order_by,
251        )? {
252            current_rank += 1;
253        }
254        set_cell(
255            rows,
256            indices[pos],
257            write_col,
258            Value::Integer(current_rank as i64),
259        );
260    }
261    Ok(())
262}
263
264fn apply_v_ntile(
265    rows: &mut [Vec<Value>],
266    indices: &[usize],
267    spec: &WindowFuncSpec,
268    write_col: usize,
269) -> Result<(), WindowError> {
270    let n = usize_arg(spec, 0, 1).max(1);
271    let total = indices.len();
272    if total == 0 {
273        return Ok(());
274    }
275    for (pos, &i) in indices.iter().enumerate() {
276        let bucket = (pos * n / total) + 1;
277        set_cell(rows, i, write_col, Value::Integer(bucket as i64));
278    }
279    Ok(())
280}
281
282fn apply_v_percent_rank(
283    rows: &mut [Vec<Value>],
284    indices: &[usize],
285    column_index: &HashMap<String, usize>,
286    spec: &WindowFuncSpec,
287    write_col: usize,
288) -> Result<(), WindowError> {
289    let total = indices.len();
290    if total == 0 {
291        return Ok(());
292    }
293    if total == 1 {
294        set_cell(rows, indices[0], write_col, Value::Float(0.0));
295        return Ok(());
296    }
297    let denom = (total - 1) as f64;
298    let mut current_rank = 1usize;
299    set_cell(rows, indices[0], write_col, Value::Float(0.0));
300    for pos in 1..total {
301        if !order_keys_equal_v(
302            rows,
303            indices[pos - 1],
304            indices[pos],
305            column_index,
306            &spec.order_by,
307        )? {
308            current_rank = pos + 1;
309        }
310        let pr = (current_rank - 1) as f64 / denom;
311        set_cell(rows, indices[pos], write_col, Value::Float(pr));
312    }
313    Ok(())
314}
315
316fn apply_v_cume_dist(
317    rows: &mut [Vec<Value>],
318    indices: &[usize],
319    column_index: &HashMap<String, usize>,
320    spec: &WindowFuncSpec,
321    write_col: usize,
322) -> Result<(), WindowError> {
323    let total = indices.len();
324    if total == 0 {
325        return Ok(());
326    }
327    let denom = total as f64;
328    let mut group_start = 0;
329    while group_start < total {
330        let mut group_end = group_start + 1;
331        while group_end < total
332            && order_keys_equal_v(
333                rows,
334                indices[group_start],
335                indices[group_end],
336                column_index,
337                &spec.order_by,
338            )?
339        {
340            group_end += 1;
341        }
342        let cd = group_end as f64 / denom;
343        for &idx in &indices[group_start..group_end] {
344            set_cell(rows, idx, write_col, Value::Float(cd));
345        }
346        group_start = group_end;
347    }
348    Ok(())
349}
350
351// ── Offset functions ──────────────────────────────────────────────────────────
352
353fn collect_arg_values(
354    rows: &[Vec<Value>],
355    indices: &[usize],
356    column_index: &HashMap<String, usize>,
357    spec: &WindowFuncSpec,
358) -> Result<Vec<Value>, WindowError> {
359    indices
360        .iter()
361        .map(|&i| match (rows.get(i), spec.args.first()) {
362            (Some(row), Some(expr)) => Ok(eval_arg_for_row(expr, row, column_index)?),
363            _ => Ok(Value::Null),
364        })
365        .collect()
366}
367
368fn apply_v_lag(
369    rows: &mut [Vec<Value>],
370    indices: &[usize],
371    column_index: &HashMap<String, usize>,
372    spec: &WindowFuncSpec,
373    write_col: usize,
374) -> Result<(), WindowError> {
375    let offset = usize_arg(spec, 1, 1);
376    let default = default_arg_value(spec, 2);
377    let values = collect_arg_values(rows, indices, column_index, spec)?;
378    for (pos, &i) in indices.iter().enumerate() {
379        let val = if pos >= offset {
380            values[pos - offset].clone()
381        } else {
382            default.clone()
383        };
384        set_cell(rows, i, write_col, val);
385    }
386    Ok(())
387}
388
389fn apply_v_lead(
390    rows: &mut [Vec<Value>],
391    indices: &[usize],
392    column_index: &HashMap<String, usize>,
393    spec: &WindowFuncSpec,
394    write_col: usize,
395) -> Result<(), WindowError> {
396    let offset = usize_arg(spec, 1, 1);
397    let default = default_arg_value(spec, 2);
398    let values = collect_arg_values(rows, indices, column_index, spec)?;
399    for (pos, &i) in indices.iter().enumerate() {
400        let val = if pos + offset < indices.len() {
401            values[pos + offset].clone()
402        } else {
403            default.clone()
404        };
405        set_cell(rows, i, write_col, val);
406    }
407    Ok(())
408}
409
410fn apply_v_nth_value(
411    rows: &mut [Vec<Value>],
412    indices: &[usize],
413    column_index: &HashMap<String, usize>,
414    spec: &WindowFuncSpec,
415    write_col: usize,
416) -> Result<(), WindowError> {
417    let n = usize_arg(spec, 1, 1).max(1);
418    let values = collect_arg_values(rows, indices, column_index, spec)?;
419    for (pos, &i) in indices.iter().enumerate() {
420        let val = if pos + 1 >= n {
421            values[n - 1].clone()
422        } else {
423            Value::Null
424        };
425        set_cell(rows, i, write_col, val);
426    }
427    Ok(())
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use crate::expr::types::SqlExpr;
434    use crate::window::spec::WindowFrame;
435
436    fn ci(names: &[&str]) -> HashMap<String, usize> {
437        names
438            .iter()
439            .enumerate()
440            .map(|(i, n)| (n.to_string(), i))
441            .collect()
442    }
443
444    fn spec(
445        func: &str,
446        args: Vec<SqlExpr>,
447        partition_by: Vec<SqlExpr>,
448        order_by: Vec<(SqlExpr, bool)>,
449    ) -> WindowFuncSpec {
450        WindowFuncSpec {
451            alias: format!("w_{func}"),
452            func_name: func.to_string(),
453            args,
454            partition_by,
455            order_by,
456            frame: WindowFrame::default(),
457        }
458    }
459
460    fn col(name: &str) -> SqlExpr {
461        SqlExpr::Column(name.to_string())
462    }
463
464    /// Single-column rows under name "v"; the window result lands at index 1.
465    fn rows_v(vals: &[i64]) -> Vec<Vec<Value>> {
466        vals.iter().map(|&v| vec![Value::Integer(v)]).collect()
467    }
468
469    fn out_int(rows: &[Vec<Value>], col_idx: usize) -> Vec<i64> {
470        rows.iter()
471            .map(|r| match &r[col_idx] {
472                Value::Integer(n) => *n,
473                other => panic!("expected integer, got {other:?}"),
474            })
475            .collect()
476    }
477
478    fn out_f64(rows: &[Vec<Value>], col_idx: usize) -> Vec<f64> {
479        rows.iter().map(|r| r[col_idx].as_f64().unwrap()).collect()
480    }
481
482    #[test]
483    fn row_number_sequential() {
484        let mut rows = rows_v(&[5, 5, 5]);
485        let cols = ci(&["v"]);
486        let s = spec("row_number", vec![], vec![], vec![]);
487        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
488        assert_eq!(out_int(&rows, 1), vec![1, 2, 3]);
489    }
490
491    #[test]
492    fn rank_handles_ties() {
493        let mut rows = rows_v(&[10, 10, 20]);
494        let cols = ci(&["v"]);
495        let s = spec("rank", vec![], vec![], vec![(col("v"), true)]);
496        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
497        assert_eq!(out_int(&rows, 1), vec![1, 1, 3]);
498    }
499
500    #[test]
501    fn dense_rank_handles_ties() {
502        let mut rows = rows_v(&[10, 10, 20]);
503        let cols = ci(&["v"]);
504        let s = spec("dense_rank", vec![], vec![], vec![(col("v"), true)]);
505        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
506        assert_eq!(out_int(&rows, 1), vec![1, 1, 2]);
507    }
508
509    #[test]
510    fn ntile_buckets() {
511        let mut rows = rows_v(&[1, 2, 3]);
512        let cols = ci(&["v"]);
513        let s = spec(
514            "ntile",
515            vec![SqlExpr::Literal(Value::Integer(2))],
516            vec![],
517            vec![(col("v"), true)],
518        );
519        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
520        assert_eq!(out_int(&rows, 1), vec![1, 1, 2]);
521    }
522
523    #[test]
524    fn lag_default_and_offset() {
525        let mut rows = rows_v(&[1, 2, 3]);
526        let cols = ci(&["v"]);
527        let s = spec("lag", vec![col("v")], vec![], vec![(col("v"), true)]);
528        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
529        // First row has no predecessor → Null; rest carry the prior value.
530        assert!(matches!(rows[0][1], Value::Null));
531        assert_eq!(rows[1][1].as_f64().unwrap() as i64, 1);
532        assert_eq!(rows[2][1].as_f64().unwrap() as i64, 2);
533    }
534
535    #[test]
536    fn lead_boundary() {
537        let mut rows = rows_v(&[1, 2, 3]);
538        let cols = ci(&["v"]);
539        let s = spec("lead", vec![col("v")], vec![], vec![(col("v"), true)]);
540        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
541        assert_eq!(rows[0][1].as_f64().unwrap() as i64, 2);
542        assert_eq!(rows[1][1].as_f64().unwrap() as i64, 3);
543        // Last row has no successor → Null.
544        assert!(matches!(rows[2][1], Value::Null));
545    }
546
547    #[test]
548    fn percent_rank_and_cume_dist() {
549        let cols = ci(&["v"]);
550
551        let mut rows = rows_v(&[10, 10, 20]);
552        let pr = spec("percent_rank", vec![], vec![], vec![(col("v"), true)]);
553        evaluate_window_functions_value(&mut rows, &cols, &[pr]).unwrap();
554        let pr_out = out_f64(&rows, 1);
555        assert!((pr_out[0] - 0.0).abs() < 1e-9);
556        assert!((pr_out[1] - 0.0).abs() < 1e-9);
557        assert!((pr_out[2] - 1.0).abs() < 1e-9);
558
559        let mut rows = rows_v(&[10, 10, 20]);
560        let cd = spec("cume_dist", vec![], vec![], vec![(col("v"), true)]);
561        evaluate_window_functions_value(&mut rows, &cols, &[cd]).unwrap();
562        let cd_out = out_f64(&rows, 1);
563        assert!((cd_out[0] - 2.0 / 3.0).abs() < 1e-9);
564        assert!((cd_out[1] - 2.0 / 3.0).abs() < 1e-9);
565        assert!((cd_out[2] - 1.0).abs() < 1e-9);
566    }
567
568    #[test]
569    fn partition_resets_row_number() {
570        let cols = ci(&["g", "v"]);
571        let mut rows = vec![
572            vec![Value::Integer(1), Value::Integer(100)],
573            vec![Value::Integer(1), Value::Integer(101)],
574            vec![Value::Integer(2), Value::Integer(102)],
575        ];
576        let s = spec("row_number", vec![], vec![col("g")], vec![]);
577        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
578        // Two rows in partition g=1 → 1,2; one row in g=2 → 1. Result at idx 2.
579        assert_eq!(out_int(&rows, 2), vec![1, 2, 1]);
580    }
581
582    #[test]
583    fn rank_orders_by_spec_order_by_not_row_arrival_order() {
584        // Rows arrive as 10, 30, 20 — not sorted by value. RANK() OVER
585        // (ORDER BY v DESC) must rank 30(1), 20(2), 10(3); the row array
586        // order must stay 10, 30, 20.
587        let mut rows = rows_v(&[10, 30, 20]);
588        let cols = ci(&["v"]);
589        let s = spec("rank", vec![], vec![], vec![(col("v"), false)]);
590        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
591        assert_eq!(out_int(&rows, 0), vec![10, 30, 20]);
592        assert_eq!(out_int(&rows, 1), vec![3, 1, 2]);
593    }
594
595    #[test]
596    fn unknown_function_errors() {
597        let mut rows = rows_v(&[1]);
598        let cols = ci(&["v"]);
599        let s = spec("nonexistent", vec![], vec![], vec![]);
600        let err = evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap_err();
601        assert!(matches!(err, WindowError::ArgEval { .. }));
602    }
603
604    #[test]
605    fn missing_partition_column_is_null_keyed() {
606        // Partitioning on an absent column must not panic — every row keys to
607        // Null and lands in one partition.
608        let cols = ci(&["v"]);
609        let mut rows = rows_v(&[1, 2]);
610        let s = spec("row_number", vec![], vec![col("missing")], vec![]);
611        evaluate_window_functions_value(&mut rows, &cols, &[s]).unwrap();
612        assert_eq!(out_int(&rows, 1), vec![1, 2]);
613    }
614}