Skip to main content

nodedb_query/window/
arg.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Window-function argument evaluation.
4//!
5//! A window function's argument is an arbitrary expression — `SUM(price * qty)`,
6//! `LAG(10 / denom)`, `SUM(1)` — not merely a column reference. Every argument
7//! is evaluated per row through the shared row evaluator, so a
8//! division/modulo-by-zero inside an argument raises
9//! `EvalError::DivisionByZero` (SQLSTATE `22012`) exactly as it does in a
10//! projection or WHERE clause, and a non-column argument yields its computed
11//! value rather than a NULL placeholder.
12
13use nodedb_types::Value;
14
15use super::spec::WindowFuncSpec;
16use crate::expr::{EvalError, SqlExpr};
17
18/// Evaluated values of one window-function argument, aligned to the caller's
19/// partition `indices` (position `p` holds the value for row `indices[p]`).
20///
21/// `None` means the function was called without that argument — `COUNT(*)`,
22/// `ROW_NUMBER()` — which is distinct from an argument that evaluated to NULL.
23pub(super) type ArgValues = Option<Vec<Value>>;
24
25/// Evaluate argument `idx` of `spec` once for every row in the partition.
26///
27/// Evaluating up front keeps each row's argument evaluated exactly once even
28/// when the frame evaluator revisits rows, and gives every aggregate the same
29/// values the frame bounds are computed against.
30pub(super) fn eval_arg_values(
31    rows: &[(String, Value)],
32    indices: &[usize],
33    spec: &WindowFuncSpec,
34    idx: usize,
35) -> Result<ArgValues, EvalError> {
36    let Some(expr) = spec.args.get(idx) else {
37        return Ok(None);
38    };
39    let values = indices
40        .iter()
41        .map(|&i| expr.eval(&rows[i].1))
42        .collect::<Result<Vec<_>, _>>()?;
43    Ok(Some(values))
44}
45
46/// Value of argument `idx` at partition position `pos`, or NULL when the
47/// function was called without that argument.
48pub(super) fn arg_at(values: &ArgValues, pos: usize) -> Value {
49    values
50        .as_ref()
51        .and_then(|v| v.get(pos).cloned())
52        .unwrap_or(Value::Null)
53}
54
55/// Resolve a constant integer argument — the `LAG`/`LEAD` offset, `NTILE`
56/// bucket count, and `NTH_VALUE` position.
57///
58/// The planner rejects a non-constant in these positions before the spec ever
59/// reaches the evaluator, so the only way to observe `default` here is an
60/// argument the caller omitted (`LAG(x)` → offset 1).
61pub(super) fn const_usize_arg(spec: &WindowFuncSpec, idx: usize, default: usize) -> usize {
62    spec.args
63        .get(idx)
64        .and_then(|e| match e {
65            SqlExpr::Literal(v) => v.as_f64().map(|n| n as usize),
66            _ => None,
67        })
68        .unwrap_or(default)
69}
70
71/// Resolve the constant `default` argument of `LAG`/`LEAD` — the value
72/// returned when the offset falls outside the partition.
73pub(super) fn const_default_arg(spec: &WindowFuncSpec, idx: usize) -> Value {
74    spec.args
75        .get(idx)
76        .and_then(|e| match e {
77            SqlExpr::Literal(v) => Some(v.clone()),
78            _ => None,
79        })
80        .unwrap_or(Value::Null)
81}