1use 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}