1use super::{
10 compare_values, eval_scalar, value_to_f64, Batch, DefaultExpressionEvaluator, ExecError,
11 ExecResult, PhysicalOperator, RowSchema, SQLParam, ScalarEvalContext, ScalarExpr, SortKey,
12 Value,
13};
14use uqa_sql::expr::RowLookup;
15
16#[derive(Debug, Clone)]
17pub enum WindowKind {
18 RowNumber,
19 Rank,
20 DenseRank,
21 Lag(ScalarExpr, i64),
22 Lead(ScalarExpr, i64),
23 Ntile(i64),
24 AggSum(ScalarExpr),
25 AggCount(Option<ScalarExpr>),
26 AggAvg(ScalarExpr),
27 AggMin(ScalarExpr),
28 AggMax(ScalarExpr),
29}
30
31#[derive(Debug, Clone)]
32pub struct WindowSpec {
33 pub partition_by: Vec<ScalarExpr>,
34 pub order_by: Vec<SortKey>,
35}
36
37pub trait WindowExecutor: Send {
41 fn consume(&mut self, batch: Batch) -> ExecResult<()>;
44
45 fn finish(&mut self) -> ExecResult<crate::spill::SpillBuffer>;
47}
48
49pub struct Window<'a> {
50 child: Box<dyn PhysicalOperator + 'a>,
51 spec: WindowSpec,
52 functions: Vec<(String, WindowKind)>,
53 params: Vec<SQLParam>,
54 schema: RowSchema,
55 executor: Option<Box<dyn WindowExecutor + 'a>>,
56 work_mem_bytes: usize,
57 output: Option<crate::spill::SpillDrain>,
58 output_spilled: bool,
59}
60
61impl Window<'static> {
62 const DEFAULT_WORK_MEM_BYTES: usize = 64 * 1024 * 1024;
63
64 pub fn new(
65 child: Box<dyn PhysicalOperator>,
66 spec: WindowSpec,
67 functions: Vec<(String, WindowKind)>,
68 params: Vec<SQLParam>,
69 ) -> Self {
70 Self::new_with_work_mem(child, spec, functions, params, Self::DEFAULT_WORK_MEM_BYTES)
71 }
72
73 pub fn new_with_work_mem(
74 child: Box<dyn PhysicalOperator>,
75 spec: WindowSpec,
76 functions: Vec<(String, WindowKind)>,
77 params: Vec<SQLParam>,
78 work_mem_bytes: usize,
79 ) -> Self {
80 let names = functions
81 .iter()
82 .map(|(name, _)| name.clone())
83 .collect::<Vec<_>>();
84 let (schema, _) = RowSchema::append(child.row_schema(), &names).canonical_projection();
85 Self {
86 child,
87 spec,
88 functions,
89 params,
90 schema,
91 executor: None,
92 work_mem_bytes,
93 output: None,
94 output_spilled: false,
95 }
96 }
97}
98
99impl<'a> Window<'a> {
100 pub fn with_executor(
103 child: Box<dyn PhysicalOperator + 'a>,
104 output_schema: Vec<String>,
105 executor: Box<dyn WindowExecutor + 'a>,
106 ) -> Self {
107 let types = vec![None; output_schema.len()];
108 Self::with_typed_executor(child, output_schema, types, executor)
109 }
110
111 pub fn with_typed_executor(
112 child: Box<dyn PhysicalOperator + 'a>,
113 output_schema: Vec<String>,
114 output_types: Vec<Option<uqa_sql::ast::ColumnType>>,
115 executor: Box<dyn WindowExecutor + 'a>,
116 ) -> Self {
117 Self::with_row_schema_executor(
118 child,
119 RowSchema::with_types(output_schema, output_types),
120 executor,
121 )
122 }
123
124 pub fn with_row_schema_executor(
125 child: Box<dyn PhysicalOperator + 'a>,
126 schema: RowSchema,
127 executor: Box<dyn WindowExecutor + 'a>,
128 ) -> Self {
129 Self {
130 child,
131 spec: WindowSpec {
132 partition_by: Vec::new(),
133 order_by: Vec::new(),
134 },
135 functions: Vec::new(),
136 params: Vec::new(),
137 schema,
138 executor: Some(executor),
139 work_mem_bytes: 0,
140 output: None,
141 output_spilled: false,
142 }
143 }
144
145 pub fn output_has_spilled(&self) -> bool {
148 self.output_spilled
149 }
150}
151
152fn builtin_window_order_key(
153 row: &dyn RowLookup,
154 spec: &WindowSpec,
155 params: &[SQLParam],
156) -> ExecResult<Vec<Value>> {
157 let context = ScalarEvalContext::from_row_lookup(row, params);
158 spec.order_by
159 .iter()
160 .map(|key| Ok(eval_scalar(&key.expr, &context)?))
161 .collect()
162}
163
164fn builtin_window_partition_value(
165 kind: &WindowKind,
166 partition: &mut crate::spill::IndexedSpill,
167 params: &[SQLParam],
168) -> ExecResult<Option<Value>> {
169 let mut count = 0_i64;
170 let mut sum = 0.0_f64;
171 let mut min = None;
172 let mut max = None;
173 let expression = match kind {
174 WindowKind::AggSum(expression)
175 | WindowKind::AggAvg(expression)
176 | WindowKind::AggMin(expression)
177 | WindowKind::AggMax(expression) => Some(expression),
178 WindowKind::AggCount(expression) => expression.as_ref(),
179 _ => return Ok(None),
180 };
181 let partition_schema = partition.row_schema().clone();
182 for index in 0..partition.len() {
183 let row = partition.get(index)?;
184 let view = partition_schema.view(&row);
185 let value = match expression {
186 Some(expression) => eval_scalar(
187 expression,
188 &ScalarEvalContext::from_row_lookup(&view, params),
189 )?,
190 None => Value::Int(1),
191 };
192 if matches!(value, Value::Null) {
193 continue;
194 }
195 count = count
196 .checked_add(1)
197 .ok_or_else(|| ExecError::Other("window aggregate row count overflow".into()))?;
198 match kind {
199 WindowKind::AggSum(_) | WindowKind::AggAvg(_) => {
200 let number = value_to_f64(&value).ok_or_else(|| {
201 ExecError::Other(format!("non-numeric window aggregate input: {value:?}"))
202 })?;
203 sum += number;
204 }
205 WindowKind::AggMin(_) => {
206 min = Some(match min.take() {
207 Some(previous) if compare_values(&previous, &value).is_le() => previous,
208 _ => value,
209 });
210 }
211 WindowKind::AggMax(_) => {
212 max = Some(match max.take() {
213 Some(previous) if compare_values(&previous, &value).is_ge() => previous,
214 _ => value,
215 });
216 }
217 WindowKind::AggCount(_) => {}
218 _ => {
219 return Err(ExecError::Other(
220 "non-aggregate window kind reached aggregate evaluation".into(),
221 ))
222 }
223 }
224 }
225 Ok(Some(match kind {
226 WindowKind::AggSum(_) => {
227 if count == 0 {
228 Value::Null
229 } else {
230 Value::Float(sum)
231 }
232 }
233 WindowKind::AggCount(_) => Value::Int(count),
234 WindowKind::AggAvg(_) => {
235 if count == 0 {
236 Value::Null
237 } else {
238 Value::Float(sum / count as f64)
239 }
240 }
241 WindowKind::AggMin(_) => min.unwrap_or(Value::Null),
242 WindowKind::AggMax(_) => max.unwrap_or(Value::Null),
243 _ => {
244 return Err(ExecError::Other(
245 "non-aggregate window kind reached aggregate result construction".into(),
246 ))
247 }
248 }))
249}
250
251fn builtin_ntile(index: u64, rows: u64, buckets: i64) -> ExecResult<Value> {
252 let buckets = u64::try_from(buckets.max(1))
253 .map_err(|_| ExecError::Other("NTILE bucket count is out of range".into()))?;
254 let base = rows / buckets;
255 let extra = rows % buckets;
256 let larger_rows = if extra == 0 {
257 0
258 } else {
259 base.checked_add(1)
260 .and_then(|value| value.checked_mul(extra))
261 .ok_or_else(|| ExecError::Other("NTILE partition size overflow".into()))?
262 };
263 let bucket = if index < larger_rows {
264 index
265 .checked_div(
266 base.checked_add(1)
267 .ok_or_else(|| ExecError::Other("NTILE bucket width overflow".into()))?,
268 )
269 .and_then(|value| value.checked_add(1))
270 .ok_or_else(|| ExecError::Other("NTILE bucket number overflow".into()))?
271 } else if base == 0 {
272 extra.max(1)
273 } else {
274 extra
275 .checked_add(
276 (index - larger_rows)
277 .checked_div(base)
278 .ok_or_else(|| ExecError::Other("invalid NTILE bucket width".into()))?,
279 )
280 .and_then(|value| value.checked_add(1))
281 .ok_or_else(|| ExecError::Other("NTILE bucket number overflow".into()))?
282 };
283 Ok(Value::Int(i64::try_from(bucket).map_err(|_| {
284 ExecError::Other("NTILE bucket number exceeds SQL integer range".into())
285 })?))
286}
287
288#[expect(
289 clippy::too_many_lines,
290 reason = "window execution preserves frame and peer ordering in one pass"
291)]
292fn emit_builtin_window_partition(
293 partition: &mut crate::spill::IndexedSpill,
294 spec: &WindowSpec,
295 functions: &[(String, WindowKind)],
296 params: &[SQLParam],
297 schema: &RowSchema,
298 output: &mut crate::spill::SpillBuffer,
299) -> ExecResult<()> {
300 let aliases = functions
301 .iter()
302 .map(|(alias, _)| alias.clone())
303 .collect::<Vec<_>>();
304 let partition_schema = partition.row_schema().clone();
305 let appended_schema = RowSchema::append(&partition_schema, &aliases);
306 let (physical_output_schema, output_slots) = appended_schema.canonical_projection();
307 if &physical_output_schema != schema {
308 return Err(ExecError::Other(format!(
309 "window output schema mismatch: expected {:?}, got {:?}",
310 schema.columns(),
311 physical_output_schema.columns()
312 )));
313 }
314 let aggregate_values = functions
315 .iter()
316 .map(|(_, kind)| builtin_window_partition_value(kind, partition, params))
317 .collect::<ExecResult<Vec<_>>>()?;
318 let mut previous_order_key = None;
319 let mut rank = 0_i64;
320 let mut dense_rank = 0_i64;
321 let mut pending = Vec::with_capacity(crate::batch::DEFAULT_BATCH_SIZE);
322 for index in 0..partition.len() {
323 let row = partition.get(index)?;
324 let row_view = partition_schema.view(&row);
325 let order_key = builtin_window_order_key(&row_view, spec, params)?;
326 if previous_order_key.as_ref() != Some(&order_key) {
327 rank = i64::try_from(
328 index
329 .checked_add(1)
330 .ok_or_else(|| ExecError::Other("window rank overflow".into()))?,
331 )
332 .map_err(|_| ExecError::Other("window rank exceeds SQL integer range".into()))?;
333 dense_rank = dense_rank
334 .checked_add(1)
335 .ok_or_else(|| ExecError::Other("window dense rank overflow".into()))?;
336 }
337 let mut window_values = Vec::with_capacity(functions.len());
338 for ((_, kind), aggregate_value) in functions.iter().zip(&aggregate_values) {
339 let value = match kind {
340 WindowKind::RowNumber => {
341 Value::Int(
342 i64::try_from(index.checked_add(1).ok_or_else(|| {
343 ExecError::Other("window row number overflow".into())
344 })?)
345 .map_err(|_| {
346 ExecError::Other("window row number exceeds SQL integer range".into())
347 })?,
348 )
349 }
350 WindowKind::Rank => Value::Int(rank),
351 WindowKind::DenseRank => Value::Int(dense_rank),
352 WindowKind::Lag(expression, offset) | WindowKind::Lead(expression, offset) => {
353 let direction = if matches!(kind, WindowKind::Lag(..)) {
354 -1_i128
355 } else {
356 1_i128
357 };
358 let target = i128::from(index) + direction * i128::from(*offset);
359 if target < 0 || target >= i128::from(partition.len()) {
360 Value::Null
361 } else {
362 let target_row = partition.get(u64::try_from(target).map_err(|_| {
363 ExecError::Other("window offset target is out of range".into())
364 })?)?;
365 let target_view = partition_schema.view(&target_row);
366 eval_scalar(
367 expression,
368 &ScalarEvalContext::from_row_lookup(&target_view, params),
369 )?
370 }
371 }
372 WindowKind::Ntile(buckets) => builtin_ntile(index, partition.len(), *buckets)?,
373 WindowKind::AggSum(_)
374 | WindowKind::AggCount(_)
375 | WindowKind::AggAvg(_)
376 | WindowKind::AggMin(_)
377 | WindowKind::AggMax(_) => aggregate_value.clone().ok_or_else(|| {
378 ExecError::Other("aggregate window value was not precomputed".into())
379 })?,
380 };
381 window_values.push(value);
382 }
383 previous_order_key = Some(order_key);
384 pending.push(
385 row.append_values(window_values)
386 .project_slots(&output_slots)
387 .without_lock_origins(),
388 );
389 if pending.len() == crate::batch::DEFAULT_BATCH_SIZE {
390 output.push(Batch::from_physical_rows(
391 schema.clone(),
392 std::mem::take(&mut pending),
393 ))?;
394 pending = Vec::with_capacity(crate::batch::DEFAULT_BATCH_SIZE);
395 }
396 }
397 if !pending.is_empty() {
398 output.push(Batch::from_physical_rows(schema.clone(), pending))?;
399 }
400 Ok(())
401}
402
403impl PhysicalOperator for Window<'_> {
404 fn row_schema(&self) -> &RowSchema {
405 &self.schema
406 }
407
408 fn open(&mut self) -> ExecResult<()> {
409 self.child.open()?;
410 self.output_spilled = false;
411 if let Some(executor) = self.executor.as_mut() {
412 while let Some(batch) = self.child.next()? {
413 executor.consume(batch)?;
414 }
415 let mut output = executor.finish()?;
416 self.output_spilled = output.has_spilled();
417 self.output = Some(output.drain()?);
418 return Ok(());
419 }
420
421 let phase_budget = (self.work_mem_bytes / 3).max(1);
422 let mut input = crate::spill::SpillBuffer::new(phase_budget);
423 while let Some(batch) = self.child.next()? {
424 input.push(batch)?;
425 }
426 let scan: Box<dyn PhysicalOperator> = Box::new(crate::spill_scan::SpillScan::new(
427 self.child.schema().to_vec(),
428 input,
429 ));
430 let mut keys = self
431 .spec
432 .partition_by
433 .iter()
434 .cloned()
435 .map(|expr| SortKey {
436 expr,
437 descending: false,
438 nulls_first: None,
439 })
440 .collect::<Vec<_>>();
441 keys.extend(self.spec.order_by.iter().cloned());
442 let evaluator = DefaultExpressionEvaluator::shared(self.params.clone());
443 let mut sorted =
444 crate::external_sort::ExternalSort::new(scan, keys, evaluator, None, phase_budget);
445 sorted.open()?;
446
447 let partition_schema = sorted.row_schema().clone();
448 let mut current_partition_key: Option<Vec<Value>> = None;
449 let mut partition = crate::spill::IndexedSpill::new(partition_schema.clone())?;
450 let mut output = crate::spill::SpillBuffer::new(phase_budget);
451 let execution = (|| -> ExecResult<()> {
452 while let Some(batch) = sorted.next()? {
453 for row in batch.rows {
454 let view = batch.schema.view(&row);
455 let context = ScalarEvalContext::from_row_lookup(&view, &self.params);
456 let key = self
457 .spec
458 .partition_by
459 .iter()
460 .map(|expression| eval_scalar(expression, &context))
461 .collect::<Result<Vec<_>, _>>()?;
462 if current_partition_key
463 .as_ref()
464 .is_some_and(|current| current != &key)
465 {
466 emit_builtin_window_partition(
467 &mut partition,
468 &self.spec,
469 &self.functions,
470 &self.params,
471 &self.schema,
472 &mut output,
473 )?;
474 partition = crate::spill::IndexedSpill::new(partition_schema.clone())?;
475 }
476 current_partition_key = Some(key);
477 partition.push(&row)?;
478 }
479 }
480 if !partition.is_empty() {
481 emit_builtin_window_partition(
482 &mut partition,
483 &self.spec,
484 &self.functions,
485 &self.params,
486 &self.schema,
487 &mut output,
488 )?;
489 }
490 Ok(())
491 })();
492 let close = sorted.close();
493 crate::physical::with_cleanup(execution, close, "close window sort after failure")?;
494 self.output_spilled = output.has_spilled();
495 self.output = Some(output.drain()?);
496 Ok(())
497 }
498
499 fn next(&mut self) -> ExecResult<Option<Batch>> {
500 let Some(output) = self.output.as_mut() else {
501 return Ok(None);
502 };
503 output.next().transpose()
504 }
505
506 fn close(&mut self) -> ExecResult<()> {
507 self.output = None;
508 self.child.close()
509 }
510}