Skip to main content

reifydb_flow/operator/
state_access.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::hash::Hash;
5
6use reifydb_codec::row::operator::state::{OperatorState, decode};
7use reifydb_core::{
8	key::operator::state::{IntoGroupStateKey, row_number_counter_key},
9	metrics::heap::HeapSize,
10	state::timer::StateStore,
11};
12use reifydb_value::{Result, value::row_number::RowNumber};
13
14pub fn mint_row_numbers<S>(store: &mut S, count: u64) -> Result<RowNumber>
15where
16	S: StateStore + ?Sized,
17{
18	let key = row_number_counter_key();
19	let seed = match store.state_get(&key)? {
20		Some(row) => decode::<u64>(&row)?,
21		None => 1,
22	};
23	store.state_set(&key, (seed + count).encode_state()?)?;
24	Ok(RowNumber(seed))
25}
26
27pub fn get<K, V>(store: &mut dyn StateStore, key: &K) -> Result<Option<V>>
28where
29	K: Hash + Eq + Clone + HeapSize,
30	for<'a> &'a K: IntoGroupStateKey,
31	V: Clone + OperatorState + HeapSize,
32{
33	let encoded_key = key.into_group_state_key();
34	match store.state_get(&encoded_key)? {
35		Some(bytes) => Ok(Some(decode::<V>(&bytes)?)),
36		None => Ok(None),
37	}
38}
39
40pub fn get_classified<K, V>(store: &mut dyn StateStore, key: &K) -> Result<Option<V>>
41where
42	K: Hash + Eq + Clone + HeapSize,
43	for<'a> &'a K: IntoGroupStateKey,
44	V: Clone + OperatorState + HeapSize,
45{
46	let encoded_key = key.into_group_state_key();
47	let (value, pre) = match store.state_get(&encoded_key)? {
48		Some(bytes) => (Some(decode::<V>(&bytes)?), Some(bytes.byte_size())),
49		None => (None, None),
50	};
51	store.state_classify(&encoded_key, pre);
52	Ok(value)
53}
54
55pub fn set<K, V>(store: &mut dyn StateStore, key: &K, value: &V) -> Result<()>
56where
57	K: Hash + Eq + Clone + HeapSize,
58	for<'a> &'a K: IntoGroupStateKey,
59	V: Clone + OperatorState + HeapSize,
60{
61	let encoded_key = key.into_group_state_key();
62	let payload = value.encode_state()?;
63	store.state_set(&encoded_key, payload)
64}
65
66pub fn put<K, V>(store: &mut dyn StateStore, key: &K, value: V) -> Result<()>
67where
68	K: Hash + Eq + Clone + HeapSize,
69	for<'a> &'a K: IntoGroupStateKey,
70	V: Clone + OperatorState + HeapSize,
71{
72	set(store, key, &value)
73}
74
75pub fn modify<K, V, R>(store: &mut dyn StateStore, key: &K, f: impl FnOnce(&mut V) -> R) -> Result<R>
76where
77	K: Hash + Eq + Clone + HeapSize,
78	for<'a> &'a K: IntoGroupStateKey,
79	V: Clone + Default + OperatorState + HeapSize,
80{
81	let encoded_key = key.into_group_state_key();
82	let (mut value, pre) = match store.state_get(&encoded_key)? {
83		Some(bytes) => (decode::<V>(&bytes)?, Some(bytes.byte_size())),
84		None => (V::default(), None),
85	};
86	store.state_classify(&encoded_key, pre);
87	let result = f(&mut value);
88	store.state_set(&encoded_key, value.encode_state()?)?;
89	Ok(result)
90}
91
92pub fn remove<K>(store: &mut dyn StateStore, key: &K) -> Result<()>
93where
94	K: Hash + Eq + Clone + HeapSize,
95	for<'a> &'a K: IntoGroupStateKey,
96{
97	let encoded_key = key.into_group_state_key();
98	store.state_remove(&encoded_key)
99}
100
101pub fn get_or_default<K, V>(store: &mut dyn StateStore, key: &K) -> Result<V>
102where
103	K: Hash + Eq + Clone + HeapSize,
104	for<'a> &'a K: IntoGroupStateKey,
105	V: Clone + Default + OperatorState + HeapSize,
106{
107	match get(store, key)? {
108		Some(value) => Ok(value),
109		None => Ok(V::default()),
110	}
111}
112
113pub fn update<K, V, U>(store: &mut dyn StateStore, key: &K, updater: U) -> Result<V>
114where
115	K: Hash + Eq + Clone + HeapSize,
116	for<'a> &'a K: IntoGroupStateKey,
117	V: Clone + Default + OperatorState + HeapSize,
118	U: FnOnce(&mut V) -> Result<()>,
119{
120	let encoded_key = key.into_group_state_key();
121	let (mut value, pre) = match store.state_get(&encoded_key)? {
122		Some(bytes) => (decode::<V>(&bytes)?, Some(bytes.byte_size())),
123		None => (V::default(), None),
124	};
125	store.state_classify(&encoded_key, pre);
126	updater(&mut value)?;
127	store.state_set(&encoded_key, value.encode_state()?)?;
128	Ok(value)
129}
130
131#[cfg(test)]
132mod tests {
133	use std::{collections::HashMap, ops::Bound};
134
135	use reifydb_codec::{
136		key::encoded::{EncodedKey, EncodedKeyRange},
137		row::pod::EncodedPodRow,
138	};
139	use reifydb_core::{
140		key::operator::state::{GroupId, GroupStateKey, custom_not_cached_key},
141		state::timer::{TimerKind, TimerStore},
142	};
143	use reifydb_macro::operator_state;
144	use reifydb_value::{
145		byte_size::ByteSize,
146		value::{datetime::DateTime, row_number::RowNumber},
147	};
148
149	use super::*;
150
151	/// A bare `String` would read as some other group's prefix; this frames the tests' string keys
152	/// the way an operator does.
153	#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
154	struct Key(String);
155
156	impl Key {
157		fn new(key: impl Into<String>) -> Self {
158			Self(key.into())
159		}
160	}
161
162	impl HeapSize for Key {
163		fn heap_size(&self) -> usize {
164			self.0.capacity()
165		}
166	}
167
168	impl IntoGroupStateKey for &Key {
169		fn into_group_state_key(self) -> GroupStateKey {
170			custom_not_cached_key(self.0.as_bytes())
171				.expect("a custom state key must be at most sixteen bytes")
172		}
173	}
174
175	#[operator_state]
176	#[derive(Debug, Clone, Copy, Default, PartialEq)]
177	struct Cell {
178		value: i32,
179	}
180
181	impl HeapSize for Cell {
182		fn heap_size(&self) -> usize {
183			0
184		}
185	}
186
187	fn cell(value: i32) -> Cell {
188		Cell {
189			value,
190		}
191	}
192
193	#[derive(Default)]
194	struct MockStore {
195		data: HashMap<Vec<u8>, EncodedPodRow>,
196		removes: usize,
197		sets: usize,
198		gets: usize,
199		classifications: Vec<(Vec<u8>, Option<ByteSize>)>,
200		// Settable clock so the persisted timestamp is observable; defaults to epoch.
201		now: DateTime,
202	}
203
204	impl TimerStore for MockStore {
205		fn arm_timer(&mut self, _due: DateTime, _kind: TimerKind, _key: &EncodedKey) -> Result<()> {
206			unreachable!("the window engine never arms timers; only the shell above it does")
207		}
208
209		fn disarm_timer(&mut self, _due: DateTime, _kind: TimerKind, _key: &EncodedKey) -> Result<()> {
210			unreachable!("the window engine never disarms timers; only the shell above it does")
211		}
212
213		fn flow_watermark(&mut self) -> Result<Option<DateTime>> {
214			Ok(None)
215		}
216	}
217
218	impl StateStore for MockStore {
219		fn state_get(&mut self, key: &GroupStateKey) -> Result<Option<EncodedPodRow>> {
220			self.gets += 1;
221			Ok(self.data.get(key.as_slice()).cloned())
222		}
223
224		fn state_get_many_visit(
225			&mut self,
226			keys: &[GroupStateKey],
227			visit: &mut dyn FnMut(GroupStateKey, EncodedPodRow) -> Result<()>,
228		) -> Result<()> {
229			for key in keys {
230				if let Some(b) = self.data.get(key.as_slice()) {
231					visit(key.clone(), b.clone())?;
232				}
233			}
234			Ok(())
235		}
236
237		fn state_classify(&mut self, key: &GroupStateKey, pre: Option<ByteSize>) {
238			self.classifications.push((key.as_slice().to_vec(), pre));
239		}
240
241		fn state_set(&mut self, key: &GroupStateKey, payload: EncodedPodRow) -> Result<()> {
242			self.sets += 1;
243			self.data.insert(key.as_slice().to_vec(), payload);
244			Ok(())
245		}
246
247		fn state_remove(&mut self, key: &GroupStateKey) -> Result<()> {
248			self.removes += 1;
249			self.data.remove(key.as_slice());
250			Ok(())
251		}
252
253		fn state_page_inner(
254			&mut self,
255			range: EncodedKeyRange,
256			limit: Option<usize>,
257		) -> Result<Vec<(GroupStateKey, EncodedPodRow)>> {
258			let after_start = |k: &[u8]| match &range.start {
259				Bound::Included(s) => k >= s.as_bytes(),
260				Bound::Excluded(s) => k > s.as_bytes(),
261				Bound::Unbounded => true,
262			};
263			let before_end = |k: &[u8]| match &range.end {
264				Bound::Included(e) => k <= e.as_bytes(),
265				Bound::Excluded(e) => k < e.as_bytes(),
266				Bound::Unbounded => true,
267			};
268			let mut matched: Vec<(Vec<u8>, EncodedPodRow)> = self
269				.data
270				.iter()
271				.filter(|(k, _)| after_start(k) && before_end(k))
272				.map(|(k, v)| (k.clone(), v.clone()))
273				.collect();
274			matched.sort_by(|a, b| a.0.cmp(&b.0));
275			if let Some(limit) = limit {
276				matched.truncate(limit);
277			}
278			Ok(matched
279				.into_iter()
280				.map(|(k, b)| {
281					let k = GroupStateKey::from_framed(EncodedKey::new(k))
282						.expect("fake store holds an unframed state key");
283					(k, b)
284				})
285				.collect())
286		}
287
288		fn get_or_create_row_numbers(
289			&mut self,
290			_group: GroupId,
291			keys: &[EncodedKey],
292		) -> Result<Vec<(RowNumber, bool)>> {
293			Ok(keys.iter().enumerate().map(|(i, _)| (RowNumber(i as u64 + 1), true)).collect())
294		}
295
296		fn get_or_create_row_numbers_for_groups(
297			&mut self,
298			groups: &[GroupId],
299		) -> Result<Vec<(RowNumber, bool)>> {
300			Ok(groups.iter().enumerate().map(|(i, _)| (RowNumber(i as u64 + 1), true)).collect())
301		}
302
303		fn remove_row_number(&mut self, _group: GroupId, _key: &EncodedKey) -> Result<()> {
304			Ok(())
305		}
306
307		fn remove_row_number_for_group(&mut self, _group: GroupId) -> Result<()> {
308			Ok(())
309		}
310
311		fn written_at(&self) -> DateTime {
312			self.now
313		}
314	}
315
316	#[test]
317	fn set_reaches_the_store_without_waiting_for_flush() {
318		// Nothing buffers a pending write, so the value must be durable the moment set returns.
319		let mut store = MockStore::default();
320
321		set(&mut store, &Key::new("a"), &cell(7)).unwrap();
322
323		assert_eq!(store.sets, 1, "set must issue exactly one state_set");
324		assert!(!store.data.is_empty(), "the value must be in the store before any flush");
325		assert_eq!(get::<_, Cell>(&mut store, &Key::new("a")).unwrap(), Some(cell(7)));
326	}
327
328	#[test]
329	fn get_reads_through_to_the_store_every_time() {
330		// A cached second read would let another writer's value go unseen.
331		let mut store = MockStore::default();
332		set(&mut store, &Key::new("a"), &cell(1)).unwrap();
333
334		get::<_, Cell>(&mut store, &Key::new("a")).unwrap();
335		get::<_, Cell>(&mut store, &Key::new("a")).unwrap();
336
337		assert_eq!(store.gets, 2, "each get must be served by the store, never from residency");
338	}
339
340	#[test]
341	fn a_miss_consults_the_store_rather_than_proving_absence() {
342		// Absence is no longer answerable from a filter; every miss must be a real store read.
343		let mut store = MockStore::default();
344
345		assert_eq!(get::<_, Cell>(&mut store, &Key::new("absent")).unwrap(), None);
346		assert_eq!(store.gets, 1, "the miss must have consulted the store");
347	}
348
349	#[test]
350	fn a_read_leaves_the_row_in_the_store_for_the_next_reader() {
351		// The read half of load-mutate-persist must not consume the row, or the base value is lost.
352		let mut store = MockStore::default();
353		set(&mut store, &Key::new("a"), &cell(3)).unwrap();
354
355		assert_eq!(get::<_, Cell>(&mut store, &Key::new("a")).unwrap(), Some(cell(3)));
356		assert_eq!(store.removes, 0, "a read must not issue a state_remove");
357		assert_eq!(get::<_, Cell>(&mut store, &Key::new("a")).unwrap(), Some(cell(3)));
358	}
359
360	#[test]
361	fn read_then_persist_round_trips_a_mutation() {
362		// Without an in-place overwrite on the persist half the mutation is silently dropped.
363		let mut store = MockStore::default();
364		set(&mut store, &Key::new("a"), &cell(1)).unwrap();
365
366		let mut value = get::<_, Cell>(&mut store, &Key::new("a")).unwrap().unwrap();
367		value.value += 41;
368		set(&mut store, &Key::new("a"), &value).unwrap();
369
370		assert_eq!(get::<_, Cell>(&mut store, &Key::new("a")).unwrap(), Some(cell(42)));
371	}
372
373	#[test]
374	fn remove_issues_a_state_remove_and_the_key_reads_back_absent() {
375		let mut store = MockStore::default();
376		set(&mut store, &Key::new("a"), &cell(5)).unwrap();
377
378		remove(&mut store, &Key::new("a")).unwrap();
379
380		assert_eq!(store.removes, 1);
381		assert_eq!(get::<_, Cell>(&mut store, &Key::new("a")).unwrap(), None);
382	}
383
384	#[test]
385	fn modify_mutates_the_stored_value_and_persists_it() {
386		// modify is load-mutate-persist in one call; skipping the persist half would strand the mutation.
387		let mut store = MockStore::default();
388		set(&mut store, &Key::new("a"), &cell(1)).unwrap();
389
390		let returned = modify(&mut store, &Key::new("a"), |value: &mut Cell| {
391			value.value += 6;
392			value.value
393		})
394		.unwrap();
395
396		assert_eq!(returned, 7);
397		assert_eq!(get::<_, Cell>(&mut store, &Key::new("a")).unwrap(), Some(cell(7)));
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
405		modify(&mut store, &Key::new("fresh"), |value: &mut Cell| {
406			value.value = 5;
407		})
408		.unwrap();
409
410		assert_eq!(get::<_, Cell>(&mut store, &Key::new("fresh")).unwrap(), Some(cell(5)));
411	}
412
413	#[test]
414	fn get_or_default_returns_the_default_only_for_an_absent_key() {
415		let mut store = MockStore::default();
416		set(&mut store, &Key::new("present"), &cell(8)).unwrap();
417
418		assert_eq!(get_or_default::<_, Cell>(&mut store, &Key::new("present")).unwrap(), cell(8));
419		assert_eq!(get_or_default::<_, Cell>(&mut store, &Key::new("absent")).unwrap(), Cell::default());
420	}
421
422	#[test]
423	fn update_persists_the_mutation_and_returns_the_new_value() {
424		// If the set half were skipped the returned value would disagree with the next read.
425		let mut store = MockStore::default();
426
427		let returned = update(&mut store, &Key::new("a"), |value: &mut Cell| {
428			value.value += 4;
429			Ok(())
430		})
431		.unwrap();
432
433		assert_eq!(returned, cell(4));
434		assert_eq!(get::<_, Cell>(&mut store, &Key::new("a")).unwrap(), Some(cell(4)));
435	}
436
437	#[test]
438	fn modify_hands_the_write_the_size_it_already_read() {
439		// The write classifies itself by reading the key again unless the caller hands the pre-image down, and
440		// modify has just paid that read. The size must be what a durable read of the row would have measured,
441		// or the census is billed a weight the store never held.
442		let mut store = MockStore::default();
443		set(&mut store, &Key::new("a"), &cell(123_456_789)).unwrap();
444		let durable = store.data.values().next().expect("the seed write is in the store").bytes().len();
445		assert!(
446			durable > 1,
447			"the seed must encode to more than one byte or the size assertion cannot discriminate"
448		);
449		store.classifications.clear();
450
451		modify::<_, Cell, _>(&mut store, &Key::new("a"), |c| c.value += 1).unwrap();
452
453		assert_eq!(store.gets, 1, "modify must classify from its own read, never pay a second one");
454		assert_eq!(
455			store.classifications.len(),
456			1,
457			"the write must be handed exactly one pre-image, or it falls back to reading the key again"
458		);
459		assert_eq!(
460			store.classifications[0].1,
461			Some(ByteSize::from_bytes(durable as u64)),
462			"the size handed down must be what a durable read of the row would measure"
463		);
464	}
465
466	#[test]
467	fn modify_of_a_key_that_is_not_there_hands_down_an_absence() {
468		// Absence is a classification too: an insert billed as a replace debits a row the census never held.
469		let mut store = MockStore::default();
470
471		modify::<_, Cell, _>(&mut store, &Key::new("missing"), |c| c.value = 3).unwrap();
472
473		assert_eq!(
474			store.classifications,
475			vec![(Key::new("missing").into_group_state_key().as_slice().to_vec(), None)],
476			"a key the read did not find must be handed down as absent, not left for the write to discover"
477		);
478	}
479
480	#[test]
481	fn update_hands_the_write_the_size_it_already_read() {
482		// update read through get_or_default, which collapses a missing row into a default and throws the
483		// existence bit away, so it could not classify at all without re-reading.
484		let mut store = MockStore::default();
485		set(&mut store, &Key::new("a"), &cell(123_456_789)).unwrap();
486		let durable = store.data.values().next().expect("the seed write is in the store").bytes().len();
487		assert!(
488			durable > 1,
489			"the seed must encode to more than one byte or the size assertion cannot discriminate"
490		);
491		store.classifications.clear();
492
493		update::<_, Cell, _>(&mut store, &Key::new("a"), |c| {
494			c.value += 5;
495			Ok(())
496		})
497		.unwrap();
498
499		assert_eq!(store.gets, 1, "update must classify from its own read, never pay a second one");
500		assert_eq!(
501			store.classifications[0].1,
502			Some(ByteSize::from_bytes(durable as u64)),
503			"the size handed down must be what a durable read of the row would measure"
504		);
505	}
506
507	#[test]
508	fn update_of_a_key_that_is_not_there_hands_down_an_absence() {
509		// The defaulting read it used to go through cannot tell this case from a present zero value.
510		let mut store = MockStore::default();
511
512		update::<_, Cell, _>(&mut store, &Key::new("missing"), |c: &mut Cell| {
513			c.value = 9;
514			Ok(())
515		})
516		.unwrap();
517
518		assert_eq!(
519			store.classifications,
520			vec![(Key::new("missing").into_group_state_key().as_slice().to_vec(), None)],
521			"a key the read did not find must be handed down as absent, not left for the write to discover"
522		);
523	}
524
525	#[test]
526	fn get_classified_hands_the_write_the_size_it_already_read() {
527		// Every caller of this reads a key it is about to write back, so the read must carry the pre-image
528		// forward; otherwise the write re-reads the same key and the helper costs two lookups, not one.
529		let mut store = MockStore::default();
530		set(&mut store, &Key::new("a"), &cell(123_456_789)).unwrap();
531		let durable = store.data.values().next().expect("the seed write is in the store").bytes().len();
532		assert!(
533			durable > 1,
534			"the seed must encode to more than one byte or the size assertion cannot discriminate"
535		);
536		store.classifications.clear();
537		store.gets = 0;
538
539		let value: Option<Cell> = get_classified(&mut store, &Key::new("a")).unwrap();
540
541		assert_eq!(value, Some(cell(123_456_789)), "classifying must not disturb the value it returns");
542		assert_eq!(store.gets, 1, "the classification must ride the read it already paid for");
543		assert_eq!(
544			store.classifications[0].1,
545			Some(ByteSize::from_bytes(durable as u64)),
546			"the size handed down must be what a durable read of the row would measure"
547		);
548	}
549
550	#[test]
551	fn get_classified_of_a_key_that_is_not_there_hands_down_an_absence() {
552		// A first write billed as a replace debits a row the census never held, so absence must be claimed
553		// as loudly as presence.
554		let mut store = MockStore::default();
555
556		let value: Option<Cell> = get_classified(&mut store, &Key::new("missing")).unwrap();
557
558		assert_eq!(value, None, "a missing key still reads as missing");
559		assert_eq!(
560			store.classifications,
561			vec![(Key::new("missing").into_group_state_key().as_slice().to_vec(), None)],
562			"a key the read did not find must be handed down as absent, not left for the write to discover"
563		);
564	}
565}