Skip to main content

reifydb_store_multi/store/
multi.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	collections::{BTreeMap, HashMap, HashSet},
6	ops::{Bound, RangeBounds},
7};
8
9use reifydb_core::{
10	actors::drop::{DropMessage, DropRequest},
11	common::CommitVersion,
12	delta::Delta,
13	encoded::{
14		key::{EncodedKey, EncodedKeyRange},
15		row::EncodedRow,
16	},
17	event::metric::{MultiCommittedEvent, MultiDelete, MultiWrite},
18	interface::store::{
19		EntryKind, MultiVersionBatch, MultiVersionCommit, MultiVersionContains, MultiVersionGet,
20		MultiVersionGetPrevious, MultiVersionRow, MultiVersionStore, classify_key, classify_range,
21		is_single_version_semantics_key,
22	},
23};
24use reifydb_store::row::page::PageId;
25use reifydb_value::{
26	reifydb_assertions,
27	util::{cowvec::CowVec, hex},
28};
29use tracing::{instrument, warn};
30
31use super::StandardMultiStore;
32use crate::{
33	MultiVersionScope, Result,
34	tier::{
35		RangeBatch, RangeCursor, TierBatch, TierStorage, VersionedGetResult,
36		commit::buffer::MultiCommitBufferTier,
37		persistent::MultiPersistentTier,
38		read::{MultiReadBufferTier, ServedChunk},
39	},
40};
41
42const TIER_SCAN_CHUNK_SIZE: usize = 32;
43
44pub(crate) const WARM_THRESHOLD: u64 = 4 * TIER_SCAN_CHUNK_SIZE as u64;
45
46impl MultiVersionGet for StandardMultiStore {
47	fn get(&self, key: &EncodedKey, version: CommitVersion) -> Result<Option<MultiVersionRow>> {
48		match classify_key(key) {
49			EntryKind::Operator(_) => self.get_operator(key, version),
50			EntryKind::Source(_) => self.get_source(key, version),
51			_ => self.get_multi(key, version),
52		}
53	}
54}
55
56impl StandardMultiStore {
57	#[instrument(name = "store::multi::get::operator", level = "trace", skip(self, key), fields(version = version.0))]
58	fn get_operator(&self, key: &EncodedKey, version: CommitVersion) -> Result<Option<MultiVersionRow>> {
59		self.get_impl(key, version)
60	}
61
62	#[instrument(name = "store::multi::get::source", level = "trace", skip(self, key), fields(version = version.0))]
63	fn get_source(&self, key: &EncodedKey, version: CommitVersion) -> Result<Option<MultiVersionRow>> {
64		self.get_impl(key, version)
65	}
66
67	#[instrument(name = "store::multi::get::multi", level = "trace", skip(self, key), fields(version = version.0))]
68	fn get_multi(&self, key: &EncodedKey, version: CommitVersion) -> Result<Option<MultiVersionRow>> {
69		self.get_impl(key, version)
70	}
71
72	#[inline]
73	fn get_impl(&self, key: &EncodedKey, version: CommitVersion) -> Result<Option<MultiVersionRow>> {
74		let table = classify_key(key);
75
76		if let Some(found) = self.get_probe_commit(table, key, version)? {
77			return Ok(found);
78		}
79		if let Some(found) = self.get_probe_read(key, version) {
80			return Ok(found);
81		}
82		if let Some(found) = self.get_probe_persistent(table, key, version)? {
83			return Ok(found);
84		}
85
86		Ok(None)
87	}
88}
89
90impl StandardMultiStore {
91	#[inline]
92	fn get_probe_commit(
93		&self,
94		table: EntryKind,
95		key: &EncodedKey,
96		version: CommitVersion,
97	) -> Result<Option<Option<MultiVersionRow>>> {
98		let Some(commit) = &self.commit else {
99			return Ok(None);
100		};
101		Ok(match commit.get(table, key.as_ref(), version)? {
102			VersionedGetResult::Value {
103				value,
104				version: v,
105			} => Some(Some(MultiVersionRow {
106				key: key.clone(),
107				row: EncodedRow(value),
108				version: v,
109			})),
110			VersionedGetResult::Tombstone => Some(None),
111			VersionedGetResult::NotFound => None,
112		})
113	}
114
115	#[inline]
116	fn get_probe_read(&self, key: &EncodedKey, version: CommitVersion) -> Option<Option<MultiVersionRow>> {
117		let read = self.read.as_ref()?;
118		match read.get(key, version) {
119			VersionedGetResult::Value {
120				value,
121				version: v,
122			} => Some(Some(MultiVersionRow {
123				key: key.clone(),
124				row: EncodedRow(value),
125				version: v,
126			})),
127			VersionedGetResult::Tombstone => Some(None),
128			VersionedGetResult::NotFound => None,
129		}
130	}
131
132	#[inline]
133	fn get_probe_persistent(
134		&self,
135		table: EntryKind,
136		key: &EncodedKey,
137		version: CommitVersion,
138	) -> Result<Option<Option<MultiVersionRow>>> {
139		let Some(persistent) = &self.persistent else {
140			return Ok(None);
141		};
142		Ok(match persistent.get(table, key.as_ref(), version)? {
143			VersionedGetResult::Value {
144				value,
145				version: v,
146			} => {
147				if let Some(read) = &self.read {
148					read.insert(key.clone(), v, Some(value.clone()));
149				}
150				Some(Some(MultiVersionRow {
151					key: key.clone(),
152					row: EncodedRow(value),
153					version: v,
154				}))
155			}
156			VersionedGetResult::Tombstone => Some(None),
157			VersionedGetResult::NotFound => None,
158		})
159	}
160}
161
162impl MultiVersionContains for StandardMultiStore {
163	#[instrument(name = "store::multi::contains", level = "trace", skip(self), fields(key_hex = %hex::display(key.as_ref()), version = version.0), ret)]
164	fn contains(&self, key: &EncodedKey, version: CommitVersion) -> Result<bool> {
165		Ok(MultiVersionGet::get(self, key, version)?.is_some())
166	}
167}
168
169impl MultiVersionCommit for StandardMultiStore {
170	#[instrument(name = "store::multi::commit", level = "debug", skip(self, deltas), fields(delta_count = deltas.len(), version = version.0))]
171	fn commit(&self, deltas: CowVec<Delta>, version: CommitVersion) -> Result<()> {
172		let classified = classify_deltas(&deltas);
173
174		let (operator_drops, source_drops) = partition_drops(classified.explicit_drops);
175		self.dispatch_drops(build_drop_batch(source_drops, &classified.pending_set_keys, version));
176
177		self.update_read_cache_on_commit(version, &classified.batches);
178
179		if !self.write_batches(version, classified.batches)? {
180			return Ok(());
181		}
182
183		self.evict_operator_state(&operator_drops)?;
184		self.emit_commit_metrics(classified.writes, classified.deletes, version);
185
186		Ok(())
187	}
188}
189
190type DropPartition = (Vec<(EntryKind, EncodedKey)>, Vec<(EntryKind, EncodedKey)>);
191
192#[inline]
193fn partition_drops(explicit_drops: Vec<(EntryKind, EncodedKey)>) -> DropPartition {
194	explicit_drops.into_iter().partition(|(table, _)| matches!(table, EntryKind::Operator(_)))
195}
196
197struct ClassifiedDeltas {
198	pending_set_keys: HashSet<EncodedKey>,
199	writes: Vec<MultiWrite>,
200	deletes: Vec<MultiDelete>,
201	batches: TierBatch,
202	explicit_drops: Vec<(EntryKind, EncodedKey)>,
203}
204
205#[inline]
206fn classify_deltas(deltas: &CowVec<Delta>) -> ClassifiedDeltas {
207	let mut pending_set_keys: HashSet<EncodedKey> = HashSet::new();
208	let mut writes: Vec<MultiWrite> = Vec::new();
209	let mut deletes: Vec<MultiDelete> = Vec::new();
210	let mut batches: TierBatch = HashMap::new();
211	let mut explicit_drops: Vec<(EntryKind, EncodedKey)> = Vec::new();
212
213	for delta in deltas.iter() {
214		let key = delta.key();
215		let table = classify_key(key);
216		let is_single_version = is_single_version_semantics_key(key);
217
218		match delta {
219			Delta::Set {
220				key,
221				row,
222			} => {
223				if is_single_version {
224					pending_set_keys.insert(key.clone());
225				}
226				writes.push(MultiWrite {
227					key: key.clone(),
228					value_bytes: row.len() as u64,
229				});
230				batches.entry(table).or_default().push((key.clone(), Some(row.0.clone())));
231			}
232			Delta::Unset {
233				key,
234				row,
235			} => {
236				deletes.push(MultiDelete {
237					key: key.clone(),
238					value_bytes: row.len() as u64,
239				});
240				batches.entry(table).or_default().push((key.clone(), None));
241			}
242			Delta::Remove {
243				key,
244			} => {
245				deletes.push(MultiDelete {
246					key: key.clone(),
247					value_bytes: 0,
248				});
249				batches.entry(table).or_default().push((key.clone(), None));
250			}
251			Delta::Drop {
252				key,
253			} => {
254				explicit_drops.push((table, key.clone()));
255			}
256		}
257	}
258
259	ClassifiedDeltas {
260		pending_set_keys,
261		writes,
262		deletes,
263		batches,
264		explicit_drops,
265	}
266}
267
268#[inline]
269fn build_drop_batch(
270	explicit_drops: Vec<(EntryKind, EncodedKey)>,
271	pending_set_keys: &HashSet<EncodedKey>,
272	version: CommitVersion,
273) -> Vec<DropRequest> {
274	let mut drop_batch = Vec::with_capacity(explicit_drops.len() + pending_set_keys.len());
275	for (table, key) in explicit_drops {
276		let pending_version = if pending_set_keys.contains(key.as_ref()) {
277			Some(version)
278		} else {
279			None
280		};
281		drop_batch.push(DropRequest {
282			table,
283			key,
284			commit_version: version,
285			pending_version,
286		});
287	}
288	for key in pending_set_keys.iter() {
289		let encoded = EncodedKey::new(key.to_vec());
290		let table = classify_key(&encoded);
291		drop_batch.push(DropRequest {
292			table,
293			key: encoded,
294			commit_version: version,
295			pending_version: Some(version),
296		});
297	}
298	drop_batch
299}
300
301impl StandardMultiStore {
302	pub fn get_many(
303		&self,
304		keys: &[EncodedKey],
305		version: CommitVersion,
306	) -> Result<HashMap<EncodedKey, MultiVersionRow>> {
307		let mut by_table: HashMap<EntryKind, Vec<&EncodedKey>> = HashMap::new();
308		for key in keys {
309			by_table.entry(classify_key(key)).or_default().push(key);
310		}
311
312		let mut out: HashMap<EncodedKey, MultiVersionRow> = HashMap::new();
313		for (table, table_keys) in by_table {
314			self.get_many_for_table(table, &table_keys, version, &mut out)?;
315		}
316
317		Ok(out)
318	}
319
320	#[inline]
321	fn get_many_for_table(
322		&self,
323		table: EntryKind,
324		table_keys: &[&EncodedKey],
325		version: CommitVersion,
326		out: &mut HashMap<EncodedKey, MultiVersionRow>,
327	) -> Result<()> {
328		let key_slices: Vec<&[u8]> = table_keys.iter().map(|k| k.as_ref()).collect();
329
330		let commit_results = self.probe_commit_batch(table, &key_slices, version)?;
331		let (read_aligned, persistent_aligned) = self.resolve_misses_through_read_and_persistent(
332			table,
333			table_keys,
334			&key_slices,
335			&commit_results,
336			version,
337		)?;
338
339		reifydb_assertions! {
340			let n = key_slices.len();
341			assert!(
342				commit_results.len() == n && read_aligned.len() == n && persistent_aligned.len() == n,
343				"per-tier result vectors must stay index-aligned with the table's keys, otherwise collect_resolved_rows \
344				 reads a tier result for the wrong key and returns mismatched rows (keys={n}, commit={}, read={}, persistent={})",
345				commit_results.len(),
346				read_aligned.len(),
347				persistent_aligned.len()
348			);
349		}
350
351		self.collect_resolved_rows(table_keys, &commit_results, &read_aligned, &persistent_aligned, out);
352		Ok(())
353	}
354
355	#[inline]
356	fn probe_commit_batch(
357		&self,
358		table: EntryKind,
359		key_slices: &[&[u8]],
360		version: CommitVersion,
361	) -> Result<Vec<VersionedGetResult>> {
362		match &self.commit {
363			Some(commit) => commit.get_many(table, key_slices, version),
364			None => Ok(vec![VersionedGetResult::NotFound; key_slices.len()]),
365		}
366	}
367
368	#[inline]
369	fn resolve_misses_through_read_and_persistent(
370		&self,
371		table: EntryKind,
372		table_keys: &[&EncodedKey],
373		key_slices: &[&[u8]],
374		commit_results: &[VersionedGetResult],
375		version: CommitVersion,
376	) -> Result<(Vec<VersionedGetResult>, Vec<VersionedGetResult>)> {
377		let mut read_aligned = vec![VersionedGetResult::NotFound; key_slices.len()];
378		let mut persistent_idx: Vec<usize> = Vec::new();
379		let mut persistent_slices: Vec<&[u8]> = Vec::new();
380		for (i, result) in commit_results.iter().enumerate() {
381			if !matches!(result, VersionedGetResult::NotFound) {
382				continue;
383			}
384			let read_hit = self
385				.read
386				.as_ref()
387				.map(|c| c.get(table_keys[i], version))
388				.unwrap_or(VersionedGetResult::NotFound);
389			match read_hit {
390				VersionedGetResult::Value {
391					value,
392					version: v,
393				} => {
394					read_aligned[i] = VersionedGetResult::Value {
395						value,
396						version: v,
397					};
398				}
399				VersionedGetResult::Tombstone => {
400					read_aligned[i] = VersionedGetResult::Tombstone;
401				}
402				VersionedGetResult::NotFound => {
403					persistent_idx.push(i);
404					persistent_slices.push(key_slices[i]);
405				}
406			}
407		}
408
409		let mut persistent_aligned = vec![VersionedGetResult::NotFound; key_slices.len()];
410		if !persistent_slices.is_empty()
411			&& let Some(persistent) = &self.persistent
412		{
413			let persistent_results = persistent.get_many(table, &persistent_slices, version)?;
414			for (slot, result) in persistent_idx.into_iter().zip(persistent_results) {
415				if let (
416					Some(read),
417					VersionedGetResult::Value {
418						value,
419						version: v,
420					},
421				) = (&self.read, &result)
422				{
423					read.insert(table_keys[slot].clone(), *v, Some(value.clone()));
424				}
425				persistent_aligned[slot] = result;
426			}
427		}
428
429		Ok((read_aligned, persistent_aligned))
430	}
431
432	#[inline]
433	fn collect_resolved_rows(
434		&self,
435		table_keys: &[&EncodedKey],
436		commit_results: &[VersionedGetResult],
437		read_aligned: &[VersionedGetResult],
438		persistent_aligned: &[VersionedGetResult],
439		out: &mut HashMap<EncodedKey, MultiVersionRow>,
440	) {
441		for (i, key) in table_keys.iter().enumerate() {
442			let resolved = match &commit_results[i] {
443				VersionedGetResult::Value {
444					value,
445					version: v,
446				} => Some((value.clone(), *v)),
447				VersionedGetResult::Tombstone => None,
448				VersionedGetResult::NotFound => match &read_aligned[i] {
449					VersionedGetResult::Value {
450						value,
451						version: v,
452					} => Some((value.clone(), *v)),
453					VersionedGetResult::Tombstone => None,
454					VersionedGetResult::NotFound => match &persistent_aligned[i] {
455						VersionedGetResult::Value {
456							value,
457							version: v,
458						} => Some((value.clone(), *v)),
459						_ => None,
460					},
461				},
462			};
463
464			if let Some((value, v)) = resolved {
465				out.insert(
466					(*key).clone(),
467					MultiVersionRow {
468						key: (*key).clone(),
469						row: EncodedRow(value),
470						version: v,
471					},
472				);
473			}
474		}
475	}
476
477	#[inline]
478	fn dispatch_drops(&self, drop_batch: Vec<DropRequest>) {
479		if drop_batch.is_empty() {
480			return;
481		}
482		if let Some(actor) = &self.drop_actor
483			&& actor.send_blocking(DropMessage::Batch(drop_batch)).is_err()
484		{
485			warn!("Failed to send drop batch");
486		}
487	}
488
489	#[inline]
490	fn update_read_cache_on_commit(&self, version: CommitVersion, batches: &TierBatch) {
491		let Some(read) = &self.read else {
492			return;
493		};
494		for (table, entries) in batches {
495			match table {
496				EntryKind::Operator(_) => {
497					for (key, value) in entries {
498						match value {
499							Some(value) => {
500								read.insert(key.clone(), version, Some(value.clone()))
501							}
502							None => read.invalidate(key),
503						}
504					}
505				}
506				_ => {
507					for (key, _) in entries {
508						read.invalidate(key);
509					}
510				}
511			}
512		}
513	}
514
515	#[inline]
516	fn write_batches(&self, version: CommitVersion, batches: TierBatch) -> Result<bool> {
517		if let Some(commit) = &self.commit {
518			commit.set(version, batches)?;
519		} else if let Some(persistent) = &self.persistent {
520			persistent.set(version, batches)?;
521		} else {
522			return Ok(false);
523		}
524		Ok(true)
525	}
526
527	fn evict_operator_state(&self, drops: &[(EntryKind, EncodedKey)]) -> Result<()> {
528		if drops.is_empty() {
529			return Ok(());
530		}
531
532		self.evict_drops_from_commit(drops)?;
533		self.delete_drops_from_persistent(drops)?;
534		self.invalidate_drops_in_read(drops);
535
536		Ok(())
537	}
538
539	#[inline]
540	fn evict_drops_from_commit(&self, drops: &[(EntryKind, EncodedKey)]) -> Result<()> {
541		let Some(commit) = &self.commit else {
542			return Ok(());
543		};
544		let mut batches: HashMap<EntryKind, Vec<(EncodedKey, CommitVersion)>> = HashMap::new();
545		for (table, key) in drops {
546			for (entry_version, _) in commit.get_all_versions(*table, key.as_ref())? {
547				batches.entry(*table).or_default().push((key.clone(), entry_version));
548			}
549		}
550		if !batches.is_empty() {
551			commit.drop(batches)?;
552		}
553		Ok(())
554	}
555
556	#[inline]
557	fn delete_drops_from_persistent(&self, drops: &[(EntryKind, EncodedKey)]) -> Result<()> {
558		let Some(persistent) = &self.persistent else {
559			return Ok(());
560		};
561		let mut by_table: HashMap<EntryKind, Vec<EncodedKey>> = HashMap::new();
562		for (table, key) in drops {
563			by_table.entry(*table).or_default().push(key.clone());
564		}
565		for (table, keys) in by_table {
566			persistent.delete_keys(table, &keys)?;
567		}
568		Ok(())
569	}
570
571	#[inline]
572	fn invalidate_drops_in_read(&self, drops: &[(EntryKind, EncodedKey)]) {
573		let Some(read) = &self.read else {
574			return;
575		};
576		for (_, key) in drops {
577			read.invalidate(key);
578		}
579	}
580
581	#[inline]
582	fn emit_commit_metrics(&self, writes: Vec<MultiWrite>, deletes: Vec<MultiDelete>, version: CommitVersion) {
583		if writes.is_empty() && deletes.is_empty() {
584			return;
585		}
586		self.event_bus.emit(MultiCommittedEvent::new(writes, deletes, vec![], version));
587	}
588}
589
590#[derive(Debug, Clone, Default)]
591pub struct MultiVersionRangeCursor {
592	pub commit: RangeCursor,
593
594	pub persistent: RangeCursor,
595
596	pub exhausted: bool,
597
598	warm_bucket: Option<PageId>,
599
600	warm_consumed: u64,
601}
602
603impl MultiVersionRangeCursor {
604	pub fn new() -> Self {
605		Self::default()
606	}
607
608	pub fn is_exhausted(&self) -> bool {
609		self.exhausted
610	}
611}
612
613pub struct TierScanQuery<'a> {
614	pub table: EntryKind,
615	pub start: &'a [u8],
616	pub end: &'a [u8],
617	pub scope: MultiVersionScope,
618	pub range: &'a EncodedKeyRange,
619}
620
621pub fn scan_tier_chunk<S: TierStorage>(
622	storage: &S,
623	cursor: &mut RangeCursor,
624	scan: &TierScanQuery,
625	collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
626) -> Result<bool> {
627	let batch = storage.range_next(
628		scan.table,
629		cursor,
630		Bound::Included(scan.start),
631		Bound::Included(scan.end),
632		scan.scope,
633		TIER_SCAN_CHUNK_SIZE,
634	)?;
635	merge_tier_batch(batch, scan.range, collected)
636}
637
638pub fn scan_tier_chunk_rev<S: TierStorage>(
639	storage: &S,
640	cursor: &mut RangeCursor,
641	scan: &TierScanQuery,
642	collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
643) -> Result<bool> {
644	let batch = storage.range_rev_next(
645		scan.table,
646		cursor,
647		Bound::Included(scan.start),
648		Bound::Included(scan.end),
649		scan.scope,
650		TIER_SCAN_CHUNK_SIZE,
651	)?;
652	merge_tier_batch(batch, scan.range, collected)
653}
654
655#[inline]
656fn merge_tier_batch(
657	batch: RangeBatch,
658	range: &EncodedKeyRange,
659	collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
660) -> Result<bool> {
661	if batch.entries.is_empty() {
662		return Ok(false);
663	}
664
665	for entry in batch.entries {
666		let original_key = entry.key.as_slice().to_vec();
667		let entry_version = entry.version;
668
669		let original_key_encoded = EncodedKey::new(original_key.clone());
670		if !range.contains(&original_key_encoded) {
671			continue;
672		}
673
674		let should_update = match collected.get(&original_key) {
675			None => true,
676			Some((existing_version, _)) => entry_version > *existing_version,
677		};
678
679		if should_update {
680			collected.insert(original_key, (entry_version, entry.value));
681		}
682	}
683
684	Ok(true)
685}
686
687#[inline]
688pub fn collected_to_batch(
689	collected: BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
690	has_more: bool,
691) -> MultiVersionBatch {
692	let items: Vec<MultiVersionRow> = collected
693		.into_iter()
694		.filter_map(|(key_bytes, (v, value))| {
695			value.map(|val| MultiVersionRow {
696				key: EncodedKey::new(key_bytes),
697				row: EncodedRow(val),
698				version: v,
699			})
700		})
701		.collect();
702
703	MultiVersionBatch {
704		items,
705		has_more,
706	}
707}
708
709#[inline]
710fn step_all_tiers(
711	buffer: Option<&MultiCommitBufferTier>,
712	buffer_cursor: &mut RangeCursor,
713	persistent: Option<&MultiPersistentTier>,
714	persistent_cursor: &mut RangeCursor,
715	scan: &TierScanQuery,
716	collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
717) -> Result<bool> {
718	let mut any_progress = false;
719	if let Some(s) = buffer
720		&& !buffer_cursor.exhausted
721	{
722		any_progress |= scan_tier_chunk(s, buffer_cursor, scan, collected)?;
723	}
724	if let Some(s) = persistent
725		&& !persistent_cursor.exhausted
726	{
727		any_progress |= scan_tier_chunk(s, persistent_cursor, scan, collected)?;
728	}
729	Ok(any_progress)
730}
731
732pub fn scan_tiers_latest(
733	buffer: Option<&MultiCommitBufferTier>,
734	persistent: Option<&MultiPersistentTier>,
735	range: EncodedKeyRange,
736	scope: MultiVersionScope,
737	max_keys: usize,
738) -> Result<MultiVersionBatch> {
739	let table = classify_key_range(&range);
740	let (start, end) = make_range_bounds(&range);
741	let scan = TierScanQuery {
742		table,
743		start: &start,
744		end: &end,
745		scope,
746		range: &range,
747	};
748
749	let mut collected: BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)> = BTreeMap::new();
750	let mut buffer_cursor = RangeCursor::default();
751	let mut persistent_cursor = RangeCursor::default();
752	let mut exhausted = false;
753
754	while collected.len() < max_keys {
755		let progress = step_all_tiers(
756			buffer,
757			&mut buffer_cursor,
758			persistent,
759			&mut persistent_cursor,
760			&scan,
761			&mut collected,
762		)?;
763		if !progress {
764			exhausted = true;
765			break;
766		}
767	}
768
769	Ok(collected_to_batch(collected, !exhausted))
770}
771
772impl StandardMultiStore {
773	pub fn range_next(
774		&self,
775		cursor: &mut MultiVersionRangeCursor,
776		range: EncodedKeyRange,
777		scope: MultiVersionScope,
778		batch_size: u64,
779	) -> Result<MultiVersionBatch> {
780		if cursor.exhausted {
781			return Ok(MultiVersionBatch {
782				items: Vec::new(),
783				has_more: false,
784			});
785		}
786
787		mark_unconfigured_exhausted(self, cursor);
788
789		let table = classify_key_range(&range);
790		let (start, end) = make_range_bounds(&range);
791		let batch_size = batch_size as usize;
792		let scan = TierScanQuery {
793			table,
794			start: &start,
795			end: &end,
796			scope,
797			range: &range,
798		};
799
800		let mut collected: BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)> = BTreeMap::new();
801
802		while collected.len() < batch_size {
803			let mut any_progress = false;
804
805			if let Some(commit) = &self.commit
806				&& !cursor.commit.exhausted
807			{
808				any_progress |= scan_tier_chunk(commit, &mut cursor.commit, &scan, &mut collected)?;
809			}
810
811			if self.persistent.is_some() && !cursor.persistent.exhausted {
812				any_progress |= self.step_persistent_cached(&scan, cursor, &mut collected, false)?;
813			}
814
815			if !any_progress {
816				cursor.exhausted = true;
817				break;
818			}
819		}
820
821		apply_forward_horizon(cursor, &mut collected);
822
823		let items: Vec<MultiVersionRow> = collected
824			.into_iter()
825			.filter_map(|(key_bytes, (v, value))| {
826				value.map(|val| MultiVersionRow {
827					key: EncodedKey::new(key_bytes),
828					row: EncodedRow(val),
829					version: v,
830				})
831			})
832			.collect();
833
834		let has_more = !cursor.exhausted;
835
836		Ok(MultiVersionBatch {
837			items,
838			has_more,
839		})
840	}
841
842	pub fn range(
843		&self,
844		range: EncodedKeyRange,
845		scope: MultiVersionScope,
846		batch_size: usize,
847	) -> MultiVersionRangeIter {
848		MultiVersionRangeIter {
849			store: self.clone(),
850			cursor: MultiVersionRangeCursor::new(),
851			range,
852			scope,
853			batch_size,
854			current_batch: Vec::new(),
855			current_index: 0,
856		}
857	}
858
859	pub fn range_rev(
860		&self,
861		range: EncodedKeyRange,
862		scope: MultiVersionScope,
863		batch_size: usize,
864	) -> MultiVersionRangeRevIter {
865		MultiVersionRangeRevIter {
866			store: self.clone(),
867			cursor: MultiVersionRangeCursor::new(),
868			range,
869			scope,
870			batch_size,
871			current_batch: Vec::new(),
872			current_index: 0,
873		}
874	}
875
876	fn range_rev_next(
877		&self,
878		cursor: &mut MultiVersionRangeCursor,
879		range: EncodedKeyRange,
880		scope: MultiVersionScope,
881		batch_size: u64,
882	) -> Result<MultiVersionBatch> {
883		if cursor.exhausted {
884			return Ok(MultiVersionBatch {
885				items: Vec::new(),
886				has_more: false,
887			});
888		}
889
890		mark_unconfigured_exhausted(self, cursor);
891
892		let table = classify_key_range(&range);
893		let (start, end) = make_range_bounds(&range);
894		let batch_size = batch_size as usize;
895		let scan = TierScanQuery {
896			table,
897			start: &start,
898			end: &end,
899			scope,
900			range: &range,
901		};
902
903		let mut collected: BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)> = BTreeMap::new();
904
905		while collected.len() < batch_size {
906			let mut any_progress = false;
907
908			if let Some(commit) = &self.commit
909				&& !cursor.commit.exhausted
910			{
911				any_progress |= scan_tier_chunk_rev(commit, &mut cursor.commit, &scan, &mut collected)?;
912			}
913
914			if self.persistent.is_some() && !cursor.persistent.exhausted {
915				any_progress |= self.step_persistent_cached(&scan, cursor, &mut collected, true)?;
916			}
917
918			if !any_progress {
919				cursor.exhausted = true;
920				break;
921			}
922		}
923
924		apply_reverse_horizon(cursor, &mut collected);
925
926		let items: Vec<MultiVersionRow> = collected
927			.into_iter()
928			.rev()
929			.filter_map(|(key_bytes, (v, value))| {
930				value.map(|val| MultiVersionRow {
931					key: EncodedKey::new(key_bytes),
932					row: EncodedRow(val),
933					version: v,
934				})
935			})
936			.collect();
937
938		let has_more = !cursor.exhausted;
939
940		Ok(MultiVersionBatch {
941			items,
942			has_more,
943		})
944	}
945
946	fn step_persistent_cached(
947		&self,
948		scan: &TierScanQuery,
949		cursor: &mut MultiVersionRangeCursor,
950		collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
951		descending: bool,
952	) -> Result<bool> {
953		let Some(persistent) = &self.persistent else {
954			return Ok(false);
955		};
956
957		if let Some(served) = self.serve_from_read_cache(scan, cursor, collected, descending) {
958			return served;
959		}
960
961		let (consumed, progressed) =
962			self.scan_persistent_chunk(persistent, scan, cursor, collected, descending)?;
963		self.warm_read_bucket_after_scan(persistent, scan, cursor, consumed)?;
964
965		Ok(progressed)
966	}
967
968	#[inline]
969	fn serve_from_read_cache(
970		&self,
971		scan: &TierScanQuery,
972		cursor: &mut MultiVersionRangeCursor,
973		collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
974		descending: bool,
975	) -> Option<Result<bool>> {
976		let (Some(read), EntryKind::Source(_)) = (&self.read, scan.table) else {
977			return None;
978		};
979		match read.serve_persistent_chunk(
980			scan.table,
981			&mut cursor.persistent,
982			scan.start,
983			scan.end,
984			scan.scope,
985			TIER_SCAN_CHUNK_SIZE,
986			descending,
987		) {
988			ServedChunk::Served(batch) => Some(merge_tier_batch(batch, scan.range, collected)),
989			ServedChunk::Gap => None,
990		}
991	}
992
993	#[inline]
994	fn scan_persistent_chunk(
995		&self,
996		persistent: &MultiPersistentTier,
997		scan: &TierScanQuery,
998		cursor: &mut MultiVersionRangeCursor,
999		collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
1000		descending: bool,
1001	) -> Result<(usize, bool)> {
1002		let batch = if descending {
1003			persistent.range_rev_next(
1004				scan.table,
1005				&mut cursor.persistent,
1006				Bound::Included(scan.start),
1007				Bound::Included(scan.end),
1008				scan.scope,
1009				TIER_SCAN_CHUNK_SIZE,
1010			)?
1011		} else {
1012			persistent.range_next(
1013				scan.table,
1014				&mut cursor.persistent,
1015				Bound::Included(scan.start),
1016				Bound::Included(scan.end),
1017				scan.scope,
1018				TIER_SCAN_CHUNK_SIZE,
1019			)?
1020		};
1021		let consumed = batch.entries.len();
1022		let progressed = merge_tier_batch(batch, scan.range, collected)?;
1023		Ok((consumed, progressed))
1024	}
1025
1026	#[inline]
1027	fn warm_read_bucket_after_scan(
1028		&self,
1029		persistent: &MultiPersistentTier,
1030		scan: &TierScanQuery,
1031		cursor: &mut MultiVersionRangeCursor,
1032		consumed: usize,
1033	) -> Result<()> {
1034		if let (Some(read), EntryKind::Source(_)) = (&self.read, scan.table) {
1035			maybe_warm_bucket(read, persistent, cursor, scan.table, consumed)?;
1036		}
1037		Ok(())
1038	}
1039}
1040
1041fn maybe_warm_bucket(
1042	read: &MultiReadBufferTier,
1043	persistent: &MultiPersistentTier,
1044	cursor: &mut MultiVersionRangeCursor,
1045	table: EntryKind,
1046	consumed: usize,
1047) -> Result<()> {
1048	let page = {
1049		let Some(last) = cursor.persistent.last_key.as_ref() else {
1050			return Ok(());
1051		};
1052		read.page_of_key(last)
1053	};
1054	if !matches!(page.kind, EntryKind::Source(_)) {
1055		return Ok(());
1056	}
1057
1058	if cursor.warm_bucket == Some(page) {
1059		cursor.warm_consumed = cursor.warm_consumed.saturating_add(consumed as u64);
1060	} else {
1061		cursor.warm_bucket = Some(page);
1062		cursor.warm_consumed = consumed as u64;
1063	}
1064
1065	if cursor.warm_consumed <= WARM_THRESHOLD {
1066		return Ok(());
1067	}
1068
1069	let Some(range) = read.page_key_range(page) else {
1070		return Ok(());
1071	};
1072	let (Bound::Included(lo), Bound::Included(hi)) = (range.start, range.end) else {
1073		return Ok(());
1074	};
1075	let entries = persistent.load_range_consistent(
1076		table,
1077		Bound::Included(lo.as_slice()),
1078		Bound::Included(hi.as_slice()),
1079		CommitVersion(u64::MAX),
1080	)?;
1081	read.populate_page(page, entries, true);
1082	cursor.warm_bucket = None;
1083	cursor.warm_consumed = 0;
1084	Ok(())
1085}
1086
1087fn mark_unconfigured_exhausted(store: &StandardMultiStore, cursor: &mut MultiVersionRangeCursor) {
1088	if store.commit.is_none() {
1089		cursor.commit.exhausted = true;
1090	}
1091	if store.persistent.is_none() {
1092		cursor.persistent.exhausted = true;
1093	}
1094}
1095
1096fn apply_forward_horizon(
1097	cursor: &mut MultiVersionRangeCursor,
1098	collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
1099) {
1100	let horizon = forward_horizon(cursor);
1101	if let Some(h) = horizon {
1102		collected.retain(|k, _| k.as_slice() <= h.as_slice());
1103		rewind_over_advanced_forward(cursor, &h);
1104	}
1105}
1106
1107fn apply_reverse_horizon(
1108	cursor: &mut MultiVersionRangeCursor,
1109	collected: &mut BTreeMap<Vec<u8>, (CommitVersion, Option<CowVec<u8>>)>,
1110) {
1111	let horizon = reverse_horizon(cursor);
1112	if let Some(h) = horizon {
1113		collected.retain(|k, _| k.as_slice() >= h.as_slice());
1114		rewind_over_advanced_reverse(cursor, &h);
1115	}
1116}
1117
1118fn forward_horizon(cursor: &MultiVersionRangeCursor) -> Option<EncodedKey> {
1119	let mut horizon: Option<EncodedKey> = None;
1120	for tier in [&cursor.commit, &cursor.persistent] {
1121		if tier.exhausted {
1122			continue;
1123		}
1124		let last = match &tier.last_key {
1125			Some(k) => k.clone(),
1126
1127			None => return None,
1128		};
1129		horizon = Some(match horizon {
1130			None => last,
1131			Some(prev) => {
1132				if last.as_slice() < prev.as_slice() {
1133					last
1134				} else {
1135					prev
1136				}
1137			}
1138		});
1139	}
1140	horizon
1141}
1142
1143fn reverse_horizon(cursor: &MultiVersionRangeCursor) -> Option<EncodedKey> {
1144	let mut horizon: Option<EncodedKey> = None;
1145	for tier in [&cursor.commit, &cursor.persistent] {
1146		if tier.exhausted {
1147			continue;
1148		}
1149		let last = match &tier.last_key {
1150			Some(k) => k.clone(),
1151			None => return None,
1152		};
1153		horizon = Some(match horizon {
1154			None => last,
1155			Some(prev) => {
1156				if last.as_slice() > prev.as_slice() {
1157					last
1158				} else {
1159					prev
1160				}
1161			}
1162		});
1163	}
1164	horizon
1165}
1166
1167fn rewind_over_advanced_forward(cursor: &mut MultiVersionRangeCursor, horizon: &EncodedKey) {
1168	for tier in [&mut cursor.commit, &mut cursor.persistent] {
1169		if let Some(last) = &tier.last_key
1170			&& last.as_slice() > horizon.as_slice()
1171		{
1172			tier.last_key = Some(horizon.clone());
1173			tier.exhausted = false;
1174		}
1175	}
1176}
1177
1178fn rewind_over_advanced_reverse(cursor: &mut MultiVersionRangeCursor, horizon: &EncodedKey) {
1179	for tier in [&mut cursor.commit, &mut cursor.persistent] {
1180		if let Some(last) = &tier.last_key
1181			&& last.as_slice() < horizon.as_slice()
1182		{
1183			tier.last_key = Some(horizon.clone());
1184			tier.exhausted = false;
1185		}
1186	}
1187}
1188
1189impl MultiVersionGetPrevious for StandardMultiStore {
1190	fn get_previous_version(
1191		&self,
1192		key: &EncodedKey,
1193		before_version: CommitVersion,
1194	) -> Result<Option<MultiVersionRow>> {
1195		if before_version.0 == 0 {
1196			return Ok(None);
1197		}
1198
1199		let table = classify_key(key);
1200		reifydb_assertions! {
1201			assert!(
1202				before_version.0 >= 1,
1203				"the before_version==0 guard must precede this subtraction, otherwise before_version.0 - 1 \
1204				 wraps to u64::MAX and the probe reads the latest version instead of the previous one \
1205				 (before_version={})",
1206				before_version.0
1207			);
1208		}
1209		let prev_version = CommitVersion(before_version.0 - 1);
1210
1211		if let Some(found) = self.previous_probe_commit(table, key, prev_version)? {
1212			return Ok(found);
1213		}
1214		if let Some(found) = self.previous_probe_read(key, prev_version) {
1215			return Ok(found);
1216		}
1217		if let Some(found) = self.previous_probe_persistent(table, key, prev_version)? {
1218			return Ok(found);
1219		}
1220
1221		Ok(None)
1222	}
1223}
1224
1225impl StandardMultiStore {
1226	#[inline]
1227	fn previous_probe_commit(
1228		&self,
1229		table: EntryKind,
1230		key: &EncodedKey,
1231		prev_version: CommitVersion,
1232	) -> Result<Option<Option<MultiVersionRow>>> {
1233		let Some(commit) = &self.commit else {
1234			return Ok(None);
1235		};
1236		Ok(match commit.get(table, key.as_ref(), prev_version)? {
1237			VersionedGetResult::Value {
1238				value,
1239				version,
1240			} => Some(Some(MultiVersionRow {
1241				key: key.clone(),
1242				row: EncodedRow(CowVec::new(value.to_vec())),
1243				version,
1244			})),
1245			VersionedGetResult::Tombstone => Some(None),
1246			VersionedGetResult::NotFound => None,
1247		})
1248	}
1249
1250	#[inline]
1251	fn previous_probe_read(
1252		&self,
1253		key: &EncodedKey,
1254		prev_version: CommitVersion,
1255	) -> Option<Option<MultiVersionRow>> {
1256		let read = self.read.as_ref()?;
1257		match read.get(key, prev_version) {
1258			VersionedGetResult::Value {
1259				value,
1260				version,
1261			} => Some(Some(MultiVersionRow {
1262				key: key.clone(),
1263				row: EncodedRow(CowVec::new(value.to_vec())),
1264				version,
1265			})),
1266			VersionedGetResult::Tombstone => Some(None),
1267			VersionedGetResult::NotFound => None,
1268		}
1269	}
1270
1271	#[inline]
1272	fn previous_probe_persistent(
1273		&self,
1274		table: EntryKind,
1275		key: &EncodedKey,
1276		prev_version: CommitVersion,
1277	) -> Result<Option<Option<MultiVersionRow>>> {
1278		let Some(persistent) = &self.persistent else {
1279			return Ok(None);
1280		};
1281		Ok(match persistent.get(table, key.as_ref(), prev_version)? {
1282			VersionedGetResult::Value {
1283				value,
1284				version,
1285			} => {
1286				if let Some(read) = &self.read {
1287					read.insert(key.clone(), version, Some(value.clone()));
1288				}
1289				Some(Some(MultiVersionRow {
1290					key: key.clone(),
1291					row: EncodedRow(CowVec::new(value.to_vec())),
1292					version,
1293				}))
1294			}
1295			VersionedGetResult::Tombstone => Some(None),
1296			VersionedGetResult::NotFound => None,
1297		})
1298	}
1299}
1300
1301impl MultiVersionStore for StandardMultiStore {}
1302
1303pub struct MultiVersionRangeIter {
1304	store: StandardMultiStore,
1305	cursor: MultiVersionRangeCursor,
1306	range: EncodedKeyRange,
1307	scope: MultiVersionScope,
1308	batch_size: usize,
1309	current_batch: Vec<MultiVersionRow>,
1310	current_index: usize,
1311}
1312
1313impl Iterator for MultiVersionRangeIter {
1314	type Item = Result<MultiVersionRow>;
1315
1316	fn next(&mut self) -> Option<Self::Item> {
1317		if self.current_index < self.current_batch.len() {
1318			let item = self.current_batch[self.current_index].clone();
1319			self.current_index += 1;
1320			return Some(Ok(item));
1321		}
1322
1323		if self.cursor.exhausted {
1324			return None;
1325		}
1326
1327		match self.store.range_next(&mut self.cursor, self.range.clone(), self.scope, self.batch_size as u64) {
1328			Ok(batch) => {
1329				if batch.items.is_empty() {
1330					if self.cursor.exhausted {
1331						return None;
1332					}
1333					return self.next();
1334				}
1335				self.current_batch = batch.items;
1336				self.current_index = 0;
1337				self.next()
1338			}
1339			Err(e) => Some(Err(e)),
1340		}
1341	}
1342}
1343
1344pub struct MultiVersionRangeRevIter {
1345	store: StandardMultiStore,
1346	cursor: MultiVersionRangeCursor,
1347	range: EncodedKeyRange,
1348	scope: MultiVersionScope,
1349	batch_size: usize,
1350	current_batch: Vec<MultiVersionRow>,
1351	current_index: usize,
1352}
1353
1354impl Iterator for MultiVersionRangeRevIter {
1355	type Item = Result<MultiVersionRow>;
1356
1357	fn next(&mut self) -> Option<Self::Item> {
1358		if self.current_index < self.current_batch.len() {
1359			let item = self.current_batch[self.current_index].clone();
1360			self.current_index += 1;
1361			return Some(Ok(item));
1362		}
1363
1364		if self.cursor.exhausted {
1365			return None;
1366		}
1367
1368		match self.store.range_rev_next(
1369			&mut self.cursor,
1370			self.range.clone(),
1371			self.scope,
1372			self.batch_size as u64,
1373		) {
1374			Ok(batch) => {
1375				if batch.items.is_empty() {
1376					if self.cursor.exhausted {
1377						return None;
1378					}
1379					return self.next();
1380				}
1381				self.current_batch = batch.items;
1382				self.current_index = 0;
1383				self.next()
1384			}
1385			Err(e) => Some(Err(e)),
1386		}
1387	}
1388}
1389
1390fn classify_key_range(range: &EncodedKeyRange) -> EntryKind {
1391	classify_range(range).unwrap_or(EntryKind::Multi)
1392}
1393
1394fn make_range_bounds(range: &EncodedKeyRange) -> (Vec<u8>, Vec<u8>) {
1395	let start = match &range.start {
1396		Bound::Included(key) => key.as_ref().to_vec(),
1397		Bound::Excluded(key) => key.as_ref().to_vec(),
1398		Bound::Unbounded => vec![],
1399	};
1400
1401	let end = match &range.end {
1402		Bound::Included(key) => key.as_ref().to_vec(),
1403		Bound::Excluded(key) => key.as_ref().to_vec(),
1404		Bound::Unbounded => vec![0xFFu8; 256],
1405	};
1406
1407	(start, end)
1408}
1409
1410#[cfg(all(test, feature = "sqlite", not(target_arch = "wasm32")))]
1411mod cache_tests {
1412	use std::collections::HashMap;
1413
1414	use reifydb_core::{
1415		common::CommitVersion,
1416		delta::Delta,
1417		encoded::{key::EncodedKey, row::EncodedRow},
1418		interface::{
1419			catalog::{flow::FlowNodeId, id::TableId, shape::ShapeId},
1420			store::{EntryKind, MultiVersionCommit},
1421		},
1422		key::{
1423			EncodableKey, flow_node_internal_state::FlowNodeInternalStateKey,
1424			flow_node_state::FlowNodeStateKey, row::RowKey,
1425		},
1426	};
1427	use reifydb_value::{cow_vec, util::cowvec::CowVec};
1428
1429	use crate::{
1430		MultiVersionScope,
1431		store::{StandardMultiStore, multi::WARM_THRESHOLD},
1432		tier::{RawEntry, TierStorage, VersionedGetResult, commit::buffer::MultiCommitBufferTier},
1433	};
1434
1435	const SHAPE: ShapeId = ShapeId::Table(TableId(1));
1436
1437	fn commit_row(store: &StandardMultiStore, n: u64, version: u64) {
1438		MultiVersionCommit::commit(
1439			store,
1440			cow_vec![Delta::Set {
1441				key: RowKey::encoded(SHAPE, n),
1442				row: EncodedRow(CowVec::new(format!("v{n}").into_bytes())),
1443			}],
1444			CommitVersion(version),
1445		)
1446		.unwrap();
1447	}
1448
1449	fn flush(store: &StandardMultiStore, cutoff: CommitVersion) {
1450		let commit = store.commit().expect("commit tier");
1451		for kind in commit.list_all_entry_kinds().unwrap() {
1452			let (to_persist, to_drop) = match commit {
1453				MultiCommitBufferTier::Memory(s) => s.collect_evictable_below(kind, cutoff),
1454			};
1455			if to_drop.is_empty() {
1456				continue;
1457			}
1458			if !to_persist.is_empty() {
1459				let persistent = store.persistent().expect("persistent tier");
1460				let mut by_version: HashMap<
1461					CommitVersion,
1462					HashMap<EntryKind, Vec<(EncodedKey, Option<CowVec<u8>>)>>,
1463				> = HashMap::new();
1464				for (key, version, value) in to_persist {
1465					by_version
1466						.entry(version)
1467						.or_default()
1468						.entry(kind)
1469						.or_default()
1470						.push((key, value));
1471				}
1472				for (version, batch) in by_version {
1473					persistent.set(version, batch).unwrap();
1474				}
1475			}
1476			for (key, _) in &to_drop {
1477				store.invalidate_read_key(key);
1478			}
1479			commit.drop(HashMap::from([(kind, to_drop)])).unwrap();
1480		}
1481	}
1482
1483	#[test]
1484	fn operator_drop_fully_removes_state_leaving_no_tombstone() {
1485		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1486		let node = FlowNodeId(7);
1487		let table = EntryKind::Operator(node);
1488		let data_key = FlowNodeStateKey::encoded(node, vec![1u8]);
1489		let internal_key = FlowNodeInternalStateKey::encoded(node, vec![2u8]);
1490
1491		for v in [1u64, 2] {
1492			MultiVersionCommit::commit(
1493				&store,
1494				cow_vec![Delta::Set {
1495					key: data_key.clone(),
1496					row: EncodedRow(CowVec::new(vec![v as u8])),
1497				}],
1498				CommitVersion(v),
1499			)
1500			.unwrap();
1501		}
1502		for v in [3u64, 4] {
1503			MultiVersionCommit::commit(
1504				&store,
1505				cow_vec![Delta::Set {
1506					key: internal_key.clone(),
1507					row: EncodedRow(CowVec::new(vec![v as u8])),
1508				}],
1509				CommitVersion(v),
1510			)
1511			.unwrap();
1512		}
1513
1514		let commit = store.commit().expect("commit tier");
1515		assert!(!commit.get_all_versions(table, data_key.as_ref()).unwrap().is_empty());
1516		assert!(!commit.get_all_versions(table, internal_key.as_ref()).unwrap().is_empty());
1517
1518		MultiVersionCommit::commit(
1519			&store,
1520			cow_vec![
1521				Delta::Drop {
1522					key: data_key.clone(),
1523				},
1524				Delta::Drop {
1525					key: internal_key.clone(),
1526				}
1527			],
1528			CommitVersion(5),
1529		)
1530		.unwrap();
1531
1532		assert!(
1533			commit.get_all_versions(table, data_key.as_ref()).unwrap().is_empty(),
1534			"operator data-state Drop must remove every version, not leave a tombstone"
1535		);
1536		assert!(
1537			commit.get_all_versions(table, internal_key.as_ref()).unwrap().is_empty(),
1538			"operator internal-state Drop must remove every version, not leave a tombstone"
1539		);
1540	}
1541
1542	#[test]
1543	fn operator_remove_leaves_a_tombstone_in_commit_tier() {
1544		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1545		let node = FlowNodeId(8);
1546		let table = EntryKind::Operator(node);
1547		let key = FlowNodeInternalStateKey::encoded(node, vec![9u8]);
1548
1549		MultiVersionCommit::commit(
1550			&store,
1551			cow_vec![Delta::Set {
1552				key: key.clone(),
1553				row: EncodedRow(CowVec::new(vec![1u8])),
1554			}],
1555			CommitVersion(1),
1556		)
1557		.unwrap();
1558		MultiVersionCommit::commit(
1559			&store,
1560			cow_vec![Delta::Remove {
1561				key: key.clone(),
1562			}],
1563			CommitVersion(2),
1564		)
1565		.unwrap();
1566
1567		let commit = store.commit().expect("commit tier");
1568		let versions = commit.get_all_versions(table, key.as_ref()).unwrap();
1569		assert!(
1570			versions.iter().any(|(_, value)| value.is_none()),
1571			"Remove leaves a tombstone in the commit tier (the path Drop must avoid); versions={versions:?}"
1572		);
1573	}
1574
1575	#[test]
1576	fn operator_state_drop_keeps_keyspace_bounded_under_churn() {
1577		const ROUNDS: u64 = 200;
1578
1579		fn current_count(store: &StandardMultiStore, table: EntryKind) -> u64 {
1580			match store.commit().expect("commit tier") {
1581				MultiCommitBufferTier::Memory(s) => s.count_current(table).unwrap(),
1582			}
1583		}
1584
1585		fn churn(evict_with_drop: bool) -> u64 {
1586			let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1587			let node = FlowNodeId(21);
1588			let table = EntryKind::Operator(node);
1589			let key_at = |round: u64| FlowNodeInternalStateKey::encoded(node, round.to_be_bytes().to_vec());
1590
1591			let mut version = 0u64;
1592			for round in 0..ROUNDS {
1593				version += 1;
1594				MultiVersionCommit::commit(
1595					&store,
1596					cow_vec![Delta::Set {
1597						key: key_at(round),
1598						row: EncodedRow(CowVec::new(vec![1u8])),
1599					}],
1600					CommitVersion(version),
1601				)
1602				.unwrap();
1603
1604				if round > 0 {
1605					version += 1;
1606					let prev = key_at(round - 1);
1607					let delta = if evict_with_drop {
1608						Delta::Drop {
1609							key: prev,
1610						}
1611					} else {
1612						Delta::Remove {
1613							key: prev,
1614						}
1615					};
1616					MultiVersionCommit::commit(&store, cow_vec![delta], CommitVersion(version))
1617						.unwrap();
1618				}
1619			}
1620			current_count(&store, table)
1621		}
1622
1623		let drop_live = churn(true);
1624		let remove_live = churn(false);
1625
1626		assert!(
1627			drop_live <= 2,
1628			"Drop must keep the operator keyspace bounded to the live set; got {drop_live}"
1629		);
1630		assert!(
1631			remove_live >= ROUNDS - 1,
1632			"Remove leaves a tombstone per round (the path Drop avoids); got {remove_live} after {ROUNDS} rounds"
1633		);
1634	}
1635
1636	#[test]
1637	fn warm_threshold_warms_only_buckets_above_threshold() {
1638		const HEAVY: u64 = WARM_THRESHOLD + 64;
1639		const LIGHT: u64 = 20;
1640		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1641
1642		for n in 1..=HEAVY {
1643			commit_row(&store, n, 1);
1644		}
1645		for n in 0..LIGHT {
1646			commit_row(&store, (1u64 << 16) + n, 1);
1647		}
1648		flush(&store, CommitVersion(1));
1649
1650		let read = store.read.clone().expect("read tier configured");
1651		let heavy_bucket = read.page_of_key(&RowKey::encoded(SHAPE, 1));
1652		let light_bucket = read.page_of_key(&RowKey::encoded(SHAPE, 1u64 << 16));
1653		assert_ne!(heavy_bucket, light_bucket, "the two row groups must land in different buckets");
1654		assert!(!read.page_is_complete(heavy_bucket), "nothing is warm before the scan");
1655
1656		let scanned = store
1657			.range(
1658				RowKey::full_scan(SHAPE),
1659				MultiVersionScope::AsOf {
1660					read: CommitVersion(10),
1661				},
1662				32,
1663			)
1664			.collect::<Result<Vec<_>, _>>()
1665			.unwrap();
1666		assert_eq!(scanned.len() as u64, HEAVY + LIGHT, "the scan returns every row regardless of warming");
1667
1668		assert!(read.page_is_complete(heavy_bucket), "a bucket scanned past the threshold must be warmed");
1669		assert!(
1670			!read.page_is_complete(light_bucket),
1671			"a bucket scanned below the threshold must not be warmed"
1672		);
1673	}
1674
1675	#[test]
1676	fn operator_state_write_through_keeps_read_cache_warm() {
1677		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1678		let read = store.read.clone().expect("read tier configured");
1679
1680		let opkey = FlowNodeStateKey::new(FlowNodeId(7), vec![1, 2, 3]).encode();
1681		MultiVersionCommit::commit(
1682			&store,
1683			cow_vec![Delta::Set {
1684				key: opkey.clone(),
1685				row: EncodedRow(CowVec::new(b"state-v10".to_vec())),
1686			}],
1687			CommitVersion(10),
1688		)
1689		.unwrap();
1690
1691		match read.get(&opkey, CommitVersion(10)) {
1692			VersionedGetResult::Value {
1693				value,
1694				version,
1695			} => {
1696				assert_eq!(
1697					value.as_ref(),
1698					b"state-v10",
1699					"the cached operator state must be the committed value"
1700				);
1701				assert_eq!(
1702					version,
1703					CommitVersion(10),
1704					"the cached entry must carry the commit version"
1705				);
1706			}
1707			other => {
1708				panic!("operator state must be served from the read cache after commit, got {other:?}")
1709			}
1710		}
1711
1712		assert!(
1713			matches!(read.get(&opkey, CommitVersion(9)), VersionedGetResult::NotFound),
1714			"a pre-write snapshot read must miss the write-through entry, not see the newer value"
1715		);
1716	}
1717
1718	#[test]
1719	fn source_row_write_clears_range_complete_on_its_page() {
1720		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1721		let read = store.read.clone().expect("read tier configured");
1722
1723		let neighbor = RowKey::encoded(SHAPE, 1);
1724		let page = read.page_of_key(&neighbor);
1725		assert_eq!(
1726			read.page_of_key(&RowKey::encoded(SHAPE, 2)),
1727			page,
1728			"both source rows must share a page for this test to exercise flag-clearing"
1729		);
1730		read.populate_page(
1731			page,
1732			vec![RawEntry {
1733				key: neighbor,
1734				version: CommitVersion(1),
1735				value: Some(CowVec::new(b"neighbor".to_vec())),
1736			}],
1737			true,
1738		);
1739		assert!(read.page_is_complete(page), "the page must start range-complete");
1740
1741		commit_row(&store, 2, 5);
1742
1743		assert!(
1744			!read.page_is_complete(page),
1745			"writing a source row into a range-complete page must clear the flag so the range cache re-warms"
1746		);
1747	}
1748}