Skip to main content

reifydb_sub_flow/operator/window/
aggregation.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::sync::LazyLock;
5
6use postcard::to_stdvec;
7use reifydb_codec::{
8	encoded::shape::{RowShape, RowShapeField},
9	key::{encoded::EncodedKey, serializer::KeySerializer},
10};
11use reifydb_core::{
12	interface::catalog::flow::FlowNodeId,
13	row::Row,
14	value::column::{ColumnWithName, columns::Columns},
15};
16use reifydb_engine::{
17	expression::{
18		compile::{CompiledExpr, compile_expression},
19		context::{CompileContext, EvalContext},
20	},
21	flow::aggregate::{AggregateContext, SlotArg, SlotKind, rewrite_aggregates, synthetic_aggregate_column_name},
22	vm::stack::SymbolTable,
23};
24use reifydb_routine::routine::registry::Routines;
25use reifydb_rql::expression::{Expression, name::display_label};
26use reifydb_runtime::context::RuntimeContext;
27use reifydb_value::{
28	Result,
29	error::Error,
30	params::Params,
31	util::hash::{Hash128, xxh3_128},
32	value::{Value, identity::IdentityId, row_number::RowNumber, value_type::ValueType},
33};
34
35use crate::{error::FlowStateError, operator::OperatorCell};
36
37static EMPTY_PARAMS: Params = Params::None;
38
39static EMPTY_SYMBOL_TABLE: LazyLock<SymbolTable> = LazyLock::new(SymbolTable::new);
40
41#[derive(Clone, Debug)]
42pub enum SlotInput {
43	Star,
44	Column(String),
45	Expr(usize),
46}
47
48#[inline]
49fn build_aggregation_shape(names: &[String], types: &[ValueType]) -> RowShape {
50	let fields: Vec<RowShapeField> = names
51		.iter()
52		.zip(types.iter())
53		.map(|(name, ty)| RowShapeField::unconstrained(name.clone(), ty.clone()))
54		.collect();
55	RowShape::new(fields)
56}
57
58pub struct Aggregation {
59	pub node: FlowNodeId,
60	pub parent: OperatorCell,
61	pub compiled_group_by: Vec<CompiledExpr>,
62	pub group_names: Vec<String>,
63	pub aggregate_output_names: Vec<String>,
64
65	pub slot_kinds: Option<Vec<SlotKind>>,
66
67	pub slot_inputs: Vec<SlotInput>,
68
69	pub compiled_slot_args: Vec<CompiledExpr>,
70
71	pub compiled_outputs: Vec<CompiledExpr>,
72
73	pub routines: Routines,
74	pub runtime_context: RuntimeContext,
75}
76
77impl Aggregation {
78	pub fn new(
79		node: FlowNodeId,
80		parent: OperatorCell,
81		group_by: Vec<Expression>,
82		aggregations: Vec<Expression>,
83		routines: Routines,
84		runtime_context: RuntimeContext,
85		context: AggregateContext,
86	) -> Self {
87		let symbols = SymbolTable::new();
88		let compile_ctx = CompileContext {
89			symbols: &symbols,
90		};
91
92		let compiled_group_by: Vec<CompiledExpr> = group_by
93			.iter()
94			.map(|e| compile_expression(&compile_ctx, e).expect("Failed to compile group_by expression"))
95			.collect();
96
97		let aggregate_output_names: Vec<String> =
98			aggregations.iter().map(|e| display_label(e).text().to_string()).collect();
99
100		let mut slots: Vec<(SlotKind, SlotArg)> = Vec::new();
101		let mut rewritten_outputs: Vec<Expression> = Vec::new();
102		let mut all_representable = !aggregations.is_empty();
103		for aggregate in &aggregations {
104			let mut expr = aggregate.clone();
105			if rewrite_aggregates(&routines, &mut expr, &mut slots, context) {
106				rewritten_outputs.push(expr);
107			} else {
108				all_representable = false;
109				break;
110			}
111		}
112		let (slot_kinds, slot_inputs, compiled_slot_args, compiled_outputs) = if all_representable {
113			let mut kinds = Vec::with_capacity(slots.len());
114			let mut inputs = Vec::with_capacity(slots.len());
115			let mut compiled_args = Vec::new();
116			for (kind, arg) in slots {
117				kinds.push(kind);
118				inputs.push(match arg {
119					SlotArg::Star => SlotInput::Star,
120					SlotArg::Column(name) => SlotInput::Column(name),
121					SlotArg::Expr(expr) => {
122						let idx = compiled_args.len();
123						compiled_args.push(compile_expression(&compile_ctx, &expr)
124							.expect("Failed to compile aggregation argument expression"));
125						SlotInput::Expr(idx)
126					}
127				});
128			}
129			let outputs: Vec<CompiledExpr> = rewritten_outputs
130				.iter()
131				.map(|e| {
132					compile_expression(&compile_ctx, e)
133						.expect("Failed to compile rewritten output expression")
134				})
135				.collect();
136			(Some(kinds), inputs, compiled_args, outputs)
137		} else {
138			(None, Vec::new(), Vec::new(), Vec::new())
139		};
140		let group_names: Vec<String> = group_by.iter().map(|e| display_label(e).text().to_string()).collect();
141
142		Self {
143			node,
144			parent,
145			compiled_group_by,
146			group_names,
147			aggregate_output_names,
148			slot_kinds,
149			slot_inputs,
150			compiled_slot_args,
151			compiled_outputs,
152			routines,
153			runtime_context,
154		}
155	}
156
157	pub fn create_window_key(&self, group_hash: Hash128, window_id: u64) -> EncodedKey {
158		let mut serializer = KeySerializer::with_capacity(32);
159		serializer.extend_bytes(b"win:");
160		serializer.extend_u128(group_hash);
161		serializer.extend_u64(window_id);
162		serializer.finish()
163	}
164
165	pub(super) fn create_engine_meta_key(&self, group_hash: Hash128, window_start: u64) -> EncodedKey {
166		let mut serializer = KeySerializer::with_capacity(32);
167		serializer.extend_bytes(b"ewm:");
168		serializer.extend_u128(group_hash);
169		serializer.extend_u64(window_start);
170		serializer.finish()
171	}
172
173	pub fn compute_groups(&self, columns: &Columns) -> Result<Vec<(Hash128, Vec<Value>)>> {
174		let row_count = columns.row_count();
175		if row_count == 0 {
176			return Ok(Vec::new());
177		}
178		if self.compiled_group_by.is_empty() {
179			return Ok(vec![(Hash128::from(0u128), Vec::new()); row_count]);
180		}
181
182		let session = self.eval_session();
183		let exec_ctx = session.with_eval(columns.clone(), row_count);
184		let mut group_columns: Vec<ColumnWithName> = Vec::new();
185		for compiled_expr in &self.compiled_group_by {
186			group_columns.push(compiled_expr.execute(&exec_ctx)?);
187		}
188
189		let mut out = Vec::with_capacity(row_count);
190		let mut buf = Vec::with_capacity(128);
191		for row_idx in 0..row_count {
192			buf.clear();
193			let mut values = Vec::with_capacity(group_columns.len());
194			for col in &group_columns {
195				let value = col.data().get_value(row_idx);
196				let bytes = to_stdvec(&value).map_err(|e| {
197					Error::from(FlowStateError::Encode {
198						state: "group-by value",
199						cause: e.to_string(),
200					})
201				})?;
202				buf.extend_from_slice(&bytes);
203				values.push(value);
204			}
205			out.push((xxh3_128(&buf), values));
206		}
207		Ok(out)
208	}
209
210	pub fn evaluate_slot_inputs(&self, columns: &Columns) -> Result<Vec<ColumnWithName>> {
211		if self.compiled_slot_args.is_empty() {
212			return Ok(Vec::new());
213		}
214		let row_count = columns.row_count();
215		let session = self.eval_session();
216		let exec_ctx = session.with_eval(columns.clone(), row_count);
217		let mut out = Vec::with_capacity(self.compiled_slot_args.len());
218		for compiled in &self.compiled_slot_args {
219			out.push(compiled.execute(&exec_ctx)?);
220		}
221		Ok(out)
222	}
223
224	pub fn build_contribution(
225		&self,
226		columns: &Columns,
227		slot_cols: &[ColumnWithName],
228		row_idx: usize,
229	) -> Vec<Option<Value>> {
230		self.slot_inputs
231			.iter()
232			.map(|input| match input {
233				SlotInput::Star => None,
234				SlotInput::Column(name) => columns.column(name).map(|c| c.data().get_value(row_idx)),
235				SlotInput::Expr(idx) => Some(slot_cols[*idx].data().get_value(row_idx)),
236			})
237			.collect()
238	}
239
240	pub fn compute_outputs(&self, slot_values: &[Value]) -> Result<Vec<Value>> {
241		if self.compiled_outputs.is_empty() {
242			return Ok(slot_values.to_vec());
243		}
244		let names: Vec<String> = (0..slot_values.len()).map(synthetic_aggregate_column_name).collect();
245		let types: Vec<_> = slot_values.iter().map(Value::get_type).collect();
246		let layout = build_aggregation_shape(&names, &types);
247		let mut encoded = layout.allocate();
248		layout.set_values(&mut encoded, slot_values);
249		let row = Row {
250			number: RowNumber(0),
251			encoded,
252			shape: layout,
253		};
254		let columns = Columns::from_row(&row);
255		let session = self.eval_session();
256		let exec_ctx = session.with_eval(columns, 1);
257		let mut out = Vec::with_capacity(self.compiled_outputs.len());
258		for compiled in &self.compiled_outputs {
259			out.push(compiled.execute(&exec_ctx)?.data().get_value(0));
260		}
261		Ok(out)
262	}
263
264	pub fn build_engine_row(
265		&self,
266		group_values: &[Value],
267		slot_values: &[Value],
268		row_number: RowNumber,
269		ts_nanos: u64,
270	) -> Result<Row> {
271		let aggregate_values = self.compute_outputs(slot_values)?;
272		let mut values = Vec::with_capacity(group_values.len() + aggregate_values.len());
273		let mut names = Vec::with_capacity(group_values.len() + aggregate_values.len());
274		let mut types = Vec::with_capacity(group_values.len() + aggregate_values.len());
275		for (value, name) in group_values.iter().zip(self.group_names.iter()) {
276			types.push(value.get_type());
277			values.push(value.clone());
278			names.push(name.clone());
279		}
280		for (value, name) in aggregate_values.iter().zip(self.aggregate_output_names.iter()) {
281			types.push(value.get_type());
282			values.push(value.clone());
283			names.push(name.clone());
284		}
285		let layout = build_aggregation_shape(&names, &types);
286		let mut encoded = layout.allocate();
287		layout.set_values(&mut encoded, &values);
288		encoded.set_timestamps(ts_nanos, ts_nanos);
289		Ok(Row {
290			number: row_number,
291			encoded,
292			shape: layout,
293		})
294	}
295
296	pub fn current_timestamp(&self) -> u64 {
297		self.runtime_context.clock.now_millis()
298	}
299
300	pub(super) fn eval_session(&self) -> EvalContext<'_> {
301		EvalContext {
302			params: &EMPTY_PARAMS,
303			symbols: &EMPTY_SYMBOL_TABLE,
304			routines: &self.routines,
305			runtime_context: &self.runtime_context,
306			arena: None,
307			identity: IdentityId::root(),
308			is_aggregate_context: false,
309			columns: Columns::empty(),
310			row_count: 1,
311			target: None,
312			take: None,
313		}
314	}
315}