Skip to main content

uqa_execution/relational/
aggregate.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Blocking hash/sort aggregation and aggregate folds.
8
9use super::{
10    compare_values, eval_scalar, Batch, DefaultExpressionEvaluator, ExecError, ExecResult,
11    PhysicalOperator, RowSchema, SQLParam, ScalarEvalContext, ScalarExpr, SortKey, Value,
12};
13use crate::ProjectedRow;
14
15mod adaptive;
16mod fold;
17mod partial;
18mod sort_fallback;
19
20pub(super) use fold::value_to_f64;
21#[cfg(test)]
22pub(super) use fold::{finalise_fold, AggFold};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum AggregateKind {
26    Count,
27    CountStar,
28    Sum,
29    Avg,
30    Min,
31    Max,
32}
33
34#[derive(Debug, Clone)]
35pub struct AggregateSpec {
36    pub kind: AggregateKind,
37    /// Argument to the aggregate. Ignored for `CountStar`.
38    pub arg: Option<ScalarExpr>,
39    /// Output column alias.
40    pub alias: String,
41    /// `COUNT(DISTINCT x)` / `SUM(DISTINCT x)` / etc.
42    pub distinct: bool,
43}
44
45/// Blocking group-by + aggregate. Pulls every row from the child
46/// during `open`, hashes each row by its group key, and folds the
47/// aggregates over each group's row set. Groups are emitted in the
48/// order they were first observed.
49pub trait AggregateExecutor: Send {
50    /// Consume one child batch. Implementations that need a blocking input must
51    /// enforce their own byte budget here; the physical operator never creates
52    /// an unbounded intermediate row vector.
53    fn consume(&mut self, batch: Batch) -> ExecResult<()>;
54
55    /// Whether this executor can fold a borrowed, positional row without a
56    /// materialized `ResultRow`. A source checks this before advancing.
57    fn supports_projected_rows(&self) -> bool {
58        false
59    }
60
61    /// Whether projected-row consumption is guaranteed not to call back into the engine while a storage backend lends its row values. Sources use this stricter capability before invoking an executor under a backend read borrow.
62    fn supports_storage_borrowed_rows(&self) -> bool {
63        false
64    }
65
66    /// Fold one projected row. Implementations advertising support must
67    /// preserve the same expression and aggregate semantics as [`Self::consume`].
68    fn consume_projected_row(&mut self, _row: &ProjectedRow<'_, '_>) -> ExecResult<()> {
69        Err(ExecError::Other(
70            "aggregate executor does not accept projected rows".into(),
71        ))
72    }
73
74    /// Finalize all groups into a byte-bounded, disk-backed output stream.
75    /// The row-oriented SQL API may materialize that stream at its public API
76    /// boundary, but physical operators must not create an unbounded result
77    /// vector first.
78    fn finish(&mut self) -> ExecResult<crate::spill::SpillBuffer>;
79}
80
81pub struct HashAggregate<'a> {
82    child: Box<dyn PhysicalOperator + 'a>,
83    group_keys: Vec<(String, ScalarExpr)>,
84    aggregates: Vec<AggregateSpec>,
85    params: Vec<SQLParam>,
86    schema: RowSchema,
87    executor: Option<Box<dyn AggregateExecutor + 'a>>,
88    work_mem_bytes: usize,
89    output: Option<crate::spill::SpillDrain>,
90    output_spilled: bool,
91}
92
93impl HashAggregate<'static> {
94    const DEFAULT_WORK_MEM_BYTES: usize = 64 * 1024 * 1024;
95
96    pub fn new(
97        child: Box<dyn PhysicalOperator>,
98        group_keys: Vec<(String, ScalarExpr)>,
99        aggregates: Vec<AggregateSpec>,
100        params: Vec<SQLParam>,
101    ) -> Self {
102        Self::new_with_work_mem(
103            child,
104            group_keys,
105            aggregates,
106            params,
107            Self::DEFAULT_WORK_MEM_BYTES,
108        )
109    }
110
111    pub fn new_with_work_mem(
112        child: Box<dyn PhysicalOperator>,
113        group_keys: Vec<(String, ScalarExpr)>,
114        aggregates: Vec<AggregateSpec>,
115        params: Vec<SQLParam>,
116        work_mem_bytes: usize,
117    ) -> Self {
118        let mut cols: Vec<String> = group_keys.iter().map(|(n, _)| n.clone()).collect();
119        for a in &aggregates {
120            cols.push(a.alias.clone());
121        }
122        let schema = RowSchema::new(cols);
123        Self {
124            child,
125            group_keys,
126            aggregates,
127            params,
128            schema,
129            executor: None,
130            work_mem_bytes,
131            output: None,
132            output_spilled: false,
133        }
134    }
135}
136
137impl<'a> HashAggregate<'a> {
138    /// Construct a physical aggregate backed by the engine's full aggregate
139    /// registry. Input is delivered incrementally through
140    /// [`AggregateExecutor::consume`].
141    pub fn with_executor(
142        child: Box<dyn PhysicalOperator + 'a>,
143        output_schema: Vec<String>,
144        executor: Box<dyn AggregateExecutor + 'a>,
145    ) -> Self {
146        let types = vec![None; output_schema.len()];
147        Self::with_typed_executor(child, output_schema, types, executor)
148    }
149
150    pub fn with_typed_executor(
151        child: Box<dyn PhysicalOperator + 'a>,
152        output_schema: Vec<String>,
153        output_types: Vec<Option<uqa_sql::ast::ColumnType>>,
154        executor: Box<dyn AggregateExecutor + 'a>,
155    ) -> Self {
156        Self {
157            child,
158            group_keys: Vec::new(),
159            aggregates: Vec::new(),
160            params: Vec::new(),
161            schema: RowSchema::with_types(output_schema, output_types),
162            executor: Some(executor),
163            work_mem_bytes: 0,
164            output: None,
165            output_spilled: false,
166        }
167    }
168
169    /// Whether final aggregate rows exceeded their output budget and were
170    /// written to disk during the current/most recent invocation.
171    pub fn output_has_spilled(&self) -> bool {
172        self.output_spilled
173    }
174}
175
176impl PhysicalOperator for HashAggregate<'_> {
177    fn row_schema(&self) -> &RowSchema {
178        &self.schema
179    }
180
181    fn open(&mut self) -> ExecResult<()> {
182        self.child.open()?;
183        self.output_spilled = false;
184        if let Some(executor) = self.executor.as_mut() {
185            let consumed_directly = executor.supports_projected_rows()
186                && self.child.consume_into_aggregate(executor.as_mut())?;
187            if !consumed_directly {
188                while let Some(batch) = self.child.next()? {
189                    executor.consume(batch)?;
190                }
191            }
192            let mut output = executor.finish()?;
193            self.output_spilled = output.has_spilled();
194            self.output = Some(output.drain()?);
195            return Ok(());
196        }
197        let mut output = if adaptive::supported(&self.group_keys, &self.aggregates) {
198            let mut aggregate = adaptive::AdaptiveBuiltinAggregate::new(
199                &self.group_keys,
200                &self.aggregates,
201                &self.params,
202                self.work_mem_bytes,
203            );
204            while let Some(batch) = self.child.next()? {
205                aggregate.consume(batch)?;
206            }
207            aggregate.finish(self.schema.clone())?
208        } else {
209            sort_fallback::execute(
210                self.child.as_mut(),
211                &self.group_keys,
212                &self.aggregates,
213                &self.params,
214                self.schema.clone(),
215                self.work_mem_bytes,
216            )?
217        };
218        self.output_spilled = output.has_spilled();
219        self.output = Some(output.drain()?);
220        Ok(())
221    }
222
223    fn next(&mut self) -> ExecResult<Option<Batch>> {
224        let Some(output) = self.output.as_mut() else {
225            return Ok(None);
226        };
227        output.next().transpose()
228    }
229
230    fn close(&mut self) -> ExecResult<()> {
231        self.output = None;
232        self.child.close()
233    }
234}
235
236// -------------------------------------------------------------------------
237// Window
238// -------------------------------------------------------------------------