Skip to main content

reifydb_core/window/engine/
rolling_incremental.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	collections::{BTreeMap, BTreeSet, HashMap},
6	fmt::Debug,
7	hash::Hash,
8	marker::PhantomData,
9};
10
11use reifydb_value::{Result, reifydb_assertions, value::row_number::RowNumber};
12use serde::{Serialize, de::DeserializeOwned};
13
14use crate::{
15	encoded::key::{EncodedKey, IntoEncodedKey},
16	window::{
17		accumulator::WindowAccumulator,
18		engine::{
19			AccumulatorEvent, EmitKind, GroupMeta, LatePolicy, MetaKey, meta_key_for,
20			rolling::{RollingBuckets, RollingBuffer, RollingResult},
21		},
22		span::Slot,
23		state::StateCache,
24		store::WindowStore,
25	},
26};
27
28type MetaLoaded<G, C> = HashMap<G, GroupMeta<C>>;
29type BufferRows<G> = HashMap<G, (RowNumber, bool)>;
30
31struct GroupSlot<C, Accumulator, Running> {
32	row_number: RowNumber,
33	is_new: bool,
34	buffer: RollingBuffer<C, Accumulator>,
35	running: Running,
36	was_empty_before: bool,
37	buffer_changed: bool,
38}
39
40pub struct RollingIncrementalEngine<G, C, Accumulator, Running> {
41	buffers: StateCache<RowNumber, RollingBuffer<C, Accumulator>>,
42	running: StateCache<RowNumber, Running>,
43	meta: StateCache<MetaKey, GroupMeta<C>>,
44	late_policy: LatePolicy,
45	_pd: PhantomData<G>,
46}
47
48impl<G, C, Accumulator, Running> Default for RollingIncrementalEngine<G, C, Accumulator, Running>
49where
50	G: Clone + Eq + Ord + Hash + Debug + Serialize + DeserializeOwned,
51	C: Slot + Hash + Serialize + DeserializeOwned,
52	Accumulator: WindowAccumulator,
53	Running: WindowAccumulator,
54	for<'a> &'a G: IntoEncodedKey,
55{
56	fn default() -> Self {
57		Self::new()
58	}
59}
60
61impl<G, C, Accumulator, Running> RollingIncrementalEngine<G, C, Accumulator, Running>
62where
63	G: Clone + Eq + Ord + Hash + Debug + Serialize + DeserializeOwned,
64	C: Slot + Hash + Serialize + DeserializeOwned,
65	Accumulator: WindowAccumulator,
66	Running: WindowAccumulator,
67	for<'a> &'a G: IntoEncodedKey,
68{
69	pub fn new() -> Self {
70		Self::with_late_policy(LatePolicy::Drop)
71	}
72
73	pub fn with_late_policy(late_policy: LatePolicy) -> Self {
74		Self {
75			buffers: StateCache::<RowNumber, RollingBuffer<C, Accumulator>>::new(8),
76			running: StateCache::<RowNumber, Running>::new(8),
77			meta: StateCache::<MetaKey, GroupMeta<C>>::new_internal(64),
78			late_policy,
79			_pd: PhantomData,
80		}
81	}
82
83	pub fn apply<S, K, WC, CR, Output>(
84		&mut self,
85		store: &mut S,
86		buckets: RollingBuckets<G, C, Accumulator::Contribution>,
87		capacity: usize,
88		row_key: K,
89		window_contribution: WC,
90		combine_running: CR,
91	) -> Result<Vec<RollingResult<G, Output>>>
92	where
93		S: WindowStore,
94		K: Fn(&G) -> EncodedKey,
95		WC: Fn(&Accumulator::Output) -> Running::Contribution,
96		CR: Fn(&G, &Running, &Accumulator::Output, C) -> Option<Output>,
97	{
98		if buckets.is_empty() {
99			return Ok(Vec::new());
100		}
101		let mut meta_loaded = self.warm_and_load_meta(store, &buckets)?;
102		let buffer_rows = self.resolve_buffer_rows(store, &buckets, &meta_loaded, &row_key)?;
103
104		let late_policy = self.late_policy;
105		let mut group_slots: BTreeMap<G, GroupSlot<C, Accumulator, Running>> = BTreeMap::new();
106
107		for ((group, coord), events) in buckets {
108			let meta = meta_loaded.entry(group.clone()).or_default();
109
110			let slot = match group_slots.get_mut(&group) {
111				Some(s) => s,
112				None => {
113					let (row_number, is_new) = match buffer_rows.get(&group) {
114						Some(&resolved) => resolved,
115						None => {
116							let key = row_key(&group);
117							store.get_or_create_row_number(&key)?
118						}
119					};
120					let buffer: RollingBuffer<C, Accumulator> =
121						self.buffers.get(store, &row_number)?.unwrap_or_default();
122					let running: Running =
123						self.running.get(store, &row_number)?.unwrap_or_default();
124					let was_empty_before = buffer.is_empty();
125					group_slots.insert(
126						group.clone(),
127						GroupSlot {
128							row_number,
129							is_new,
130							buffer,
131							running,
132							was_empty_before,
133							buffer_changed: false,
134						},
135					);
136					group_slots.get_mut(&group).expect("just inserted")
137				}
138			};
139
140			let late = matches!(meta.high_water, Some(hw) if coord < hw)
141				&& matches!(late_policy, LatePolicy::Drop)
142				&& !slot.buffer.contains_key(&coord);
143
144			let mut accumulator = slot.buffer.remove(&coord).unwrap_or_default();
145			let old_value = accumulator.finalize();
146			let mut touched = false;
147			for event in events {
148				match event {
149					AccumulatorEvent::Add(c) => {
150						if late {
151							continue;
152						}
153						accumulator.add(&c);
154						touched = true;
155					}
156					AccumulatorEvent::Remove(c) => {
157						if accumulator.is_empty() {
158							continue;
159						}
160						accumulator.remove(&c);
161						touched = true;
162					}
163				}
164			}
165			if !touched {
166				continue;
167			}
168			let new_value = accumulator.finalize();
169
170			if let Some(old) = &old_value {
171				slot.running.remove(&window_contribution(old));
172			}
173			if let Some(new) = &new_value {
174				slot.running.add(&window_contribution(new));
175			}
176
177			if !accumulator.is_empty() {
178				slot.buffer.insert(coord, accumulator);
179			}
180			while slot.buffer.len() > capacity {
181				if let Some((_, evicted)) = slot.buffer.pop_first()
182					&& let Some(value) = evicted.finalize()
183				{
184					slot.running.remove(&window_contribution(&value));
185				}
186			}
187			slot.buffer_changed = true;
188
189			meta.high_water = Some(match meta.high_water {
190				Some(hw) if hw > coord => hw,
191				_ => coord,
192			});
193		}
194
195		let mut results: Vec<RollingResult<G, Output>> = Vec::new();
196		for (group, slot) in group_slots {
197			if !slot.buffer_changed {
198				continue;
199			}
200			let output = match slot.buffer.iter().next_back() {
201				Some((coord, accumulator)) => accumulator
202					.finalize()
203					.and_then(|newest| combine_running(&group, &slot.running, &newest, *coord)),
204				None => None,
205			};
206			self.buffers.put(store, &slot.row_number, slot.buffer)?;
207			self.running.put(store, &slot.row_number, slot.running)?;
208
209			if let Some(out) = output {
210				let kind = if slot.is_new || slot.was_empty_before {
211					EmitKind::Insert
212				} else {
213					EmitKind::Update
214				};
215				results.push(RollingResult {
216					row_number: slot.row_number,
217					group,
218					value: out,
219					prior: None,
220					kind,
221				});
222			}
223		}
224		self.persist_meta(store, meta_loaded)?;
225		Ok(results)
226	}
227
228	pub fn flush<S: WindowStore>(&mut self, store: &mut S) -> Result<()> {
229		self.buffers.flush(store)?;
230		self.running.flush(store)?;
231		self.meta.flush(store)?;
232		Ok(())
233	}
234
235	fn warm_and_load_meta<S: WindowStore>(
236		&mut self,
237		store: &mut S,
238		buckets: &RollingBuckets<G, C, Accumulator::Contribution>,
239	) -> Result<MetaLoaded<G, C>> {
240		let meta_keys: Vec<MetaKey> = buckets
241			.keys()
242			.map(|(group, _)| group)
243			.collect::<BTreeSet<_>>()
244			.into_iter()
245			.map(meta_key_for)
246			.collect();
247		self.meta.warm(store, &meta_keys)?;
248
249		let mut meta_loaded: MetaLoaded<G, C> = HashMap::new();
250		for (group, _) in buckets.keys() {
251			if !meta_loaded.contains_key(group) {
252				let m = self.meta.get(store, &meta_key_for(group))?.unwrap_or_default();
253				meta_loaded.insert(group.clone(), m);
254			}
255		}
256		Ok(meta_loaded)
257	}
258
259	fn resolve_buffer_rows<S, K>(
260		&mut self,
261		store: &mut S,
262		buckets: &RollingBuckets<G, C, Accumulator::Contribution>,
263		meta_loaded: &MetaLoaded<G, C>,
264		row_key: &K,
265	) -> Result<BufferRows<G>>
266	where
267		S: WindowStore,
268		K: Fn(&G) -> EncodedKey,
269	{
270		let mut buffer_rows: BufferRows<G> = HashMap::new();
271		let mut resolve_order: Vec<G> = Vec::new();
272		let mut group_keys: Vec<EncodedKey> = Vec::new();
273		let mut seen: BTreeSet<G> = BTreeSet::new();
274		for (group, coord) in buckets.keys() {
275			let initial_high_water = meta_loaded.get(group).and_then(|m| m.high_water);
276			if initial_high_water.is_none_or(|hw| *coord >= hw) && seen.insert(group.clone()) {
277				resolve_order.push(group.clone());
278				group_keys.push(row_key(group));
279			}
280		}
281		let resolved_rows = store.get_or_create_row_numbers(&group_keys)?;
282		reifydb_assertions! {
283			let resolved = resolved_rows.len();
284			let requested = group_keys.len();
285			assert!(
286				resolved == requested,
287				"get_or_create_row_numbers returned {resolved} rows for {requested} group keys; \
288				 the zip below pairs resolve_order with resolved_rows by position, so a length \
289				 mismatch would silently leave some groups without a buffer_rows entry and route \
290				 them through the per-bucket get_or_create_row_number fallback, diverging behaviour"
291			);
292		}
293		let state_keys: Vec<RowNumber> = resolved_rows.iter().map(|(rn, _)| *rn).collect();
294		for (group, resolved) in resolve_order.into_iter().zip(resolved_rows) {
295			buffer_rows.insert(group, resolved);
296		}
297		self.buffers.warm(store, &state_keys)?;
298		self.running.warm(store, &state_keys)?;
299		Ok(buffer_rows)
300	}
301
302	fn persist_meta<S: WindowStore>(&mut self, store: &mut S, meta_loaded: MetaLoaded<G, C>) -> Result<()> {
303		for (group, meta) in meta_loaded {
304			self.meta.set(store, &meta_key_for(&group), &meta)?;
305		}
306		Ok(())
307	}
308}