Skip to main content

reifydb_core/window/engine/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4//! Schema-agnostic windowing state-machine engines.
5//!
6//! Each engine owns the per-(group,window) accumulator state, high-water late
7//! rejection, eviction, and diff routing (`Insert -> add`,
8//! `Update -> remove(pre) + add(post)`, `Remove -> remove(pre)`). The caller
9//! (the "face") owns extraction (`row -> (group, coord, contribution)`) and
10//! output construction; it hands the engine pre-bucketed events and receives
11//! [`WindowResult`]s to translate into diffs.
12
13pub mod multi_rolling;
14pub mod rolling;
15pub mod rolling_incremental;
16pub mod tumbling;
17pub mod tumbling_carry;
18
19use std::ops::Bound;
20
21use reifydb_value::value::row_number::RowNumber;
22use serde::{Deserialize, Serialize};
23
24use crate::{
25	encoded::key::{EncodedKey, EncodedKeyRange, IntoEncodedKey},
26	key::flow_node_internal_state::FlowNodeInternalStateKey,
27	util::encoding::keycode::encode_u64,
28	window::span::WindowSpan,
29};
30
31/// One contribution routed to a window accumulator.
32pub enum AccumulatorEvent<C> {
33	Add(C),
34	Remove(C),
35}
36
37/// How an engine treats an event whose window coordinate is below the per-group
38/// high-water mark (an event for an already-closed window).
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
40pub enum LatePolicy {
41	/// Event-time semantics: drop late events (deterministic under replay).
42	#[default]
43	Drop,
44	/// Accept late events into their (re-opened) window. High-water still
45	/// tracks the max coordinate seen but never rejects.
46	Process,
47}
48
49/// How a finalized window value should be emitted downstream.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum EmitKind {
52	Insert,
53	Update,
54	Remove,
55}
56
57/// A finalized window the engine produced; the face turns it into a diff.
58pub struct WindowResult<G, Coord, Output> {
59	pub row_number: RowNumber,
60	pub group: G,
61	pub span: WindowSpan<Coord>,
62	pub value: Output,
63	/// The finalized value before this batch's events, when the window was
64	/// non-empty (used by faces that emit a real pre on Update/Remove). `None`
65	/// for a brand-new window. Faces that don't need it (the sdk drivers)
66	/// ignore it.
67	pub prior: Option<Output>,
68	pub kind: EmitKind,
69}
70
71/// Per-group metadata: the highest window start seen, used to drop late events
72/// for already-closed windows.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(bound(serialize = "K: Serialize", deserialize = "K: serde::de::DeserializeOwned"))]
75pub struct GroupMeta<K> {
76	pub high_water: Option<K>,
77}
78
79impl<K> Default for GroupMeta<K> {
80	fn default() -> Self {
81		Self {
82			high_water: None,
83		}
84	}
85}
86
87/// State-cache key for a group's [`GroupMeta`], tagged so it lives in a
88/// distinct keyspace from the per-window accumulators.
89#[derive(Clone, Hash, PartialEq, Eq)]
90pub struct MetaKey(pub EncodedKey);
91
92impl IntoEncodedKey for &MetaKey {
93	fn into_encoded_key(self) -> EncodedKey {
94		let inner = self.0.as_ref();
95		let mut bytes = Vec::with_capacity(1 + inner.len());
96		bytes.push(FlowNodeInternalStateKey::WINDOW_META_TAG);
97		bytes.extend_from_slice(inner);
98		EncodedKey::new(bytes)
99	}
100}
101
102pub fn meta_key_for<G>(group: &G) -> MetaKey
103where
104	for<'a> &'a G: IntoEncodedKey,
105{
106	MetaKey(group.into_encoded_key())
107}
108
109pub fn expiry_key<G>(expiry: u64, group: &G, suffix: &[u8]) -> EncodedKey
110where
111	for<'a> &'a G: IntoEncodedKey,
112{
113	let group = group.into_encoded_key();
114	let group = group.as_ref();
115	let mut bytes = Vec::with_capacity(1 + 8 + group.len() + suffix.len());
116	bytes.push(FlowNodeInternalStateKey::WINDOW_EXPIRY_TAG);
117	bytes.extend_from_slice(&encode_u64(expiry));
118	bytes.extend_from_slice(group);
119	bytes.extend_from_slice(suffix);
120	EncodedKey::new(bytes)
121}
122
123pub fn expiry_due_range(threshold: u64) -> EncodedKeyRange {
124	let mut start = Vec::with_capacity(1 + 8);
125	start.push(FlowNodeInternalStateKey::WINDOW_EXPIRY_TAG);
126	start.extend_from_slice(&encode_u64(threshold));
127	let end = vec![FlowNodeInternalStateKey::WINDOW_EXPIRY_TAG + 1];
128	EncodedKeyRange::new(Bound::Included(EncodedKey::new(start)), Bound::Excluded(EncodedKey::new(end)))
129}
130
131#[cfg(test)]
132pub(crate) mod test_support {
133	use std::{collections::HashMap, ops::Bound};
134
135	use postcard::{from_bytes, to_allocvec};
136	use reifydb_value::{Result, value::row_number::RowNumber};
137	use serde::{Deserialize, Serialize, de::DeserializeOwned};
138
139	use crate::{
140		encoded::key::{EncodedKey, EncodedKeyRange},
141		key::flow_node_internal_state::FlowNodeInternalStateKey,
142		window::{accumulator::WindowAccumulator, store::WindowStore},
143	};
144
145	#[derive(Default)]
146	pub(crate) struct MockStore {
147		data: HashMap<Vec<u8>, Vec<u8>>,
148		internal: HashMap<Vec<u8>, Vec<u8>>,
149		rows: HashMap<Vec<u8>, u64>,
150		next_row: u64,
151	}
152
153	impl MockStore {
154		pub(crate) fn index_entry_count(&mut self) -> usize {
155			self.internal
156				.keys()
157				.filter(|k| k.first() == Some(&FlowNodeInternalStateKey::WINDOW_EXPIRY_TAG))
158				.count()
159		}
160	}
161
162	impl WindowStore for MockStore {
163		fn state_get<V: DeserializeOwned>(&mut self, key: &EncodedKey) -> Result<Option<V>> {
164			Ok(self.data.get(key.as_bytes()).map(|b| from_bytes(b).expect("decode")))
165		}
166		fn state_get_many_visit<V: DeserializeOwned>(
167			&mut self,
168			keys: &[EncodedKey],
169			visit: &mut dyn FnMut(EncodedKey, V) -> Result<()>,
170		) -> Result<()> {
171			for key in keys {
172				if let Some(b) = self.data.get(key.as_bytes()) {
173					visit(key.clone(), from_bytes(b).expect("decode"))?;
174				}
175			}
176			Ok(())
177		}
178		fn state_set<V: Serialize>(&mut self, key: &EncodedKey, value: &V) -> Result<()> {
179			self.data.insert(key.as_bytes().to_vec(), to_allocvec(value).expect("encode"));
180			Ok(())
181		}
182		fn state_remove(&mut self, key: &EncodedKey) -> Result<()> {
183			self.data.remove(key.as_bytes());
184			Ok(())
185		}
186		fn state_drop(&mut self, key: &EncodedKey) -> Result<()> {
187			self.data.remove(key.as_bytes());
188			Ok(())
189		}
190		fn internal_get<V: DeserializeOwned>(&mut self, key: &EncodedKey) -> Result<Option<V>> {
191			Ok(self.internal.get(key.as_bytes()).map(|b| from_bytes(b).expect("decode")))
192		}
193		fn internal_get_many_visit<V: DeserializeOwned>(
194			&mut self,
195			keys: &[EncodedKey],
196			visit: &mut dyn FnMut(EncodedKey, V) -> Result<()>,
197		) -> Result<()> {
198			for key in keys {
199				if let Some(b) = self.internal.get(key.as_bytes()) {
200					visit(key.clone(), from_bytes(b).expect("decode"))?;
201				}
202			}
203			Ok(())
204		}
205		fn internal_set<V: Serialize>(&mut self, key: &EncodedKey, value: &V) -> Result<()> {
206			self.internal.insert(key.as_bytes().to_vec(), to_allocvec(value).expect("encode"));
207			Ok(())
208		}
209		fn internal_remove(&mut self, key: &EncodedKey) -> Result<()> {
210			self.internal.remove(key.as_bytes());
211			Ok(())
212		}
213		fn internal_drop(&mut self, key: &EncodedKey) -> Result<()> {
214			self.internal.remove(key.as_bytes());
215			Ok(())
216		}
217		fn internal_range_visit<V: DeserializeOwned>(
218			&mut self,
219			range: EncodedKeyRange,
220			visit: &mut dyn FnMut(EncodedKey, V) -> Result<()>,
221		) -> Result<()> {
222			let after_start = |k: &[u8]| match &range.start {
223				Bound::Included(s) => k >= s.as_bytes(),
224				Bound::Excluded(s) => k > s.as_bytes(),
225				Bound::Unbounded => true,
226			};
227			let before_end = |k: &[u8]| match &range.end {
228				Bound::Included(e) => k <= e.as_bytes(),
229				Bound::Excluded(e) => k < e.as_bytes(),
230				Bound::Unbounded => true,
231			};
232			let mut matched: Vec<(Vec<u8>, Vec<u8>)> = self
233				.internal
234				.iter()
235				.filter(|(k, _)| after_start(k) && before_end(k))
236				.map(|(k, v)| (k.clone(), v.clone()))
237				.collect();
238			matched.sort_by(|a, b| a.0.cmp(&b.0));
239			for (k, b) in matched {
240				visit(EncodedKey::new(k), from_bytes(&b).expect("decode"))?;
241			}
242			Ok(())
243		}
244		fn get_or_create_row_number(&mut self, key: &EncodedKey) -> Result<(RowNumber, bool)> {
245			if let Some(rn) = self.rows.get(key.as_bytes()) {
246				return Ok((RowNumber(*rn), false));
247			}
248			self.next_row += 1;
249			self.rows.insert(key.as_bytes().to_vec(), self.next_row);
250			Ok((RowNumber(self.next_row), true))
251		}
252		fn get_or_create_row_numbers(&mut self, keys: &[EncodedKey]) -> Result<Vec<(RowNumber, bool)>> {
253			keys.iter().map(|k| self.get_or_create_row_number(k)).collect()
254		}
255		fn allocate_row_numbers(&mut self, count: u64) -> Result<RowNumber> {
256			let start = self.next_row + 1;
257			self.next_row += count;
258			Ok(RowNumber(start))
259		}
260		fn clock_now_nanos(&self) -> u64 {
261			0
262		}
263	}
264
265	#[derive(Clone, Debug, Default, Serialize, Deserialize)]
266	pub(crate) struct SumAccumulator {
267		pub sum: i64,
268		pub count: u64,
269	}
270
271	impl WindowAccumulator for SumAccumulator {
272		type Contribution = i64;
273		type Output = i64;
274
275		fn add(&mut self, contribution: &i64) {
276			self.sum += *contribution;
277			self.count += 1;
278		}
279		fn remove(&mut self, contribution: &i64) {
280			self.sum -= *contribution;
281			self.count = self.count.saturating_sub(1);
282		}
283		fn finalize(&self) -> Option<i64> {
284			if self.count == 0 {
285				None
286			} else {
287				Some(self.sum)
288			}
289		}
290		fn is_empty(&self) -> bool {
291			self.count == 0
292		}
293	}
294
295	#[derive(Clone, Debug, Default, Serialize, Deserialize)]
296	pub(crate) struct StampedSum {
297		pub sum: i64,
298		pub count: u64,
299		pub stamp: Option<u64>,
300	}
301
302	impl WindowAccumulator for StampedSum {
303		type Contribution = (i64, u64);
304		type Output = i64;
305
306		fn add(&mut self, contribution: &(i64, u64)) {
307			self.sum += contribution.0;
308			self.count += 1;
309			self.stamp = Some(self.stamp.map_or(contribution.1, |s| s.max(contribution.1)));
310		}
311		fn remove(&mut self, contribution: &(i64, u64)) {
312			self.sum -= contribution.0;
313			self.count = self.count.saturating_sub(1);
314		}
315		fn finalize(&self) -> Option<i64> {
316			if self.count == 0 {
317				None
318			} else {
319				Some(self.sum)
320			}
321		}
322		fn is_empty(&self) -> bool {
323			self.count == 0
324		}
325		fn stamp(&self) -> Option<u64> {
326			self.stamp
327		}
328	}
329}