Skip to main content

reifydb_sub_flow/transaction/
state.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::collections::HashMap;
5
6use reifydb_codec::{
7	encoded::{
8		row::{EncodedRow, SHAPE_HEADER_SIZE},
9		shape::RowShape,
10	},
11	key::encoded::{EncodedKey, EncodedKeyRange},
12};
13use reifydb_core::{
14	interface::{
15		catalog::flow::FlowNodeId,
16		store::{MultiVersionBatch, MultiVersionRow},
17	},
18	key::{EncodableKey, flow_node_internal_state::FlowNodeInternalStateKey, flow_node_state::FlowNodeStateKey},
19};
20use reifydb_transaction::multi::RangeScope;
21use reifydb_value::Result;
22use tracing::{Span, field, instrument};
23
24use super::FlowTransaction;
25
26#[derive(Clone, Copy)]
27enum StateScope {
28	Public,
29	Internal,
30}
31
32impl StateScope {
33	fn encode(self, id: FlowNodeId, key: &EncodedKey) -> EncodedKey {
34		match self {
35			StateScope::Public => FlowNodeStateKey::new(id, key.as_ref().to_vec()).encode(),
36			StateScope::Internal => FlowNodeInternalStateKey::new(id, key.as_ref().to_vec()).encode(),
37		}
38	}
39}
40
41impl FlowTransaction {
42	#[instrument(name = "flow::state::get", level = "trace", skip(self), fields(
43		node_id = id.0,
44		key_len = key.as_bytes().len(),
45		found = field::Empty
46	))]
47	pub fn state_get(&mut self, id: FlowNodeId, key: &EncodedKey) -> Result<Option<EncodedRow>> {
48		let result = self.scoped_get(StateScope::Public, id, key)?;
49		Span::current().record("found", result.is_some());
50		Ok(result)
51	}
52
53	#[instrument(name = "flow::state::get_many", level = "debug", skip(self, keys), fields(
54		node_id = id.0,
55		key_count = keys.len(),
56		found_count = field::Empty
57	))]
58	pub fn state_get_many(&mut self, id: FlowNodeId, keys: &[EncodedKey]) -> Result<MultiVersionBatch> {
59		let batch = self.scoped_get_many(StateScope::Public, id, keys)?;
60		Span::current().record("found_count", batch.items.len());
61		Ok(batch)
62	}
63
64	#[instrument(name = "flow::state::prefetch", level = "debug", skip(self, keys), fields(node_id = id.0, key_count = keys.len()))]
65	pub fn prefetch_state(&mut self, id: FlowNodeId, keys: &[EncodedKey]) -> Result<()> {
66		if keys.is_empty() {
67			return Ok(());
68		}
69
70		let batch = self.state_get_many(id, keys)?;
71		let mut found: HashMap<EncodedKey, EncodedRow> = HashMap::with_capacity(batch.items.len());
72		for item in batch.items {
73			found.insert(item.key, item.row);
74		}
75
76		let inner = self.inner_mut();
77		for key in keys {
78			let encoded_key = StateScope::Public.encode(id, key);
79			let value = found.get(&encoded_key).cloned();
80			inner.prefetch.insert(encoded_key, value);
81		}
82		Ok(())
83	}
84
85	#[instrument(name = "flow::state::set", level = "trace", skip(self, value), fields(
86		node_id = id.0,
87		key_len = key.as_bytes().len(),
88		value_len = value.len()
89	))]
90	pub fn state_set(&mut self, id: FlowNodeId, key: &EncodedKey, value: EncodedRow) -> Result<()> {
91		self.scoped_set(StateScope::Public, id, key, value)
92	}
93
94	#[instrument(name = "flow::state::remove", level = "trace", skip(self), fields(
95		node_id = id.0,
96		key_len = key.as_bytes().len()
97	))]
98	pub fn state_remove(&mut self, id: FlowNodeId, key: &EncodedKey) -> Result<()> {
99		self.scoped_remove(StateScope::Public, id, key)
100	}
101
102	#[instrument(name = "flow::state::drop", level = "trace", skip(self), fields(
103		node_id = id.0,
104		key_len = key.as_bytes().len()
105	))]
106	pub fn state_drop(&mut self, id: FlowNodeId, key: &EncodedKey) -> Result<()> {
107		self.scoped_drop(StateScope::Public, id, key)
108	}
109
110	#[instrument(name = "flow::internal_state::get", level = "trace", skip(self), fields(
111		node_id = id.0,
112		key_len = key.as_bytes().len(),
113		found = field::Empty
114	))]
115	pub fn internal_state_get(&mut self, id: FlowNodeId, key: &EncodedKey) -> Result<Option<EncodedRow>> {
116		let result = self.scoped_get(StateScope::Internal, id, key)?;
117		Span::current().record("found", result.is_some());
118		Ok(result)
119	}
120
121	#[instrument(name = "flow::internal_state::get_many", level = "debug", skip(self, keys), fields(
122		node_id = id.0,
123		key_count = keys.len(),
124		found_count = field::Empty
125	))]
126	pub fn internal_state_get_many(&mut self, id: FlowNodeId, keys: &[EncodedKey]) -> Result<MultiVersionBatch> {
127		let batch = self.scoped_get_many(StateScope::Internal, id, keys)?;
128		Span::current().record("found_count", batch.items.len());
129		Ok(batch)
130	}
131
132	#[instrument(name = "flow::internal_state::set", level = "trace", skip(self, value), fields(
133		node_id = id.0,
134		key_len = key.as_bytes().len(),
135		value_len = value.len()
136	))]
137	pub fn internal_state_set(&mut self, id: FlowNodeId, key: &EncodedKey, value: EncodedRow) -> Result<()> {
138		self.scoped_set(StateScope::Internal, id, key, value)
139	}
140
141	#[instrument(name = "flow::internal_state::remove", level = "trace", skip(self), fields(
142		node_id = id.0,
143		key_len = key.as_bytes().len()
144	))]
145	pub fn internal_state_remove(&mut self, id: FlowNodeId, key: &EncodedKey) -> Result<()> {
146		self.scoped_remove(StateScope::Internal, id, key)
147	}
148
149	#[instrument(name = "flow::internal_state::drop", level = "trace", skip(self), fields(
150		node_id = id.0,
151		key_len = key.as_bytes().len()
152	))]
153	pub fn internal_state_drop(&mut self, id: FlowNodeId, key: &EncodedKey) -> Result<()> {
154		self.scoped_drop(StateScope::Internal, id, key)
155	}
156
157	#[instrument(name = "flow::state::scan", level = "debug", skip(self), fields(
158		node_id = id.0,
159		result_count = field::Empty
160	))]
161	pub fn state_scan_all(&mut self, id: FlowNodeId) -> Result<MultiVersionBatch> {
162		let range = FlowNodeStateKey::node_range(id);
163		let iter = self.range(range, RangeScope::All, 1024);
164		let mut items = Vec::new();
165		for result in iter {
166			items.push(result?);
167		}
168		Span::current().record("result_count", items.len());
169		Ok(MultiVersionBatch {
170			items,
171			has_more: false,
172		})
173	}
174
175	#[instrument(name = "flow::state::range", level = "debug", skip(self, range), fields(
176		node_id = id.0
177	))]
178	pub fn state_range_all(&mut self, id: FlowNodeId, range: EncodedKeyRange) -> Result<MultiVersionBatch> {
179		let prefixed_range = range.with_prefix(FlowNodeStateKey::encoded(id, vec![]));
180		let iter = self.range(prefixed_range, RangeScope::All, 1024);
181		let mut items = Vec::new();
182		for result in iter {
183			items.push(result?);
184		}
185		Ok(MultiVersionBatch {
186			items,
187			has_more: false,
188		})
189	}
190
191	#[instrument(name = "flow::internal_state::range", level = "debug", skip(self, range), fields(
192		node_id = id.0
193	))]
194	pub fn internal_state_range_all(
195		&mut self,
196		id: FlowNodeId,
197		range: EncodedKeyRange,
198	) -> Result<MultiVersionBatch> {
199		let prefixed_range = range.with_prefix(FlowNodeInternalStateKey::encoded(id, vec![]));
200		let iter = self.range(prefixed_range, RangeScope::All, 1024);
201		let mut items = Vec::new();
202		for result in iter {
203			items.push(result?);
204		}
205		Ok(MultiVersionBatch {
206			items,
207			has_more: false,
208		})
209	}
210
211	#[instrument(name = "flow::state::clear", level = "trace", skip(self), fields(
212		node_id = id.0,
213		keys_removed = field::Empty
214	))]
215	pub fn state_clear(&mut self, id: FlowNodeId) -> Result<()> {
216		let keys_to_remove = self.scan_keys_for_clear(id)?;
217
218		let count = keys_to_remove.len();
219		self.remove_keys(keys_to_remove)?;
220
221		Span::current().record("keys_removed", count);
222		Ok(())
223	}
224
225	#[inline]
226	#[instrument(name = "flow::state::clear::scan", level = "trace", skip(self), fields(node_id = id.0))]
227	fn scan_keys_for_clear(&mut self, id: FlowNodeId) -> Result<Vec<EncodedKey>> {
228		let range = FlowNodeStateKey::node_range(id);
229		let iter = self.range(range, RangeScope::All, 1024);
230		let mut keys = Vec::new();
231		for result in iter {
232			let multi = result?;
233			keys.push(multi.key);
234		}
235		Ok(keys)
236	}
237
238	#[inline]
239	#[instrument(name = "flow::state::clear::remove", level = "trace", skip(self, keys), fields(count = keys.len()))]
240	fn remove_keys(&mut self, keys: Vec<EncodedKey>) -> Result<()> {
241		for key in keys {
242			self.remove(&key)?;
243		}
244		Ok(())
245	}
246
247	#[instrument(name = "flow::state::load_or_create", level = "debug", skip(self, shape), fields(
248		node_id = id.0,
249		key_len = key.as_bytes().len(),
250		created
251	))]
252	pub fn load_or_create_row(&mut self, id: FlowNodeId, key: &EncodedKey, shape: &RowShape) -> Result<EncodedRow> {
253		match self.state_get(id, key)? {
254			Some(row) => {
255				Span::current().record("created", false);
256				Ok(row)
257			}
258			None => {
259				Span::current().record("created", true);
260				Ok(shape.allocate())
261			}
262		}
263	}
264
265	#[instrument(name = "flow::state::save", level = "trace", skip(self, row), fields(
266		node_id = id.0,
267		key_len = key.as_bytes().len()
268	))]
269	pub fn save_row(&mut self, id: FlowNodeId, key: &EncodedKey, row: EncodedRow) -> Result<()> {
270		self.state_set(id, key, row)
271	}
272
273	fn scoped_get(&mut self, scope: StateScope, id: FlowNodeId, key: &EncodedKey) -> Result<Option<EncodedRow>> {
274		let encoded_key = scope.encode(id, key);
275		self.get(&encoded_key)
276	}
277
278	fn scoped_get_many(
279		&mut self,
280		scope: StateScope,
281		id: FlowNodeId,
282		keys: &[EncodedKey],
283	) -> Result<MultiVersionBatch> {
284		let version = self.version();
285		let encoded: Vec<EncodedKey> = keys.iter().map(|key| scope.encode(id, key)).collect();
286
287		let mut items: Vec<MultiVersionRow> = Vec::new();
288		let mut to_batch: Vec<EncodedKey> = Vec::new();
289
290		for encoded_key in &encoded {
291			match self.lookup_overlays(encoded_key) {
292				Some(None) => continue,
293				Some(Some(row)) => items.push(MultiVersionRow {
294					key: encoded_key.clone(),
295					row,
296					version,
297				}),
298				None => to_batch.push(encoded_key.clone()),
299			}
300		}
301
302		self.fetch_external(&to_batch, &mut items)?;
303
304		Ok(MultiVersionBatch {
305			items,
306			has_more: false,
307		})
308	}
309
310	#[inline]
311	fn lookup_overlays(&self, encoded_key: &EncodedKey) -> Option<Option<EncodedRow>> {
312		let pending = {
313			let inner = self.inner();
314			if inner.pending.is_removed(encoded_key) {
315				Some(None)
316			} else {
317				inner.pending.get(encoded_key).map(|row| Some(row.clone()))
318			}
319		};
320		if pending.is_some() {
321			return pending;
322		}
323
324		if let Self::Transactional {
325			base_pending,
326			..
327		} = self
328		{
329			if base_pending.is_removed(encoded_key) {
330				Some(None)
331			} else {
332				base_pending.get(encoded_key).map(|row| Some(row.clone()))
333			}
334		} else {
335			None
336		}
337	}
338
339	#[inline]
340	fn fetch_external(&mut self, to_batch: &[EncodedKey], items: &mut Vec<MultiVersionRow>) -> Result<()> {
341		if to_batch.is_empty() {
342			return Ok(());
343		}
344
345		if let Self::Ephemeral {
346			inner,
347			state,
348		} = self
349		{
350			let version = inner.version;
351			for encoded_key in to_batch {
352				if let Some(row) = state.get(encoded_key) {
353					items.push(MultiVersionRow {
354						key: encoded_key.clone(),
355						row: row.clone(),
356						version,
357					});
358				}
359			}
360		} else {
361			let inner = self.inner_mut();
362			let found = inner.state_query.as_ref().unwrap().get_many(to_batch)?;
363			for encoded_key in to_batch {
364				if let Some(multi) = found.get(encoded_key) {
365					items.push(multi.clone());
366				}
367			}
368		}
369
370		Ok(())
371	}
372
373	fn scoped_set(
374		&mut self,
375		scope: StateScope,
376		id: FlowNodeId,
377		key: &EncodedKey,
378		mut value: EncodedRow,
379	) -> Result<()> {
380		let encoded_key = scope.encode(id, key);
381
382		if value.len() >= SHAPE_HEADER_SIZE
383			&& let Some(prior) = self.get(&encoded_key)?
384			&& prior.len() >= SHAPE_HEADER_SIZE
385		{
386			let prior_created = prior.created_at_nanos();
387			if prior_created != 0 {
388				let updated = value.updated_at_nanos();
389				value.set_timestamps(prior_created, updated);
390			}
391		}
392
393		self.set(&encoded_key, value)
394	}
395
396	fn scoped_remove(&mut self, scope: StateScope, id: FlowNodeId, key: &EncodedKey) -> Result<()> {
397		let encoded_key = scope.encode(id, key);
398		self.remove(&encoded_key)
399	}
400
401	fn scoped_drop(&mut self, scope: StateScope, id: FlowNodeId, key: &EncodedKey) -> Result<()> {
402		let encoded_key = scope.encode(id, key);
403		self.drop_key(&encoded_key)
404	}
405}
406
407#[cfg(test)]
408pub mod tests {
409	use std::{collections::Bound, sync::Arc};
410
411	use reifydb_catalog::catalog::Catalog;
412	use reifydb_codec::{
413		encoded::{row::EncodedRow, shape::RowShape},
414		key::encoded::{EncodedKey, EncodedKeyRange},
415	};
416	use reifydb_core::{actors::pending::Pending, common::CommitVersion, interface::catalog::flow::FlowNodeId};
417	use reifydb_engine::test_harness::TestEngine;
418	use reifydb_runtime::context::clock::{Clock, MockClock};
419	use reifydb_transaction::interceptor::interceptors::Interceptors;
420	use reifydb_value::{
421		util::cowvec::CowVec,
422		value::{identity::IdentityId, value_type::ValueType},
423	};
424
425	use super::*;
426	use crate::{
427		operator::stateful::test_utils::test::create_test_transaction,
428		transaction::{CommittingParams, DeferredParams, TransactionalParams, allocators::FlowAllocators},
429	};
430
431	fn commit_state_row(engine: &TestEngine, node: FlowNodeId, key: &EncodedKey, row: EncodedRow) -> CommitVersion {
432		let mut cmd = engine.begin_command(IdentityId::system()).unwrap();
433		cmd.disable_conflict_tracking().unwrap();
434		cmd.set(&FlowNodeStateKey::new(node, key.as_ref().to_vec()).encode(), row).unwrap();
435		cmd.commit_unchecked().unwrap()
436	}
437
438	fn make_key(s: &str) -> EncodedKey {
439		EncodedKey::new(s.as_bytes().to_vec())
440	}
441
442	fn make_value(s: &str) -> EncodedRow {
443		EncodedRow(CowVec::new(s.as_bytes().to_vec()))
444	}
445
446	#[test]
447	fn test_state_get_set() {
448		let parent = create_test_transaction();
449		let mut txn = FlowTransaction::deferred(
450			&parent,
451			CommitVersion(1),
452			Catalog::testing(),
453			Interceptors::new(),
454			Clock::Mock(MockClock::from_millis(1000)),
455		);
456
457		let node_id = FlowNodeId(1);
458		let key = make_key("state_key");
459		let value = make_value("state_value");
460
461		// Set state
462		txn.state_set(node_id, &key, value.clone()).unwrap();
463
464		// Get state back
465		let result = txn.state_get(node_id, &key).unwrap();
466		assert_eq!(result, Some(value));
467	}
468
469	#[test]
470	fn test_internal_state_get_many() {
471		let parent = create_test_transaction();
472		let mut txn = FlowTransaction::deferred(
473			&parent,
474			CommitVersion(1),
475			Catalog::testing(),
476			Interceptors::new(),
477			Clock::Mock(MockClock::from_millis(1000)),
478		);
479
480		let node_id = FlowNodeId(1);
481		txn.internal_state_set(node_id, &make_key("a"), make_value("1")).unwrap();
482		txn.internal_state_set(node_id, &make_key("b"), make_value("2")).unwrap();
483
484		// A data-state key sharing the name must not leak into the internal batch read:
485		// the two namespaces use different envelopes.
486		txn.state_set(node_id, &make_key("a"), make_value("data")).unwrap();
487
488		let batch = txn
489			.internal_state_get_many(node_id, &[make_key("a"), make_key("b"), make_key("missing")])
490			.unwrap();
491
492		// Missing key is omitted; present keys come back under the internal envelope.
493		assert_eq!(batch.items.len(), 2);
494		let mut decoded: Vec<(Vec<u8>, EncodedRow)> = batch
495			.items
496			.iter()
497			.map(|item| (FlowNodeInternalStateKey::decode(&item.key).unwrap().key, item.row.clone()))
498			.collect();
499		decoded.sort_by(|a, b| a.0.cmp(&b.0));
500		assert_eq!(decoded[0], (b"a".to_vec(), make_value("1")));
501		assert_eq!(decoded[1], (b"b".to_vec(), make_value("2")));
502	}
503
504	#[test]
505	fn test_state_get_nonexistent() {
506		let parent = create_test_transaction();
507		let mut txn = FlowTransaction::deferred(
508			&parent,
509			CommitVersion(1),
510			Catalog::testing(),
511			Interceptors::new(),
512			Clock::Mock(MockClock::from_millis(1000)),
513		);
514
515		let node_id = FlowNodeId(1);
516		let key = make_key("missing");
517
518		let result = txn.state_get(node_id, &key).unwrap();
519		assert_eq!(result, None);
520	}
521
522	#[test]
523	fn test_state_remove() {
524		let parent = create_test_transaction();
525		let mut txn = FlowTransaction::deferred(
526			&parent,
527			CommitVersion(1),
528			Catalog::testing(),
529			Interceptors::new(),
530			Clock::Mock(MockClock::from_millis(1000)),
531		);
532
533		let node_id = FlowNodeId(1);
534		let key = make_key("state_key");
535		let value = make_value("state_value");
536
537		// Set then remove
538		txn.state_set(node_id, &key, value.clone()).unwrap();
539		assert_eq!(txn.state_get(node_id, &key).unwrap(), Some(value));
540
541		txn.state_remove(node_id, &key).unwrap();
542		assert_eq!(txn.state_get(node_id, &key).unwrap(), None);
543	}
544
545	#[test]
546	fn test_state_isolation_between_nodes() {
547		let parent = create_test_transaction();
548		let mut txn = FlowTransaction::deferred(
549			&parent,
550			CommitVersion(1),
551			Catalog::testing(),
552			Interceptors::new(),
553			Clock::Mock(MockClock::from_millis(1000)),
554		);
555
556		let node1 = FlowNodeId(1);
557		let node2 = FlowNodeId(2);
558		let key = make_key("same_key");
559
560		txn.state_set(node1, &key, make_value("node1_value")).unwrap();
561		txn.state_set(node2, &key, make_value("node2_value")).unwrap();
562
563		// Each node should have its own value
564		assert_eq!(txn.state_get(node1, &key).unwrap(), Some(make_value("node1_value")));
565		assert_eq!(txn.state_get(node2, &key).unwrap(), Some(make_value("node2_value")));
566	}
567
568	#[test]
569	fn test_state_scan_all() {
570		let parent = create_test_transaction();
571		let mut txn = FlowTransaction::deferred(
572			&parent,
573			CommitVersion(1),
574			Catalog::testing(),
575			Interceptors::new(),
576			Clock::Mock(MockClock::from_millis(1000)),
577		);
578
579		let node_id = FlowNodeId(1);
580
581		txn.state_set(node_id, &make_key("key1"), make_value("value1")).unwrap();
582		txn.state_set(node_id, &make_key("key2"), make_value("value2")).unwrap();
583		txn.state_set(node_id, &make_key("key3"), make_value("value3")).unwrap();
584
585		let iter = txn.state_scan_all(node_id).unwrap();
586		let items: Vec<_> = iter.items.into_iter().collect();
587
588		assert_eq!(items.len(), 3);
589	}
590
591	#[test]
592	fn test_state_scan_only_own_node() {
593		let parent = create_test_transaction();
594		let mut txn = FlowTransaction::deferred(
595			&parent,
596			CommitVersion(1),
597			Catalog::testing(),
598			Interceptors::new(),
599			Clock::Mock(MockClock::from_millis(1000)),
600		);
601
602		let node1 = FlowNodeId(1);
603		let node2 = FlowNodeId(2);
604
605		txn.state_set(node1, &make_key("key1"), make_value("value1")).unwrap();
606		txn.state_set(node1, &make_key("key2"), make_value("value2")).unwrap();
607		txn.state_set(node2, &make_key("key3"), make_value("value3")).unwrap();
608
609		// Scan node1 should only return node1's state
610		let items: Vec<_> = txn.state_scan_all(node1).unwrap().items.into_iter().collect();
611		assert_eq!(items.len(), 2);
612
613		// Scan node2 should only return node2's state
614		let items: Vec<_> = txn.state_scan_all(node2).unwrap().items.into_iter().collect();
615		assert_eq!(items.len(), 1);
616	}
617
618	#[test]
619	fn test_state_scan_empty() {
620		let parent = create_test_transaction();
621		let mut txn = FlowTransaction::deferred(
622			&parent,
623			CommitVersion(1),
624			Catalog::testing(),
625			Interceptors::new(),
626			Clock::Mock(MockClock::from_millis(1000)),
627		);
628
629		let node_id = FlowNodeId(1);
630
631		let iter = txn.state_scan_all(node_id).unwrap();
632		assert!(iter.items.into_iter().next().is_none());
633	}
634
635	#[test]
636	fn test_state_range_all() {
637		let parent = create_test_transaction();
638		let mut txn = FlowTransaction::deferred(
639			&parent,
640			CommitVersion(1),
641			Catalog::testing(),
642			Interceptors::new(),
643			Clock::Mock(MockClock::from_millis(1000)),
644		);
645
646		let node_id = FlowNodeId(1);
647
648		txn.state_set(node_id, &make_key("a"), make_value("1")).unwrap();
649		txn.state_set(node_id, &make_key("b"), make_value("2")).unwrap();
650		txn.state_set(node_id, &make_key("c"), make_value("3")).unwrap();
651		txn.state_set(node_id, &make_key("d"), make_value("4")).unwrap();
652
653		// Range query from "b" to "d" (exclusive)
654		let range = EncodedKeyRange::new(Bound::Included(make_key("b")), Bound::Excluded(make_key("d")));
655		let iter = txn.state_range_all(node_id, range).unwrap();
656		let items: Vec<_> = iter.items.into_iter().collect();
657
658		// Should only include "b" and "c"
659		assert_eq!(items.len(), 2);
660	}
661
662	#[test]
663	fn test_state_clear() {
664		let parent = create_test_transaction();
665		let mut txn = FlowTransaction::deferred(
666			&parent,
667			CommitVersion(1),
668			Catalog::testing(),
669			Interceptors::new(),
670			Clock::Mock(MockClock::from_millis(1000)),
671		);
672
673		let node_id = FlowNodeId(1);
674
675		txn.state_set(node_id, &make_key("key1"), make_value("value1")).unwrap();
676		txn.state_set(node_id, &make_key("key2"), make_value("value2")).unwrap();
677		txn.state_set(node_id, &make_key("key3"), make_value("value3")).unwrap();
678
679		// Verify state exists
680		assert_eq!(txn.state_scan_all(node_id).unwrap().items.into_iter().count(), 3);
681
682		// Clear all state
683		txn.state_clear(node_id).unwrap();
684
685		// Verify state is empty
686		assert_eq!(txn.state_scan_all(node_id).unwrap().items.into_iter().count(), 0);
687	}
688
689	#[test]
690	fn test_state_clear_only_own_node() {
691		let parent = create_test_transaction();
692		let mut txn = FlowTransaction::deferred(
693			&parent,
694			CommitVersion(1),
695			Catalog::testing(),
696			Interceptors::new(),
697			Clock::Mock(MockClock::from_millis(1000)),
698		);
699
700		let node1 = FlowNodeId(1);
701		let node2 = FlowNodeId(2);
702
703		txn.state_set(node1, &make_key("key1"), make_value("value1")).unwrap();
704		txn.state_set(node1, &make_key("key2"), make_value("value2")).unwrap();
705		txn.state_set(node2, &make_key("key3"), make_value("value3")).unwrap();
706
707		// Clear node1
708		txn.state_clear(node1).unwrap();
709
710		// Node1 should be empty
711		assert_eq!(txn.state_scan_all(node1).unwrap().items.into_iter().count(), 0);
712
713		// Node2 should still have state
714		assert_eq!(txn.state_scan_all(node2).unwrap().items.into_iter().count(), 1);
715	}
716
717	#[test]
718	fn test_state_clear_empty_node() {
719		let parent = create_test_transaction();
720		let mut txn = FlowTransaction::deferred(
721			&parent,
722			CommitVersion(1),
723			Catalog::testing(),
724			Interceptors::new(),
725			Clock::Mock(MockClock::from_millis(1000)),
726		);
727
728		let node_id = FlowNodeId(1);
729
730		// Clear on empty node should not error
731		txn.state_clear(node_id).unwrap();
732	}
733
734	#[test]
735	fn test_load_or_create_existing() {
736		let parent = create_test_transaction();
737		let mut txn = FlowTransaction::deferred(
738			&parent,
739			CommitVersion(1),
740			Catalog::testing(),
741			Interceptors::new(),
742			Clock::Mock(MockClock::from_millis(1000)),
743		);
744
745		let node_id = FlowNodeId(1);
746		let key = make_key("key1");
747		let value = make_value("existing");
748		let shape = RowShape::testing(&[ValueType::Int8, ValueType::Float8]);
749
750		// Set existing state
751		txn.state_set(node_id, &key, value.clone()).unwrap();
752
753		// load_or_create should return existing value
754		let result = txn.load_or_create_row(node_id, &key, &shape).unwrap();
755		assert_eq!(result, value);
756	}
757
758	#[test]
759	fn test_load_or_create_new() {
760		let parent = create_test_transaction();
761		let mut txn = FlowTransaction::deferred(
762			&parent,
763			CommitVersion(1),
764			Catalog::testing(),
765			Interceptors::new(),
766			Clock::Mock(MockClock::from_millis(1000)),
767		);
768
769		let node_id = FlowNodeId(1);
770		let key = make_key("key1");
771		let shape = RowShape::testing(&[ValueType::Int8, ValueType::Float8]);
772
773		// load_or_create should allocate new row
774		let result = txn.load_or_create_row(node_id, &key, &shape).unwrap();
775
776		// Result should be a newly allocated row (shape.allocate())
777		assert!(!result.is_empty());
778	}
779
780	#[test]
781	fn test_save_row() {
782		let parent = create_test_transaction();
783		let mut txn = FlowTransaction::deferred(
784			&parent,
785			CommitVersion(1),
786			Catalog::testing(),
787			Interceptors::new(),
788			Clock::Mock(MockClock::from_millis(1000)),
789		);
790
791		let node_id = FlowNodeId(1);
792		let key = make_key("key1");
793		let row = make_value("row_data");
794
795		txn.save_row(node_id, &key, row.clone()).unwrap();
796
797		// Verify saved
798		let result = txn.state_get(node_id, &key).unwrap();
799		assert_eq!(result, Some(row));
800	}
801
802	#[test]
803	fn test_state_multiple_nodes() {
804		let parent = create_test_transaction();
805		let mut txn = FlowTransaction::deferred(
806			&parent,
807			CommitVersion(1),
808			Catalog::testing(),
809			Interceptors::new(),
810			Clock::Mock(MockClock::from_millis(1000)),
811		);
812
813		let node1 = FlowNodeId(1);
814		let node2 = FlowNodeId(2);
815		let node3 = FlowNodeId(3);
816
817		txn.state_set(node1, &make_key("a"), make_value("n1_a")).unwrap();
818		txn.state_set(node1, &make_key("b"), make_value("n1_b")).unwrap();
819		txn.state_set(node2, &make_key("a"), make_value("n2_a")).unwrap();
820		txn.state_set(node3, &make_key("c"), make_value("n3_c")).unwrap();
821
822		// Verify each node has correct state
823		assert_eq!(txn.state_get(node1, &make_key("a")).unwrap(), Some(make_value("n1_a")));
824		assert_eq!(txn.state_get(node1, &make_key("b")).unwrap(), Some(make_value("n1_b")));
825		assert_eq!(txn.state_get(node2, &make_key("a")).unwrap(), Some(make_value("n2_a")));
826		assert_eq!(txn.state_get(node3, &make_key("c")).unwrap(), Some(make_value("n3_c")));
827
828		// Cross-node keys should not exist
829		assert_eq!(txn.state_get(node2, &make_key("b")).unwrap(), None);
830		assert_eq!(txn.state_get(node3, &make_key("a")).unwrap(), None);
831	}
832
833	#[test]
834	fn deferred_read_sees_state_committed_above_primitive_version() {
835		// A deferred consume's operator-state reads must observe the latest committed
836		// snapshot, not be bounded to the consume's own input (primitive) version. A
837		// prior consume's accumulated join state is committed at that consume's COMMIT
838		// version, which is strictly greater than any input data version. If a later
839		// consume read operator state bounded to its own lower primitive_version, the
840		// other side of a join written by the prior consume would be invisible and the
841		// row would wrongly emit an unmatched (null) result. This pins that invariant
842		// (it is the root cause of the deferred left-join null-match flake).
843		let engine = TestEngine::new();
844		let node_id = FlowNodeId(1);
845		let inner_key = make_key("late_right_side");
846		let value = make_value("matched_row");
847
848		// The primitive (input) version we will read at. Two further commits then push
849		// the operator-state write strictly more than one version above it, so the read
850		// bound (which resolves to primitive_version + 1) cannot reach it on its own.
851		let primitive_version = commit_state_row(&engine, node_id, &make_key("warmup_a"), make_value("a"));
852		commit_state_row(&engine, node_id, &make_key("warmup_b"), make_value("b"));
853		let committed_at = commit_state_row(&engine, node_id, &inner_key, value.clone());
854		assert!(
855			committed_at.0 >= primitive_version.0 + 2,
856			"operator state must commit at least two versions above the primitive version: committed_at={committed_at:?} primitive_version={primitive_version:?}"
857		);
858
859		let (state_version, lease) = engine.acquire_current_snapshot_lease().unwrap();
860		assert!(state_version >= committed_at);
861
862		let query = engine.multi().begin_query_at_version(&lease).unwrap();
863		let state_query = engine.multi().begin_query_at_version(&lease).unwrap();
864		let mut txn = FlowTransaction::deferred_from_parts(DeferredParams {
865			version: primitive_version,
866			pending: Pending::new(),
867			query,
868			state_query,
869			dictionary_query: None,
870			single: engine.single().clone(),
871			catalog: Catalog::testing(),
872			interceptors: engine.create_interceptors(),
873			clock: engine.clock().clone(),
874			allocators: FlowAllocators::new(),
875		});
876
877		let batch = txn.state_get_many(node_id, &[inner_key]).unwrap();
878		assert_eq!(
879			batch.items.len(),
880			1,
881			"operator state committed at {committed_at:?} (above primitive_version {primitive_version:?}) must be visible to a deferred read"
882		);
883		assert_eq!(batch.items[0].row, value);
884	}
885
886	#[test]
887	fn committing_persists_state_writes_and_keeps_prior_state() {
888		// The committing variant wraps the command being committed: its state writes route
889		// to that command (state_set -> cmd, not the in-memory pending) and become durable
890		// when the flow commits, alongside any state committed by prior transactions. This
891		// guards the committing write+commit path the transactional tick relies on; a
892		// regression that dropped these writes or failed to persist them would be caught.
893		let engine = TestEngine::new();
894		let node_id = FlowNodeId(1);
895		let prior_key = make_key("prior");
896		let prior_value = make_value("prior_value");
897		commit_state_row(&engine, node_id, &prior_key, prior_value.clone());
898
899		let written_key = make_key("written_by_tick");
900		let written_value = make_value("tick_value");
901		{
902			let cmd = engine.begin_command(IdentityId::system()).unwrap();
903			let mut txn = FlowTransaction::committing(CommittingParams {
904				cmd,
905				catalog: Catalog::testing(),
906				interceptors: engine.create_interceptors(),
907				clock: engine.clock().clone(),
908				allocators: FlowAllocators::new(),
909			})
910			.unwrap();
911			txn.state_set(node_id, &written_key, written_value.clone()).unwrap();
912			txn.commit().unwrap();
913		}
914
915		// After the committing flow commits, both the prior state and the state it wrote
916		// are durable and observable at the latest snapshot.
917		let (_version, lease) = engine.acquire_current_snapshot_lease().unwrap();
918		let query = engine.multi().begin_query_at_version(&lease).unwrap();
919		let prior_encoded = FlowNodeStateKey::new(node_id, prior_key.as_ref().to_vec()).encode();
920		let written_encoded = FlowNodeStateKey::new(node_id, written_key.as_ref().to_vec()).encode();
921		let found = query.get_many(&[prior_encoded.clone(), written_encoded.clone()]).unwrap();
922		assert_eq!(
923			found.len(),
924			2,
925			"the committing flow's write and the prior committed state must both be durable after commit"
926		);
927		assert_eq!(found.get(&prior_encoded).unwrap().row, prior_value);
928		assert_eq!(found.get(&written_encoded).unwrap().row, written_value);
929	}
930
931	#[test]
932	fn transactional_read_sees_committed_state_below_version_and_base_pending() {
933		// The transactional variant reads committed operator state via state_query (opened
934		// at the latest snapshot by the interceptor) plus a base_pending overlay for the
935		// current transaction's own writes. Its state read must NOT be bounded to the txn
936		// `version`: here `version` is set below the committed state, which must still be
937		// visible. This is the exact situation that broke the deferred path; this guards
938		// the transactional path against the same version-bounding regression.
939		let engine = TestEngine::new();
940		let node_id = FlowNodeId(1);
941		let committed_key = make_key("committed");
942		let committed_value = make_value("committed_value");
943
944		let low_version = commit_state_row(&engine, node_id, &make_key("warmup"), make_value("w"));
945		commit_state_row(&engine, node_id, &make_key("bump"), make_value("bump"));
946		let committed_at = commit_state_row(&engine, node_id, &committed_key, committed_value.clone());
947		assert!(
948			committed_at.0 >= low_version.0 + 2,
949			"committed state must land at least two versions above the txn version so a wrongful bound (which resolves to version + 1) would hide it: committed_at={committed_at:?} low_version={low_version:?}"
950		);
951
952		let base_key = make_key("in_flight");
953		let base_value = make_value("in_flight_value");
954		let mut base_pending = Pending::new();
955		base_pending.insert(
956			FlowNodeStateKey::new(node_id, base_key.as_ref().to_vec()).encode(),
957			base_value.clone(),
958		);
959
960		let mut txn = FlowTransaction::transactional(TransactionalParams {
961			version: low_version,
962			pending: Pending::new(),
963			base_pending,
964			query: engine.multi().begin_query().unwrap(),
965			state_query: engine.multi().begin_query().unwrap(),
966			single: engine.single().clone(),
967			catalog: Catalog::testing(),
968			interceptors: engine.create_interceptors(),
969			clock: engine.clock().clone(),
970			view_overlay: Arc::new(Vec::new()),
971			allocators: FlowAllocators::new(),
972		});
973
974		// Committed state above the txn version is visible (state_query is at the snapshot).
975		let committed = txn.state_get_many(node_id, &[committed_key]).unwrap();
976		assert_eq!(
977			committed.items.len(),
978			1,
979			"committed state at {committed_at:?} must be visible even though the txn version is {low_version:?}"
980		);
981		assert_eq!(committed.items[0].row, committed_value);
982
983		// base_pending (the current transaction's writes) is visible via the overlay.
984		let base = txn.state_get_many(node_id, &[base_key]).unwrap();
985		assert_eq!(base.items.len(), 1);
986		assert_eq!(base.items[0].row, base_value);
987	}
988
989	#[test]
990	fn ephemeral_read_sees_state_map_and_pending() {
991		// The ephemeral variant has no state_query; it serves operator-state reads from an
992		// in-memory state map (its seeded prior state) with the pending overlay on top.
993		// Guards that both the seeded map and live writes are read back.
994		let engine = TestEngine::new();
995		let node_id = FlowNodeId(1);
996		let seeded_key = make_key("seeded");
997		let seeded_value = make_value("seeded_value");
998
999		let mut state = HashMap::new();
1000		state.insert(
1001			FlowNodeStateKey::new(node_id, seeded_key.as_ref().to_vec()).encode(),
1002			seeded_value.clone(),
1003		);
1004
1005		let mut txn = FlowTransaction::ephemeral(
1006			CommitVersion(1),
1007			engine.multi().begin_query().unwrap(),
1008			engine.single().clone(),
1009			Catalog::testing(),
1010			state,
1011			engine.clock().clone(),
1012		);
1013
1014		let seeded = txn.state_get_many(node_id, &[seeded_key]).unwrap();
1015		assert_eq!(seeded.items.len(), 1, "seeded ephemeral state must be readable");
1016		assert_eq!(seeded.items[0].row, seeded_value);
1017
1018		// A live write is visible via the pending overlay.
1019		let live_key = make_key("live");
1020		let live_value = make_value("live_value");
1021		txn.state_set(node_id, &live_key, live_value.clone()).unwrap();
1022		let live = txn.state_get_many(node_id, &[live_key]).unwrap();
1023		assert_eq!(live.items.len(), 1);
1024		assert_eq!(live.items[0].row, live_value);
1025	}
1026}