radixdb_executor/window/mod.rs
1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Window Function Execution
16//!
17//! This module implements window function execution for SQL queries.
18//!
19//! Supports:
20//! - ROW_NUMBER() - Sequential row numbering
21//! - RANK() - Ranking with gaps
22//! - DENSE_RANK() - Ranking without gaps
23//! - NTILE(n) - Divides rows into n groups
24//! - LEAD(col, offset, default) - Access next row's value
25//! - LAG(col, offset, default) - Access previous row's value
26//!
27//! Window clauses:
28//! - OVER () - Entire result set as one partition
29//! - OVER (PARTITION BY col) - Partition by column values
30//! - OVER (ORDER BY col) - Order within partition
31
32#[cfg(feature = "parallel")]
33use rayon::prelude::*;
34use rustc_hash::FxHashMap;
35use smallvec::SmallVec;
36use std::cmp::Ordering;
37
38use radixdb_core::row_vec::RowVec;
39use radixdb_core::value::NULL_VALUE;
40use radixdb_core::{CompactVec, StringMap};
41use radixdb_core::{Error, Result, Row, Value};
42
43/// Type alias for partition keys - stack-allocated for common case (up to 4 columns)
44type PartitionKey = SmallVec<[Value; 4]>;
45
46use radixdb_functions::{FunctionRegistry, WindowFunction};
47use radixdb_sql::ast::*;
48use radixdb_storage::traits::{QueryResult, Table};
49
50use super::context::ExecutionContext;
51use super::expression::{ExpressionEval, MultiExpressionEval};
52use super::result::{ColumnarResult, ExecutorResult};
53use super::utils::build_column_index_map;
54
55mod aggregate;
56mod execute;
57mod partition;
58mod planning;
59#[cfg(test)]
60mod tests;
61
62/// Narrow composition contract required by window planning and execution.
63pub trait WindowHost: Sync {
64 fn window_function_registry(&self) -> &FunctionRegistry;
65}
66
67/// Single owner for window planning, partition state, execution and result
68/// finalization. The host supplies only the immutable function registry.
69pub struct WindowExecutor<'a, H: WindowHost + ?Sized> {
70 host: &'a H,
71}
72
73impl<'a, H: WindowHost + ?Sized> WindowExecutor<'a, H> {
74 fn new(host: &'a H) -> Self {
75 Self { host }
76 }
77}
78
79/// Compatibility surface used by the composition root while the surrounding
80/// SELECT orchestration is migrated.
81pub trait WindowExecutorExt: WindowHost {
82 fn execute_select_with_window_functions(
83 &self,
84 stmt: &SelectStatement,
85 ctx: &ExecutionContext,
86 base_rows: &[(i64, Row)],
87 base_columns: &[String],
88 ) -> Result<Box<dyn QueryResult>> {
89 WindowExecutor::new(self).execute_select_with_window_functions(
90 stmt,
91 ctx,
92 base_rows,
93 base_columns,
94 )
95 }
96
97 fn execute_select_with_window_functions_presorted(
98 &self,
99 stmt: &SelectStatement,
100 ctx: &ExecutionContext,
101 base_rows: &[(i64, Row)],
102 base_columns: &[String],
103 pre_sorted: Option<WindowPreSortedState>,
104 ) -> Result<Box<dyn QueryResult>> {
105 WindowExecutor::new(self).execute_select_with_window_functions_presorted(
106 stmt,
107 ctx,
108 base_rows,
109 base_columns,
110 pre_sorted,
111 )
112 }
113
114 fn execute_select_with_window_functions_pregrouped(
115 &self,
116 stmt: &SelectStatement,
117 ctx: &ExecutionContext,
118 base_rows: &[(i64, Row)],
119 base_columns: &[String],
120 pre_grouped: WindowPreGroupedState,
121 ) -> Result<Box<dyn QueryResult>> {
122 WindowExecutor::new(self).execute_select_with_window_functions_pregrouped(
123 stmt,
124 ctx,
125 base_rows,
126 base_columns,
127 pre_grouped,
128 )
129 }
130
131 fn execute_select_with_window_functions_lazy_partition(
132 &self,
133 stmt: &SelectStatement,
134 ctx: &ExecutionContext,
135 table: &dyn Table,
136 base_columns: &[String],
137 partition_col: &str,
138 limit: usize,
139 ) -> Result<Box<dyn QueryResult>> {
140 WindowExecutor::new(self).execute_select_with_window_functions_lazy_partition(
141 stmt,
142 ctx,
143 table,
144 base_columns,
145 partition_col,
146 limit,
147 )
148 }
149}
150
151impl<T: WindowHost + ?Sized> WindowExecutorExt for T {}
152
153/// Information about a window function call in a SELECT list
154#[derive(Clone, Debug)]
155pub struct WindowFunctionInfo {
156 /// The window function name (ROW_NUMBER, RANK, etc.)
157 pub name: String,
158 /// Arguments to the function (for LEAD, LAG, NTILE)
159 pub arguments: Vec<Expression>,
160 /// Partition by column names (simple identifiers only, for fast-path lookups)
161 pub partition_by: Vec<String>,
162 /// Original PARTITION BY expressions (includes function calls, complex exprs)
163 pub partition_by_exprs: Vec<Expression>,
164 /// Order by expressions
165 pub order_by: Vec<OrderByExpression>,
166 /// Window frame specification (e.g., ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)
167 pub frame: Option<WindowFrame>,
168 /// Result column name (may include alias)
169 pub column_name: String,
170 /// Whether DISTINCT was specified (for COUNT(DISTINCT col) OVER())
171 pub is_distinct: bool,
172}
173
174/// Information about a SELECT list item for window function processing
175pub struct SelectItem {
176 pub output_name: String,
177 pub source: SelectItemSource,
178}
179
180/// Source of a SELECT item value
181#[allow(clippy::large_enum_variant)]
182pub enum SelectItemSource {
183 BaseColumn(usize),
184 /// Window function name - stored in lowercase for O(1) lookup in window_value_map
185 WindowFunction(String),
186 Expression(Expression),
187 /// Expression containing one or more window functions.
188 /// Stores (expression, list_of_window_function_names_lowercase).
189 /// Each Window node in the expression is replaced with a placeholder identifier
190 /// referencing the corresponding name at the same index.
191 ExpressionWithWindow(Expression, Vec<String>),
192}
193
194/// Pre-sorted state for window function optimization
195/// When rows are pre-sorted by an indexed column, we can skip sorting in window functions
196#[derive(Clone, Debug)]
197pub struct WindowPreSortedState {
198 /// Column name that rows are sorted by (lowercase)
199 pub column: String,
200 /// Whether sorted in ascending order
201 pub ascending: bool,
202}
203
204/// Columnar layout for ORDER BY values - optimized for sorting performance
205///
206/// Instead of `Vec<Vec<(Value, bool)>>` (row-oriented, N allocations for N rows),
207/// this uses `Vec<Vec<Value>>` (column-oriented, K allocations for K ORDER BY columns).
208///
209/// Benefits:
210/// - Reduces allocations from O(N) to O(K) where K = number of ORDER BY columns
211/// - Stores ascending flags once per column instead of once per value
212/// - Better cache locality when accessing sort keys across rows
213#[derive(Clone, Debug)]
214pub struct ColumnarOrderByValues {
215 /// Column values: columns[col_idx][row_idx] = value
216 columns: Vec<Vec<Value>>,
217 /// Ascending flags: one per ORDER BY column
218 ascending: Vec<bool>,
219 /// Resolved NULL placement: one per ORDER BY column.
220 nulls_first: Vec<bool>,
221 /// Number of rows
222 num_rows: usize,
223}
224
225impl ColumnarOrderByValues {
226 /// Check if empty
227 #[inline]
228 pub fn is_empty(&self) -> bool {
229 self.num_rows == 0 || self.columns.is_empty()
230 }
231
232 /// Get number of columns
233 #[inline]
234 pub fn num_columns(&self) -> usize {
235 self.columns.len()
236 }
237
238 /// Get value at (row, column)
239 #[inline]
240 pub fn get(&self, row_idx: usize, col_idx: usize) -> Option<&Value> {
241 self.columns.get(col_idx).and_then(|col| col.get(row_idx))
242 }
243
244 /// Get first ORDER BY value for a row
245 #[inline]
246 pub fn get_first(&self, row_idx: usize) -> Option<&Value> {
247 self.get(row_idx, 0)
248 }
249
250 /// Get ascending flag for column
251 #[inline]
252 pub fn is_ascending(&self, col_idx: usize) -> bool {
253 self.ascending.get(col_idx).copied().unwrap_or(true)
254 }
255
256 /// Get resolved NULL placement for column.
257 #[inline]
258 pub fn nulls_first(&self, col_idx: usize) -> bool {
259 self.nulls_first.get(col_idx).copied().unwrap_or(false)
260 }
261
262 /// Compare ORDER BY values of two rows for equality (without cloning)
263 /// Returns true if all ORDER BY column values are equal
264 #[inline]
265 pub fn rows_equal(&self, row_a: usize, row_b: usize) -> bool {
266 for col in &self.columns {
267 let val_a = col.get(row_a);
268 let val_b = col.get(row_b);
269 match (val_a, val_b) {
270 (Some(a), Some(b)) if a == b => continue,
271 (None, None) => continue,
272 _ => return false,
273 }
274 }
275 true
276 }
277}
278
279/// Pre-grouped state for window function PARTITION BY optimization
280/// When rows are fetched grouped by an indexed partition column, we can skip hash-based grouping
281#[derive(Clone)]
282pub struct WindowPreGroupedState {
283 /// Pre-built partition map: partition key -> row indices
284 pub partition_map: FxHashMap<PartitionKey, Vec<usize>>,
285 /// The column name (lowercase) that the partition map was built from
286 pub partition_column: String,
287}