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}