Skip to main content

reifydb_core/state/
cache.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{hash::Hash, marker::PhantomData};
5
6use reifydb_codec::row::operator::{OperatorState, decode};
7use reifydb_value::Result;
8
9use crate::{key::operator_state::IntoGroupStateKey, metrics::heap::HeapSize, state::store::StateStore};
10
11pub struct StateCache<K, V> {
12	marker: PhantomData<fn(K) -> V>,
13}
14
15impl<K, V> Default for StateCache<K, V> {
16	fn default() -> Self {
17		Self::new()
18	}
19}
20
21impl<K, V> StateCache<K, V> {
22	pub fn new() -> Self {
23		Self {
24			marker: PhantomData,
25		}
26	}
27}
28
29impl<K, V> StateCache<K, V>
30where
31	K: Hash + Eq + Clone + HeapSize,
32	for<'a> &'a K: IntoGroupStateKey,
33	V: Clone + OperatorState + HeapSize,
34{
35	pub fn get(&mut self, store: &mut dyn StateStore, key: &K) -> Result<Option<V>> {
36		let encoded_key = key.into_group_state_key();
37		match store.state_get(&encoded_key)? {
38			Some(bytes) => Ok(Some(decode::<V>(&bytes)?)),
39			None => Ok(None),
40		}
41	}
42
43	pub fn set(&mut self, store: &mut dyn StateStore, key: &K, value: &V) -> Result<()> {
44		let encoded_key = key.into_group_state_key();
45		let payload = value.encode_state(store.written_at())?;
46		store.state_set(&encoded_key, payload)
47	}
48
49	pub fn put(&mut self, store: &mut dyn StateStore, key: &K, value: V) -> Result<()> {
50		self.set(store, key, &value)
51	}
52
53	pub fn modify<R>(&mut self, store: &mut dyn StateStore, key: &K, f: impl FnOnce(&mut V) -> R) -> Result<R>
54	where
55		V: Default,
56	{
57		let encoded_key = key.into_group_state_key();
58		let now = store.written_at();
59		let mut value = match store.state_get(&encoded_key)? {
60			Some(bytes) => decode::<V>(&bytes)?,
61			None => V::default(),
62		};
63		let result = f(&mut value);
64		store.state_set(&encoded_key, value.encode_state(now)?)?;
65		Ok(result)
66	}
67
68	pub fn remove(&mut self, store: &mut dyn StateStore, key: &K) -> Result<()> {
69		let encoded_key = key.into_group_state_key();
70		store.state_remove(&encoded_key)
71	}
72}
73
74impl<K, V> StateCache<K, V>
75where
76	K: Hash + Eq + Clone + HeapSize,
77	for<'a> &'a K: IntoGroupStateKey,
78	V: Clone + Default + OperatorState + HeapSize,
79{
80	pub fn get_or_default(&mut self, store: &mut dyn StateStore, key: &K) -> Result<V> {
81		match self.get(store, key)? {
82			Some(value) => Ok(value),
83			None => Ok(V::default()),
84		}
85	}
86
87	pub fn update<U>(&mut self, store: &mut dyn StateStore, key: &K, updater: U) -> Result<V>
88	where
89		U: FnOnce(&mut V) -> Result<()>,
90	{
91		let mut value = self.get_or_default(store, key)?;
92		updater(&mut value)?;
93		self.set(store, key, &value)?;
94		Ok(value)
95	}
96}
97
98#[cfg(test)]
99mod tests {
100	use std::{collections::HashMap, ops::Bound};
101
102	use reifydb_codec::{
103		key::encoded::{EncodedKey, EncodedKeyRange},
104		row::operator::EncodedOperatorRow,
105	};
106	use reifydb_macro::operator_state;
107	use reifydb_value::value::{datetime::DateTime, row_number::RowNumber};
108
109	use super::*;
110	use crate::{
111		key::operator_state::{GroupId, GroupStateKey, Keyspace},
112		state::store::{TimerKind, TimerStore},
113	};
114
115	/// A bare `String` would read as some other group's prefix; this frames the tests' string keys
116	/// the way an operator does.
117	#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
118	struct Key(String);
119
120	impl Key {
121		fn new(key: impl Into<String>) -> Self {
122			Self(key.into())
123		}
124	}
125
126	impl HeapSize for Key {
127		fn heap_size(&self) -> usize {
128			self.0.capacity()
129		}
130	}
131
132	impl IntoGroupStateKey for &Key {
133		fn into_group_state_key(self) -> GroupStateKey {
134			GroupStateKey::root(Keyspace::CUSTOM, self.0.as_bytes())
135		}
136	}
137
138	#[operator_state]
139	#[derive(Debug, Clone, Copy, Default, PartialEq)]
140	struct Cell {
141		value: i32,
142	}
143
144	impl HeapSize for Cell {
145		fn heap_size(&self) -> usize {
146			0
147		}
148	}
149
150	fn cell(value: i32) -> Cell {
151		Cell {
152			value,
153		}
154	}
155
156	#[derive(Default)]
157	struct MockStore {
158		data: HashMap<Vec<u8>, EncodedOperatorRow>,
159		groups: HashMap<Vec<u8>, GroupId>,
160		removes: usize,
161		sets: usize,
162		gets: usize,
163		// Settable clock so the persisted timestamp is observable; defaults to epoch.
164		now: DateTime,
165	}
166
167	impl TimerStore for MockStore {
168		fn arm_timer(&mut self, _due: DateTime, _kind: TimerKind, _key: &EncodedKey) -> Result<()> {
169			unreachable!("the window engine never arms timers; only the shell above it does")
170		}
171
172		fn disarm_timer(&mut self, _due: DateTime, _kind: TimerKind, _key: &EncodedKey) -> Result<()> {
173			unreachable!("the window engine never disarms timers; only the shell above it does")
174		}
175
176		fn flow_watermark(&mut self) -> Result<Option<DateTime>> {
177			Ok(None)
178		}
179	}
180
181	impl StateStore for MockStore {
182		fn intern_groups(&mut self, groups: &[EncodedKey]) -> Result<Vec<(GroupId, bool)>> {
183			let mut interned = Vec::with_capacity(groups.len());
184			for group in groups {
185				let bytes = group.as_bytes().to_vec();
186				match self.groups.get(&bytes) {
187					Some(id) => interned.push((*id, false)),
188					None => {
189						let next = GroupId(self.groups.len() as u64 + GroupId::FIRST.0);
190						self.groups.insert(bytes, next);
191						interned.push((next, true));
192					}
193				}
194			}
195			Ok(interned)
196		}
197
198		fn lookup_groups(&mut self, groups: &[EncodedKey]) -> Result<Vec<Option<GroupId>>> {
199			Ok(groups.iter().map(|group| self.groups.get(group.as_bytes()).copied()).collect())
200		}
201
202		fn state_get(&mut self, key: &GroupStateKey) -> Result<Option<EncodedOperatorRow>> {
203			self.gets += 1;
204			Ok(self.data.get(key.as_slice()).cloned())
205		}
206
207		fn state_get_many_visit(
208			&mut self,
209			keys: &[GroupStateKey],
210			visit: &mut dyn FnMut(GroupStateKey, EncodedOperatorRow) -> Result<()>,
211		) -> Result<()> {
212			for key in keys {
213				if let Some(b) = self.data.get(key.as_slice()) {
214					visit(key.clone(), b.clone())?;
215				}
216			}
217			Ok(())
218		}
219
220		fn state_set(&mut self, key: &GroupStateKey, payload: EncodedOperatorRow) -> Result<()> {
221			self.sets += 1;
222			self.data.insert(key.as_slice().to_vec(), payload);
223			Ok(())
224		}
225
226		fn state_remove(&mut self, key: &GroupStateKey) -> Result<()> {
227			self.removes += 1;
228			self.data.remove(key.as_slice());
229			Ok(())
230		}
231
232		fn state_range_visit(
233			&mut self,
234			range: EncodedKeyRange,
235			limit: Option<usize>,
236			visit: &mut dyn FnMut(GroupStateKey, EncodedOperatorRow) -> Result<()>,
237		) -> Result<()> {
238			let after_start = |k: &[u8]| match &range.start {
239				Bound::Included(s) => k >= s.as_bytes(),
240				Bound::Excluded(s) => k > s.as_bytes(),
241				Bound::Unbounded => true,
242			};
243			let before_end = |k: &[u8]| match &range.end {
244				Bound::Included(e) => k <= e.as_bytes(),
245				Bound::Excluded(e) => k < e.as_bytes(),
246				Bound::Unbounded => true,
247			};
248			let mut matched: Vec<(Vec<u8>, EncodedOperatorRow)> = self
249				.data
250				.iter()
251				.filter(|(k, _)| after_start(k) && before_end(k))
252				.map(|(k, v)| (k.clone(), v.clone()))
253				.collect();
254			matched.sort_by(|a, b| a.0.cmp(&b.0));
255			if let Some(limit) = limit {
256				matched.truncate(limit);
257			}
258			for (k, b) in matched {
259				let k = GroupStateKey::from_framed(EncodedKey::new(k))
260					.expect("fake store holds an unframed state key");
261				visit(k, b)?;
262			}
263			Ok(())
264		}
265
266		fn get_or_create_row_numbers(
267			&mut self,
268			_group: GroupId,
269			keys: &[EncodedKey],
270		) -> Result<Vec<(RowNumber, bool)>> {
271			Ok(keys.iter().enumerate().map(|(i, _)| (RowNumber(i as u64 + 1), true)).collect())
272		}
273
274		fn get_or_create_row_numbers_for_pairs(
275			&mut self,
276			pairs: &[(GroupId, EncodedKey)],
277		) -> Result<Vec<(RowNumber, bool)>> {
278			Ok(pairs.iter().enumerate().map(|(i, _)| (RowNumber(i as u64 + 1), true)).collect())
279		}
280
281		fn remove_row_number(&mut self, _group: GroupId, _key: &EncodedKey) -> Result<()> {
282			Ok(())
283		}
284
285		fn written_at(&self) -> DateTime {
286			self.now
287		}
288	}
289
290	#[test]
291	fn set_reaches_the_store_without_waiting_for_flush() {
292		// Nothing buffers a pending write, so the value must be durable the moment set returns.
293		let mut store = MockStore::default();
294		let mut cache: StateCache<Key, Cell> = StateCache::new();
295
296		cache.set(&mut store, &Key::new("a"), &cell(7)).unwrap();
297
298		assert_eq!(store.sets, 1, "set must issue exactly one state_set");
299		assert!(!store.data.is_empty(), "the value must be in the store before any flush");
300		assert_eq!(cache.get(&mut store, &Key::new("a")).unwrap(), Some(cell(7)));
301	}
302
303	#[test]
304	fn get_reads_through_to_the_store_every_time() {
305		// A cached second read would let another writer's value go unseen.
306		let mut store = MockStore::default();
307		let mut cache: StateCache<Key, Cell> = StateCache::new();
308		cache.set(&mut store, &Key::new("a"), &cell(1)).unwrap();
309
310		cache.get(&mut store, &Key::new("a")).unwrap();
311		cache.get(&mut store, &Key::new("a")).unwrap();
312
313		assert_eq!(store.gets, 2, "each get must be served by the store, never from residency");
314	}
315
316	#[test]
317	fn a_miss_consults_the_store_rather_than_proving_absence() {
318		// Absence is no longer answerable from a filter; every miss must be a real store read.
319		let mut store = MockStore::default();
320		let mut cache: StateCache<Key, Cell> = StateCache::new();
321
322		assert_eq!(cache.get(&mut store, &Key::new("absent")).unwrap(), None);
323		assert_eq!(store.gets, 1, "the miss must have consulted the store");
324	}
325
326	#[test]
327	fn a_read_leaves_the_row_in_the_store_for_the_next_reader() {
328		// The read half of load-mutate-persist must not consume the row, or the base value is lost.
329		let mut store = MockStore::default();
330		let mut cache: StateCache<Key, Cell> = StateCache::new();
331		cache.set(&mut store, &Key::new("a"), &cell(3)).unwrap();
332
333		assert_eq!(cache.get(&mut store, &Key::new("a")).unwrap(), Some(cell(3)));
334		assert_eq!(store.removes, 0, "a read must not issue a state_remove");
335		assert_eq!(cache.get(&mut store, &Key::new("a")).unwrap(), Some(cell(3)));
336	}
337
338	#[test]
339	fn read_then_persist_round_trips_a_mutation() {
340		// Without an in-place overwrite on the persist half the mutation is silently dropped.
341		let mut store = MockStore::default();
342		let mut cache: StateCache<Key, Cell> = StateCache::new();
343		cache.set(&mut store, &Key::new("a"), &cell(1)).unwrap();
344
345		let mut value = cache.get(&mut store, &Key::new("a")).unwrap().unwrap();
346		value.value += 41;
347		cache.set(&mut store, &Key::new("a"), &value).unwrap();
348
349		assert_eq!(cache.get(&mut store, &Key::new("a")).unwrap(), Some(cell(42)));
350	}
351
352	#[test]
353	fn remove_issues_a_state_remove_and_the_key_reads_back_absent() {
354		let mut store = MockStore::default();
355		let mut cache: StateCache<Key, Cell> = StateCache::new();
356		cache.set(&mut store, &Key::new("a"), &cell(5)).unwrap();
357
358		cache.remove(&mut store, &Key::new("a")).unwrap();
359
360		assert_eq!(store.removes, 1);
361		assert_eq!(cache.get(&mut store, &Key::new("a")).unwrap(), None);
362	}
363
364	#[test]
365	fn modify_mutates_the_stored_value_and_persists_it() {
366		// modify is load-mutate-persist in one call; skipping the persist half would strand the mutation.
367		let mut store = MockStore::default();
368		let mut cache: StateCache<Key, Cell> = StateCache::new();
369		cache.set(&mut store, &Key::new("a"), &cell(1)).unwrap();
370
371		let returned = cache
372			.modify(&mut store, &Key::new("a"), |value| {
373				value.value += 6;
374				value.value
375			})
376			.unwrap();
377
378		assert_eq!(returned, 7);
379		assert_eq!(cache.get(&mut store, &Key::new("a")).unwrap(), Some(cell(7)));
380	}
381
382	#[test]
383	fn modify_persists_the_row_with_a_refreshed_time() {
384		// The rewritten row must carry the store's current clock, or floor expiry reads a stale stamp.
385		let mut store = MockStore::default();
386		let mut cache: StateCache<Key, Cell> = StateCache::new();
387		cache.set(&mut store, &Key::new("a"), &cell(1)).unwrap();
388		store.now = DateTime::from_nanos(4_000);
389
390		cache.modify(&mut store, &Key::new("a"), |value| {
391			value.value = 3;
392		})
393		.unwrap();
394
395		let key = (&Key::new("a")).into_group_state_key();
396		let stored = store.data.get(key.as_slice()).expect("the row was written");
397		assert_eq!(stored.time(), DateTime::from_nanos(4_000));
398	}
399
400	#[test]
401	fn modify_on_a_miss_starts_from_default_and_persists() {
402		// Otherwise the first mutation of a never-written group is lost.
403		let mut store = MockStore::default();
404		let mut cache: StateCache<Key, Cell> = StateCache::new();
405
406		cache.modify(&mut store, &Key::new("fresh"), |value| {
407			value.value = 5;
408		})
409		.unwrap();
410
411		assert_eq!(cache.get(&mut store, &Key::new("fresh")).unwrap(), Some(cell(5)));
412	}
413
414	#[test]
415	fn get_or_default_returns_the_default_only_for_an_absent_key() {
416		let mut store = MockStore::default();
417		let mut cache: StateCache<Key, Cell> = StateCache::new();
418		cache.set(&mut store, &Key::new("present"), &cell(8)).unwrap();
419
420		assert_eq!(cache.get_or_default(&mut store, &Key::new("present")).unwrap(), cell(8));
421		assert_eq!(cache.get_or_default(&mut store, &Key::new("absent")).unwrap(), Cell::default());
422	}
423
424	#[test]
425	fn update_persists_the_mutation_and_returns_the_new_value() {
426		// If the set half were skipped the returned value would disagree with the next read.
427		let mut store = MockStore::default();
428		let mut cache: StateCache<Key, Cell> = StateCache::new();
429
430		let returned = cache
431			.update(&mut store, &Key::new("a"), |value| {
432				value.value += 4;
433				Ok(())
434			})
435			.unwrap();
436
437		assert_eq!(returned, cell(4));
438		assert_eq!(cache.get(&mut store, &Key::new("a")).unwrap(), Some(cell(4)));
439	}
440}