vibesql_executor/evaluator/window/
value.rs

1//! Value window functions
2//!
3//! Implements LAG, LEAD, FIRST_VALUE, and LAST_VALUE for accessing values from other rows in the
4//! partition.
5
6use vibesql_ast::Expression;
7use vibesql_types::SqlValue;
8
9use super::{partitioning::Partition, utils::evaluate_default_value};
10
11/// Evaluate LAG() value window function
12///
13/// Accesses a value from a previous row in the partition (offset rows back).
14/// Returns the value from (current_row_idx - offset).
15/// If offset goes before partition start, returns default value (or NULL).
16///
17/// Signature: LAG(expr [, offset [, default]])
18/// - expr: Expression to evaluate on the offset row
19/// - offset: Number of rows back (default: 1)
20/// - default: Value to return when offset is out of bounds (default: NULL)
21///
22/// Example: LAG(price, 1) OVER (ORDER BY date)
23/// Example: LAG(revenue, 1, 0) OVER (PARTITION BY region ORDER BY month)
24///
25/// Requires: ORDER BY in window spec (partition must be sorted)
26pub fn evaluate_lag<F>(
27    partition: &Partition,
28    current_row_idx: usize,
29    value_expr: &Expression,
30    offset: Option<i64>,
31    default: Option<&Expression>,
32    eval_fn: F,
33) -> Result<SqlValue, String>
34where
35    F: Fn(&Expression, &vibesql_storage::Row) -> Result<SqlValue, String>,
36{
37    let offset_val = offset.unwrap_or(1);
38
39    // Validate offset is non-negative
40    if offset_val < 0 {
41        return Err(format!("LAG offset must be non-negative, got {}", offset_val));
42    }
43
44    // Calculate target row index (current_row_idx - offset)
45    let target_idx = if offset_val as usize > current_row_idx {
46        // Offset goes before partition start - return default
47        return evaluate_default_value(default);
48    } else {
49        current_row_idx - offset_val as usize
50    };
51
52    // Get value from target row
53    if let Some(target_row) = partition.rows.get(target_idx) {
54        eval_fn(value_expr, target_row)
55    } else {
56        // Should not happen if bounds check is correct
57        Ok(evaluate_default_value(default)?)
58    }
59}
60
61/// Evaluate LEAD() value window function
62///
63/// Accesses a value from a subsequent row in the partition (offset rows forward).
64/// Returns the value from (current_row_idx + offset).
65/// If offset goes past partition end, returns default value (or NULL).
66///
67/// Signature: LEAD(expr [, offset [, default]])
68/// - expr: Expression to evaluate on the offset row
69/// - offset: Number of rows forward (default: 1)
70/// - default: Value to return when offset is out of bounds (default: NULL)
71///
72/// Example: LEAD(price, 1) OVER (ORDER BY date)
73/// Example: LEAD(sales, 3, 0) OVER (PARTITION BY product ORDER BY quarter)
74///
75/// Requires: ORDER BY in window spec (partition must be sorted)
76pub fn evaluate_lead<F>(
77    partition: &Partition,
78    current_row_idx: usize,
79    value_expr: &Expression,
80    offset: Option<i64>,
81    default: Option<&Expression>,
82    eval_fn: F,
83) -> Result<SqlValue, String>
84where
85    F: Fn(&Expression, &vibesql_storage::Row) -> Result<SqlValue, String>,
86{
87    let offset_val = offset.unwrap_or(1);
88
89    // Validate offset is non-negative
90    if offset_val < 0 {
91        return Err(format!("LEAD offset must be non-negative, got {}", offset_val));
92    }
93
94    // Calculate target row index (current_row_idx + offset)
95    let target_idx = current_row_idx + offset_val as usize;
96
97    // Check if target is beyond partition end
98    if target_idx >= partition.len() {
99        return evaluate_default_value(default);
100    }
101
102    // Get value from target row
103    if let Some(target_row) = partition.rows.get(target_idx) {
104        eval_fn(value_expr, target_row)
105    } else {
106        // Should not happen if bounds check is correct
107        Ok(evaluate_default_value(default)?)
108    }
109}
110
111/// Evaluate FIRST_VALUE() value window function
112///
113/// Returns the value of the expression evaluated on the first row of the partition.
114/// Useful for getting the initial value in an ordered partition.
115///
116/// Signature: FIRST_VALUE(expr)
117/// - expr: Expression to evaluate on the first row
118///
119/// Example: FIRST_VALUE(price) OVER (PARTITION BY product ORDER BY date)
120/// Example: FIRST_VALUE(salary) OVER (PARTITION BY department ORDER BY hire_date)
121///
122/// Note: Typically used with ORDER BY to get the "earliest" value according to ordering.
123pub fn evaluate_first_value<F>(
124    partition: &Partition,
125    value_expr: &Expression,
126    eval_fn: F,
127) -> Result<SqlValue, String>
128where
129    F: Fn(&Expression, &vibesql_storage::Row) -> Result<SqlValue, String>,
130{
131    // Get the first row in the partition
132    if let Some(first_row) = partition.rows.first() {
133        eval_fn(value_expr, first_row)
134    } else {
135        // Empty partition - return NULL
136        Ok(SqlValue::Null)
137    }
138}
139
140/// Evaluate LAST_VALUE() value window function
141///
142/// Returns the value of the expression evaluated on the last row of the partition.
143/// Useful for getting the final value in an ordered partition.
144///
145/// Signature: LAST_VALUE(expr)
146/// - expr: Expression to evaluate on the last row
147///
148/// Example: LAST_VALUE(price) OVER (PARTITION BY product ORDER BY date)
149/// Example: LAST_VALUE(status) OVER (PARTITION BY order_id ORDER BY timestamp)
150///
151/// Note: With default frame (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),
152/// LAST_VALUE returns the current row's value. To get the actual last value in the
153/// partition, use frame: RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
154/// This implementation always returns the last row in the partition.
155pub fn evaluate_last_value<F>(
156    partition: &Partition,
157    value_expr: &Expression,
158    eval_fn: F,
159) -> Result<SqlValue, String>
160where
161    F: Fn(&Expression, &vibesql_storage::Row) -> Result<SqlValue, String>,
162{
163    // Get the last row in the partition
164    if let Some(last_row) = partition.rows.last() {
165        eval_fn(value_expr, last_row)
166    } else {
167        // Empty partition - return NULL
168        Ok(SqlValue::Null)
169    }
170}
171
172/// Evaluate NTH_VALUE() value window function
173///
174/// Returns the value of the expression from the Nth row in the window frame.
175/// N is 1-based (NTH_VALUE(expr, 1) returns the first row's value).
176///
177/// Signature: NTH_VALUE(expr, n)
178/// - expr: Expression to evaluate on the Nth row
179/// - n: 1-based row position (must be positive integer)
180///
181/// Example: NTH_VALUE(price, 2) OVER (ORDER BY date) -- returns 2nd row's price
182/// Example: NTH_VALUE(name, 1) OVER (PARTITION BY dept ORDER BY salary DESC)
183///
184/// Returns NULL if N is out of bounds or if the partition has fewer than N rows.
185pub fn evaluate_nth_value<F>(
186    partition: &Partition,
187    n: i64,
188    value_expr: &Expression,
189    eval_fn: F,
190) -> Result<SqlValue, String>
191where
192    F: Fn(&Expression, &vibesql_storage::Row) -> Result<SqlValue, String>,
193{
194    // N must be positive (1-based indexing)
195    if n < 1 {
196        return Err(format!("NTH_VALUE n must be a positive integer, got {}", n));
197    }
198
199    // Convert to 0-based index
200    let idx = (n - 1) as usize;
201
202    // Check if index is within partition bounds
203    if idx >= partition.len() {
204        // Out of bounds - return NULL
205        return Ok(SqlValue::Null);
206    }
207
208    // Get value from the Nth row
209    if let Some(row) = partition.rows.get(idx) {
210        eval_fn(value_expr, row)
211    } else {
212        // Should not happen if bounds check is correct
213        Ok(SqlValue::Null)
214    }
215}