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, btree_map::Entry},
6	ops::{Bound, RangeBounds},
7	sync::atomic::Ordering,
8	vec,
9};
10
11use reifydb_codec::{
12	key::encoded::{EncodedKey, EncodedKeyRange},
13	row::bytes::EncodedBytes,
14};
15use reifydb_core::{
16	common::CommitVersion,
17	delta::Delta,
18	error::diagnostic::internal::internal,
19	event::metric::{MultiCommittedEvent, MultiDelete, MultiWrite},
20	interface::{
21		catalog::storage::StorageId,
22		store::{
23			EntryKind, MultiVersionBatch, MultiVersionCommit, MultiVersionContains, MultiVersionGet,
24			MultiVersionGetPrevious, MultiVersionRow, MultiVersionStore, StorageKey, classify_key_of,
25			classify_range, storage_key, storage_key_of,
26		},
27	},
28	key::{
29		any::TaggedKey,
30		row::{StoragePartitionedRowKey, StorageRowKey},
31	},
32};
33use reifydb_store::coverage::cursor::Cursor;
34use reifydb_store_commit::{
35	MultiVersionScope, RangeBatch, RangeCursor, RangeStop, RawEntry, TierBatch, VersionedGetResult,
36	store::CommitStore,
37};
38use reifydb_value::{
39	error, reifydb_assertions,
40	util::{cowvec::CowVec, hex::display},
41};
42use tracing::instrument;
43
44use super::StandardMultiStore;
45use crate::{
46	Result,
47	tier::{
48		TierStorage,
49		persistent::{MultiPersistentTier, NarrowRangeRequest, PersistentRangeLayout},
50		range::{NarrowLayout, ServedChunk, resume_after},
51	},
52};
53
54const TIER_SCAN_CHUNK_SIZE: usize = 32;
55
56#[derive(Clone, Copy)]
57struct ClassifiedKey<'a> {
58	key: &'a EncodedKey,
59	storage_key: Option<StorageKey>,
60}
61
62impl MultiVersionGet for StandardMultiStore {
63	fn get(&self, key: &TaggedKey, version: CommitVersion) -> Result<Option<MultiVersionRow<TaggedKey>>> {
64		let (table, storage_key) = storage_key_of(key);
65		let encoded = key.encode();
66		let row = match table {
67			EntryKind::Source(_, _) => self.get_source(table, storage_key, &encoded, version)?,
68			_ => self.get_multi(table, storage_key, &encoded, version)?,
69		};
70		Ok(row.map(|row| MultiVersionRow {
71			key: key.clone(),
72			bytes: row.bytes,
73			version: row.version,
74		}))
75	}
76}
77
78impl StandardMultiStore {
79	#[instrument(name = "store::multi::get::source", level = "trace", skip(self, key), fields(version = version.0))]
80	fn get_source(
81		&self,
82		table: EntryKind,
83		storage_key: Option<StorageKey>,
84		key: &EncodedKey,
85		version: CommitVersion,
86	) -> Result<Option<MultiVersionRow>> {
87		self.get_impl(table, storage_key, key, version)
88	}
89
90	#[instrument(name = "store::multi::get::multi", level = "trace", skip(self, key), fields(version = version.0))]
91	fn get_multi(
92		&self,
93		table: EntryKind,
94		storage_key: Option<StorageKey>,
95		key: &EncodedKey,
96		version: CommitVersion,
97	) -> Result<Option<MultiVersionRow>> {
98		self.get_impl(table, storage_key, key, version)
99	}
100
101	#[inline]
102	fn get_impl(
103		&self,
104		table: EntryKind,
105		storage_key: Option<StorageKey>,
106		key: &EncodedKey,
107		version: CommitVersion,
108	) -> Result<Option<MultiVersionRow>> {
109		if let Some(found) = self.get_probe_commit(table, key, version)? {
110			return Ok(found);
111		}
112		if let Some(found) = self.get_probe_read(table, storage_key, key, version) {
113			return Ok(found);
114		}
115		if let Some(found) = self.get_probe_persistent(table, storage_key, key, version)? {
116			return Ok(found);
117		}
118
119		Ok(None)
120	}
121}
122
123impl StandardMultiStore {
124	#[inline]
125	fn get_probe_commit(
126		&self,
127		table: EntryKind,
128		key: &EncodedKey,
129		version: CommitVersion,
130	) -> Result<Option<Option<MultiVersionRow>>> {
131		Ok(match self.commit.get(table, key.as_ref(), version)? {
132			VersionedGetResult::Value {
133				value,
134				version: v,
135			} => Some(Some(MultiVersionRow {
136				key: key.clone(),
137				bytes: EncodedBytes(value),
138				version: v,
139			})),
140			VersionedGetResult::Tombstone => Some(None),
141			VersionedGetResult::NotFound => None,
142		})
143	}
144
145	#[inline]
146	fn get_probe_read(
147		&self,
148		table: EntryKind,
149		storage_key: Option<StorageKey>,
150		key: &EncodedKey,
151		version: CommitVersion,
152	) -> Option<Option<MultiVersionRow>> {
153		let point = self.point.as_ref()?;
154		match point.get(table, storage_key, key, version) {
155			VersionedGetResult::Value {
156				value,
157				version: v,
158			} => Some(Some(MultiVersionRow {
159				key: key.clone(),
160				bytes: EncodedBytes(value),
161				version: v,
162			})),
163			VersionedGetResult::Tombstone => Some(None),
164			VersionedGetResult::NotFound => None,
165		}
166	}
167
168	#[inline]
169	fn get_probe_persistent(
170		&self,
171		table: EntryKind,
172		storage_key: Option<StorageKey>,
173		key: &EncodedKey,
174		version: CommitVersion,
175	) -> Result<Option<Option<MultiVersionRow>>> {
176		let Some(persistent) = &self.persistent else {
177			return Ok(None);
178		};
179		if !persistent.filter().may_contain((table, key)) {
180			return Ok(None);
181		}
182		self.persistent_probes.fetch_add(1, Ordering::Relaxed);
183		Ok(match persistent.get(table, key.as_ref(), version)? {
184			VersionedGetResult::Value {
185				value,
186				version: v,
187			} => {
188				if let Some(point) = &self.point {
189					point.insert(table, storage_key, key.clone(), v, Some(value.clone()));
190				}
191				Some(Some(MultiVersionRow {
192					key: key.clone(),
193					bytes: EncodedBytes(value),
194					version: v,
195				}))
196			}
197			VersionedGetResult::Tombstone => Some(None),
198			VersionedGetResult::NotFound => {
199				self.persistent_absent.fetch_add(1, Ordering::Relaxed);
200				None
201			}
202		})
203	}
204}
205
206impl MultiVersionContains for StandardMultiStore {
207	#[instrument(name = "store::multi::contains", level = "trace", skip(self, key), fields(version = version.0), ret)]
208	fn contains(&self, key: &TaggedKey, version: CommitVersion) -> Result<bool> {
209		Ok(MultiVersionGet::get(self, key, version)?.is_some())
210	}
211}
212
213impl MultiVersionCommit for StandardMultiStore {
214	#[instrument(name = "store::multi::commit", level = "debug", skip(self, deltas), fields(delta_count = deltas.len(), version = version.0))]
215	fn commit(&self, deltas: CowVec<Delta>, version: CommitVersion) -> Result<()> {
216		let classified = classify_deltas(&deltas);
217
218		self.update_read_cache_on_commit(&classified.batches);
219
220		self.write_batches(version, classified.batches)?;
221
222		self.emit_commit_metrics(classified.writes, classified.deletes, version);
223
224		Ok(())
225	}
226}
227
228struct ClassifiedDeltas {
229	writes: Vec<MultiWrite>,
230	deletes: Vec<MultiDelete>,
231	batches: TierBatch,
232}
233
234#[inline]
235fn classify_deltas(deltas: &CowVec<Delta>) -> ClassifiedDeltas {
236	let mut writes: Vec<MultiWrite> = Vec::new();
237	let mut deletes: Vec<MultiDelete> = Vec::new();
238	let mut batches: TierBatch = HashMap::new();
239
240	for delta in deltas.iter() {
241		let table = classify_key_of(delta.key());
242		let encoded = delta.key().encode();
243
244		match delta {
245			Delta::Set {
246				bytes,
247				..
248			} => {
249				writes.push(MultiWrite {
250					key: encoded.clone(),
251					value_bytes: bytes.len() as u64,
252				});
253				batches.entry(table).or_default().push((encoded, Some(bytes.0.clone())));
254			}
255			Delta::Remove {
256				..
257			} => {
258				deletes.push(MultiDelete {
259					key: encoded.clone(),
260				});
261				batches.entry(table).or_default().push((encoded, None));
262			}
263		}
264	}
265
266	ClassifiedDeltas {
267		writes,
268		deletes,
269		batches,
270	}
271}
272
273impl StandardMultiStore {
274	pub fn get_many(
275		&self,
276		keys: &[EncodedKey],
277		version: CommitVersion,
278	) -> Result<HashMap<EncodedKey, MultiVersionRow>> {
279		let mut by_table: HashMap<EntryKind, Vec<ClassifiedKey<'_>>> = HashMap::new();
280		for key in keys {
281			let (table, storage_key) = storage_key(key);
282			by_table.entry(table).or_default().push(ClassifiedKey {
283				key,
284				storage_key,
285			});
286		}
287
288		let mut out: HashMap<EncodedKey, MultiVersionRow> = HashMap::new();
289		for (table, table_keys) in by_table {
290			self.get_many_for_table(table, &table_keys, version, &mut out)?;
291		}
292
293		Ok(out)
294	}
295
296	pub fn get_many_versioned(
297		&self,
298		keys: &[EncodedKey],
299		version: CommitVersion,
300	) -> Result<HashMap<EncodedKey, VersionedGetResult>> {
301		let mut by_table: HashMap<EntryKind, Vec<ClassifiedKey<'_>>> = HashMap::new();
302		for key in keys {
303			let (table, storage_key) = storage_key(key);
304			by_table.entry(table).or_default().push(ClassifiedKey {
305				key,
306				storage_key,
307			});
308		}
309
310		let mut out: HashMap<EncodedKey, VersionedGetResult> = HashMap::new();
311		for (table, table_keys) in by_table {
312			let (commit_results, read_aligned, persistent_aligned) =
313				self.probe_tiers(table, &table_keys, version)?;
314			for (i, routed) in table_keys.iter().enumerate() {
315				let resolved = match &commit_results[i] {
316					VersionedGetResult::NotFound => match &read_aligned[i] {
317						VersionedGetResult::NotFound => &persistent_aligned[i],
318						found => found,
319					},
320					found => found,
321				};
322				out.insert(routed.key.clone(), resolved.clone());
323			}
324		}
325
326		Ok(out)
327	}
328
329	#[inline]
330	fn probe_tiers(
331		&self,
332		table: EntryKind,
333		table_keys: &[ClassifiedKey<'_>],
334		version: CommitVersion,
335	) -> Result<(Vec<VersionedGetResult>, Vec<VersionedGetResult>, Vec<VersionedGetResult>)> {
336		let key_slices: Vec<&[u8]> = table_keys.iter().map(|routed| routed.key.as_ref()).collect();
337
338		let commit_results = self.probe_commit_batch(table, &key_slices, version)?;
339		let (read_aligned, persistent_aligned) = self.resolve_misses_through_read_and_persistent(
340			table,
341			table_keys,
342			&key_slices,
343			&commit_results,
344			version,
345		)?;
346
347		Ok((commit_results, read_aligned, persistent_aligned))
348	}
349
350	#[inline]
351	fn get_many_for_table(
352		&self,
353		table: EntryKind,
354		table_keys: &[ClassifiedKey<'_>],
355		version: CommitVersion,
356		out: &mut HashMap<EncodedKey, MultiVersionRow>,
357	) -> Result<()> {
358		let (commit_results, read_aligned, persistent_aligned) =
359			self.probe_tiers(table, table_keys, version)?;
360
361		reifydb_assertions! {
362			let n = table_keys.len();
363			assert!(
364				commit_results.len() == n && read_aligned.len() == n && persistent_aligned.len() == n,
365				"per-tier result vectors must stay index-aligned with the table's keys, otherwise collect_resolved_rows \
366				 reads a tier result for the wrong key and returns mismatched rows (keys={n}, commit={}, read={}, persistent={})",
367				commit_results.len(),
368				read_aligned.len(),
369				persistent_aligned.len()
370			);
371		}
372
373		self.collect_resolved_rows(table_keys, &commit_results, &read_aligned, &persistent_aligned, out);
374		Ok(())
375	}
376
377	#[inline]
378	fn probe_commit_batch(
379		&self,
380		table: EntryKind,
381		key_slices: &[&[u8]],
382		version: CommitVersion,
383	) -> Result<Vec<VersionedGetResult>> {
384		self.commit.get_many(table, key_slices, version)
385	}
386
387	#[inline]
388	fn resolve_misses_through_read_and_persistent(
389		&self,
390		table: EntryKind,
391		table_keys: &[ClassifiedKey<'_>],
392		key_slices: &[&[u8]],
393		commit_results: &[VersionedGetResult],
394		version: CommitVersion,
395	) -> Result<(Vec<VersionedGetResult>, Vec<VersionedGetResult>)> {
396		let mut read_aligned = vec![VersionedGetResult::NotFound; key_slices.len()];
397		let mut persistent_idx: Vec<usize> = Vec::new();
398		let mut persistent_slices: Vec<&[u8]> = Vec::new();
399		for (i, result) in commit_results.iter().enumerate() {
400			if !matches!(result, VersionedGetResult::NotFound) {
401				continue;
402			}
403			let read_hit = self
404				.point
405				.as_ref()
406				.map(|c| c.get(table, table_keys[i].storage_key, table_keys[i].key, version))
407				.unwrap_or(VersionedGetResult::NotFound);
408			match read_hit {
409				VersionedGetResult::Value {
410					value,
411					version: v,
412				} => {
413					read_aligned[i] = VersionedGetResult::Value {
414						value,
415						version: v,
416					};
417				}
418				VersionedGetResult::Tombstone => {
419					read_aligned[i] = VersionedGetResult::Tombstone;
420				}
421				VersionedGetResult::NotFound => {
422					let maybe = match &self.persistent {
423						Some(persistent) => {
424							persistent.filter().may_contain((table, table_keys[i].key))
425						}
426						None => false,
427					};
428					if maybe {
429						persistent_idx.push(i);
430						persistent_slices.push(key_slices[i]);
431					}
432				}
433			}
434		}
435
436		let mut persistent_aligned = vec![VersionedGetResult::NotFound; key_slices.len()];
437		if !persistent_slices.is_empty()
438			&& let Some(persistent) = &self.persistent
439		{
440			self.persistent_probes.fetch_add(persistent_slices.len() as u64, Ordering::Relaxed);
441			let persistent_results = persistent.get_many(table, &persistent_slices, version)?;
442			for (slot, result) in persistent_idx.into_iter().zip(persistent_results) {
443				if matches!(result, VersionedGetResult::NotFound) {
444					self.persistent_absent.fetch_add(1, Ordering::Relaxed);
445				}
446				if let VersionedGetResult::Value {
447					value,
448					version: v,
449				} = &result && let Some(point) = &self.point
450				{
451					point.insert(
452						table,
453						table_keys[slot].storage_key,
454						table_keys[slot].key.clone(),
455						*v,
456						Some(value.clone()),
457					);
458				}
459				persistent_aligned[slot] = result;
460			}
461		}
462
463		Ok((read_aligned, persistent_aligned))
464	}
465
466	#[inline]
467	fn collect_resolved_rows(
468		&self,
469		table_keys: &[ClassifiedKey<'_>],
470		commit_results: &[VersionedGetResult],
471		read_aligned: &[VersionedGetResult],
472		persistent_aligned: &[VersionedGetResult],
473		out: &mut HashMap<EncodedKey, MultiVersionRow>,
474	) {
475		for (i, routed) in table_keys.iter().enumerate() {
476			let resolved = match &commit_results[i] {
477				VersionedGetResult::Value {
478					value,
479					version: v,
480				} => Some((value.clone(), *v)),
481				VersionedGetResult::Tombstone => None,
482				VersionedGetResult::NotFound => match &read_aligned[i] {
483					VersionedGetResult::Value {
484						value,
485						version: v,
486					} => Some((value.clone(), *v)),
487					VersionedGetResult::Tombstone => None,
488					VersionedGetResult::NotFound => match &persistent_aligned[i] {
489						VersionedGetResult::Value {
490							value,
491							version: v,
492						} => Some((value.clone(), *v)),
493						_ => None,
494					},
495				},
496			};
497
498			if let Some((value, v)) = resolved {
499				out.insert(
500					routed.key.clone(),
501					MultiVersionRow {
502						key: routed.key.clone(),
503						bytes: EncodedBytes(value),
504						version: v,
505					},
506				);
507			}
508		}
509	}
510
511	#[inline]
512	fn update_read_cache_on_commit(&self, batches: &TierBatch) {
513		if self.point.is_none() && self.range.is_none() {
514			return;
515		}
516		for (table, entries) in batches.iter() {
517			for (key, _) in entries {
518				if let Some(range) = &self.range {
519					range.invalidate(*table, key);
520				}
521				if let Some(point) = &self.point {
522					point.invalidate(*table, storage_key(key).1, key);
523				}
524			}
525		}
526	}
527
528	#[inline]
529	fn write_batches(&self, version: CommitVersion, batches: TierBatch) -> Result<()> {
530		self.commit.set(version, batches)
531	}
532
533	#[inline]
534	fn emit_commit_metrics(&self, writes: Vec<MultiWrite>, deletes: Vec<MultiDelete>, version: CommitVersion) {
535		if writes.is_empty() && deletes.is_empty() {
536			return;
537		}
538		self.event_bus.emit(MultiCommittedEvent::new(writes, deletes, version));
539	}
540}
541
542#[derive(Debug, Clone, Default)]
543pub struct MultiVersionRangeCursor {
544	pub commit: RangeCursor,
545
546	pub persistent: RangeCursor,
547
548	pub exhausted: bool,
549
550	persistent_recheck_spent: bool,
551
552	materialize: bool,
553}
554
555impl MultiVersionRangeCursor {
556	pub fn new() -> Self {
557		Self {
558			materialize: true,
559			..Default::default()
560		}
561	}
562
563	pub fn cold() -> Self {
564		Self {
565			materialize: false,
566			..Default::default()
567		}
568	}
569
570	pub fn is_exhausted(&self) -> bool {
571		self.exhausted
572	}
573}
574
575pub struct TierScanQuery<'a> {
576	pub table: EntryKind,
577	pub start: &'a [u8],
578	pub end: &'a [u8],
579	pub scope: MultiVersionScope,
580	pub range: &'a EncodedKeyRange,
581}
582
583pub fn scan_tier_chunk<S: TierStorage>(
584	storage: &S,
585	cursor: &mut RangeCursor,
586	scan: &TierScanQuery,
587	collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
588) -> Result<()> {
589	let batch = storage.range_next(
590		scan.table,
591		cursor,
592		Bound::Included(scan.start),
593		Bound::Included(scan.end),
594		scan.scope,
595		TIER_SCAN_CHUNK_SIZE,
596	)?;
597	merge_tier_batch(batch, scan.range, collected)
598}
599
600pub fn scan_tier_chunk_rev<S: TierStorage>(
601	storage: &S,
602	cursor: &mut RangeCursor,
603	scan: &TierScanQuery,
604	collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
605) -> Result<()> {
606	let batch = storage.range_rev_next(
607		scan.table,
608		cursor,
609		Bound::Included(scan.start),
610		Bound::Included(scan.end),
611		scan.scope,
612		TIER_SCAN_CHUNK_SIZE,
613	)?;
614	merge_tier_batch(batch, scan.range, collected)
615}
616
617#[inline]
618fn merge_tier_batch(
619	batch: RangeBatch,
620	range: &EncodedKeyRange,
621	collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
622) -> Result<()> {
623	for entry in batch.entries {
624		if !range.contains(&entry.key) {
625			continue;
626		}
627
628		match collected.entry(entry.key) {
629			Entry::Vacant(slot) => {
630				slot.insert((entry.version, entry.value));
631			}
632			Entry::Occupied(mut slot) => {
633				if entry.version > slot.get().0 {
634					slot.insert((entry.version, entry.value));
635				}
636			}
637		}
638	}
639
640	Ok(())
641}
642
643#[inline]
644fn decode_range_rows(
645	collected: BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
646) -> Result<Vec<MultiVersionRow<TaggedKey>>> {
647	let mut items = Vec::with_capacity(collected.len());
648	for (key, (version, value)) in collected {
649		let Some(bytes) = value else {
650			continue;
651		};
652		let key = TaggedKey::decode(&key).ok_or_else(|| {
653			error!(internal(format!("a stored key names no live kind: {}", display(key.as_ref()))))
654		})?;
655		items.push(MultiVersionRow {
656			key,
657			bytes: EncodedBytes(bytes),
658			version,
659		});
660	}
661	Ok(items)
662}
663
664#[inline]
665pub fn collected_to_batch(
666	collected: BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
667	has_more: bool,
668) -> Result<MultiVersionBatch<TaggedKey>> {
669	Ok(MultiVersionBatch {
670		items: decode_range_rows(collected)?,
671		has_more,
672	})
673}
674
675#[inline]
676fn step_all_tiers(
677	buffer: Option<&CommitStore>,
678	buffer_cursor: &mut RangeCursor,
679	persistent: Option<&MultiPersistentTier>,
680	persistent_cursor: &mut RangeCursor,
681	scan: &TierScanQuery,
682	collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
683) -> Result<()> {
684	if let Some(s) = buffer
685		&& !buffer_cursor.is_exhausted()
686	{
687		scan_tier_chunk(s, buffer_cursor, scan, collected)?;
688	}
689	if let Some(s) = persistent
690		&& !persistent_cursor.is_exhausted()
691	{
692		scan_tier_chunk(s, persistent_cursor, scan, collected)?;
693	}
694	Ok(())
695}
696
697pub fn scan_tiers_latest(
698	buffer: Option<&CommitStore>,
699	persistent: Option<&MultiPersistentTier>,
700	range: EncodedKeyRange,
701	scope: MultiVersionScope,
702	max_keys: usize,
703) -> Result<MultiVersionBatch<TaggedKey>> {
704	let table = classify_key_range(&range);
705	let (start, end) = make_range_bounds(&range);
706	let scan = TierScanQuery {
707		table,
708		start: &start,
709		end: &end,
710		scope,
711		range: &range,
712	};
713
714	let mut collected: BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)> = BTreeMap::new();
715	let mut buffer_cursor = if buffer.is_none() {
716		RangeCursor::start_exhausted()
717	} else {
718		RangeCursor::new()
719	};
720	let mut persistent_cursor = if persistent.is_none() {
721		RangeCursor::start_exhausted()
722	} else {
723		RangeCursor::new()
724	};
725	let mut exhausted = false;
726
727	while collected.len() < max_keys {
728		step_all_tiers(buffer, &mut buffer_cursor, persistent, &mut persistent_cursor, &scan, &mut collected)?;
729		if buffer_cursor.is_exhausted() && persistent_cursor.is_exhausted() {
730			exhausted = true;
731			break;
732		}
733	}
734
735	collected_to_batch(collected, !exhausted)
736}
737
738impl StandardMultiStore {
739	pub fn range_next(
740		&self,
741		cursor: &mut MultiVersionRangeCursor,
742		range: EncodedKeyRange,
743		scope: MultiVersionScope,
744		batch_size: u64,
745	) -> Result<MultiVersionBatch<TaggedKey>> {
746		if cursor.exhausted {
747			return Ok(MultiVersionBatch {
748				items: Vec::new(),
749				has_more: false,
750			});
751		}
752
753		mark_unconfigured_exhausted(self, cursor);
754
755		let table = classify_key_range(&range);
756		let (start, end) = make_range_bounds(&range);
757		let batch_size = batch_size as usize;
758		let scan = TierScanQuery {
759			table,
760			start: &start,
761			end: &end,
762			scope,
763			range: &range,
764		};
765
766		let mut collected: BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)> = BTreeMap::new();
767
768		while collected.len() < batch_size {
769			if !cursor.commit.is_exhausted() {
770				scan_tier_chunk(&self.commit, &mut cursor.commit, &scan, &mut collected)?;
771			}
772
773			if self.persistent.is_some() && !cursor.persistent.is_exhausted() {
774				self.step_persistent_cached(&scan, cursor, &mut collected, false)?;
775			}
776
777			if cursor.commit.is_exhausted() && cursor.persistent.is_exhausted() {
778				if reread_persistent_absent_before_its_table(cursor) {
779					continue;
780				}
781				cursor.exhausted = true;
782				break;
783			}
784		}
785
786		apply_forward_horizon(cursor, &mut collected);
787
788		let items = decode_range_rows(collected)?;
789
790		let has_more = !cursor.exhausted;
791
792		Ok(MultiVersionBatch {
793			items,
794			has_more,
795		})
796	}
797
798	pub fn range(
799		&self,
800		range: EncodedKeyRange,
801		scope: MultiVersionScope,
802		batch_size: usize,
803	) -> MultiVersionRangeIter {
804		MultiVersionRangeIter {
805			store: self.clone(),
806			cursor: MultiVersionRangeCursor::new(),
807			range,
808			scope,
809			batch_size,
810			current_batch: Vec::new().into_iter(),
811		}
812	}
813
814	pub fn range_persistence(
815		&self,
816		range: EncodedKeyRange,
817		scope: MultiVersionScope,
818		batch_size: usize,
819	) -> MultiVersionRangeIter {
820		MultiVersionRangeIter {
821			store: self.clone(),
822			cursor: MultiVersionRangeCursor::cold(),
823			range,
824			scope,
825			batch_size,
826			current_batch: Vec::new().into_iter(),
827		}
828	}
829
830	pub fn range_rev(
831		&self,
832		range: EncodedKeyRange,
833		scope: MultiVersionScope,
834		batch_size: usize,
835	) -> MultiVersionRangeRevIter {
836		MultiVersionRangeRevIter {
837			store: self.clone(),
838			cursor: MultiVersionRangeCursor::new(),
839			range,
840			scope,
841			batch_size,
842			current_batch: Vec::new().into_iter(),
843		}
844	}
845
846	pub fn range_rev_persistence(
847		&self,
848		range: EncodedKeyRange,
849		scope: MultiVersionScope,
850		batch_size: usize,
851	) -> MultiVersionRangeRevIter {
852		MultiVersionRangeRevIter {
853			store: self.clone(),
854			cursor: MultiVersionRangeCursor::cold(),
855			range,
856			scope,
857			batch_size,
858			current_batch: Vec::new().into_iter(),
859		}
860	}
861
862	fn range_rev_next(
863		&self,
864		cursor: &mut MultiVersionRangeCursor,
865		range: EncodedKeyRange,
866		scope: MultiVersionScope,
867		batch_size: u64,
868	) -> Result<MultiVersionBatch<TaggedKey>> {
869		if cursor.exhausted {
870			return Ok(MultiVersionBatch {
871				items: Vec::new(),
872				has_more: false,
873			});
874		}
875
876		mark_unconfigured_exhausted(self, cursor);
877
878		let table = classify_key_range(&range);
879		let (start, end) = make_range_bounds(&range);
880		let batch_size = batch_size as usize;
881		let scan = TierScanQuery {
882			table,
883			start: &start,
884			end: &end,
885			scope,
886			range: &range,
887		};
888
889		let mut collected: BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)> = BTreeMap::new();
890
891		while collected.len() < batch_size {
892			if !cursor.commit.is_exhausted() {
893				scan_tier_chunk_rev(&self.commit, &mut cursor.commit, &scan, &mut collected)?;
894			}
895
896			if self.persistent.is_some() && !cursor.persistent.is_exhausted() {
897				self.step_persistent_cached(&scan, cursor, &mut collected, true)?;
898			}
899
900			if cursor.commit.is_exhausted() && cursor.persistent.is_exhausted() {
901				if reread_persistent_absent_before_its_table(cursor) {
902					continue;
903				}
904				cursor.exhausted = true;
905				break;
906			}
907		}
908
909		apply_reverse_horizon(cursor, &mut collected);
910
911		let mut items = decode_range_rows(collected)?;
912		items.reverse();
913
914		let has_more = !cursor.exhausted;
915
916		Ok(MultiVersionBatch {
917			items,
918			has_more,
919		})
920	}
921
922	fn step_persistent_cached(
923		&self,
924		scan: &TierScanQuery,
925		cursor: &mut MultiVersionRangeCursor,
926		collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
927		descending: bool,
928	) -> Result<()> {
929		let Some(persistent) = &self.persistent else {
930			return Ok(());
931		};
932
933		match self.serve_from_read_cache(scan, cursor, collected, descending) {
934			Some(served) => served?,
935			None => self.scan_persistent_chunk(persistent, scan, cursor, collected, descending)?,
936		}
937
938		Ok(())
939	}
940
941	#[inline]
942	fn serve_from_read_cache(
943		&self,
944		scan: &TierScanQuery,
945		cursor: &mut MultiVersionRangeCursor,
946		collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
947		descending: bool,
948	) -> Option<Result<()>> {
949		let (Some(range), true) = (&self.range, scan.table.caches_ranges()) else {
950			return None;
951		};
952		match range.serve_persistent_chunk(
953			scan.table,
954			&mut cursor.persistent,
955			scan.start,
956			scan.end,
957			scan.scope,
958			TIER_SCAN_CHUNK_SIZE,
959			descending,
960		) {
961			ServedChunk::Served(batch) => Some(merge_tier_batch(batch, scan.range, collected)),
962			ServedChunk::Gap => None,
963		}
964	}
965
966	#[inline]
967	fn scan_persistent_chunk(
968		&self,
969		persistent: &MultiPersistentTier,
970		scan: &TierScanQuery,
971		cursor: &mut MultiVersionRangeCursor,
972		collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
973		descending: bool,
974	) -> Result<()> {
975		let resumed_at = cursor.persistent.last_key().cloned();
976		let batch = if descending {
977			persistent.range_rev_next(
978				scan.table,
979				&mut cursor.persistent,
980				Bound::Included(scan.start),
981				Bound::Included(scan.end),
982				scan.scope,
983				TIER_SCAN_CHUNK_SIZE,
984			)?
985		} else {
986			persistent.range_next(
987				scan.table,
988				&mut cursor.persistent,
989				Bound::Included(scan.start),
990				Bound::Included(scan.end),
991				scan.scope,
992				TIER_SCAN_CHUNK_SIZE,
993			)?
994		};
995		if !descending && cursor.materialize {
996			self.materialize_scanned_chunk(
997				persistent,
998				scan,
999				resumed_at.as_ref(),
1000				&cursor.persistent,
1001				&batch,
1002			)?;
1003		}
1004		merge_tier_batch(batch, scan.range, collected)
1005	}
1006
1007	#[inline]
1008	fn materialize_scanned_chunk(
1009		&self,
1010		persistent: &MultiPersistentTier,
1011		scan: &TierScanQuery,
1012		resumed_at: Option<&EncodedKey>,
1013		cursor: &RangeCursor,
1014		batch: &RangeBatch,
1015	) -> Result<()> {
1016		let (Some(range), true) = (&self.range, scan.table.caches_ranges()) else {
1017			return Ok(());
1018		};
1019		let MultiVersionScope::AsOf {
1020			read: at,
1021		} = scan.scope
1022		else {
1023			return Ok(());
1024		};
1025		if at < persistent.install_floor()? {
1026			return Ok(());
1027		}
1028		let range_start = EncodedKey::new(scan.start);
1029		let lo = match resumed_at {
1030			Some(last) => match resume_after::<StorageRowKey>(scan.table, last) {
1031				Some(next) => next.max(range_start),
1032				None => return Ok(()),
1033			},
1034			None => range_start,
1035		};
1036		let through = match (cursor.scanned_to_end(), cursor.is_exhausted(), cursor.last_key()) {
1037			(true, _, _) => EncodedKey::new(scan.end),
1038			(false, true, _) => return Ok(()),
1039			(false, false, Some(last)) => last.clone(),
1040			(false, false, None) => return Ok(()),
1041		};
1042		range.materialize_scanned_chunk(scan.table, &lo, &through, &batch.entries);
1043		Ok(())
1044	}
1045}
1046
1047fn reread_persistent_absent_before_its_table(cursor: &mut MultiVersionRangeCursor) -> bool {
1048	if cursor.persistent_recheck_spent || cursor.persistent.stop() != Some(&RangeStop::AbsentTable) {
1049		return false;
1050	}
1051	cursor.persistent_recheck_spent = true;
1052	cursor.persistent.reopen();
1053	true
1054}
1055
1056fn mark_unconfigured_exhausted(store: &StandardMultiStore, cursor: &mut MultiVersionRangeCursor) {
1057	if store.persistent.is_none() {
1058		cursor.persistent.finish();
1059	}
1060}
1061
1062fn apply_forward_horizon(
1063	cursor: &mut MultiVersionRangeCursor,
1064	collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
1065) {
1066	let horizon = forward_horizon(cursor);
1067	if let Some(h) = horizon {
1068		collected.retain(|k, _| k.as_slice() <= h.as_slice());
1069		rewind_over_advanced_forward(cursor, &h);
1070	}
1071}
1072
1073fn apply_reverse_horizon(
1074	cursor: &mut MultiVersionRangeCursor,
1075	collected: &mut BTreeMap<EncodedKey, (CommitVersion, Option<CowVec<u8>>)>,
1076) {
1077	let horizon = reverse_horizon(cursor);
1078	if let Some(h) = horizon {
1079		collected.retain(|k, _| k.as_slice() >= h.as_slice());
1080		rewind_over_advanced_reverse(cursor, &h);
1081	}
1082}
1083
1084fn forward_horizon(cursor: &MultiVersionRangeCursor) -> Option<EncodedKey> {
1085	let mut horizon: Option<EncodedKey> = None;
1086	for tier in [&cursor.commit, &cursor.persistent] {
1087		if tier.is_exhausted() {
1088			continue;
1089		}
1090		let last = match tier.last_key() {
1091			Some(k) => k.clone(),
1092
1093			None => return None,
1094		};
1095		horizon = Some(match horizon {
1096			None => last,
1097			Some(prev) => {
1098				if last.as_slice() < prev.as_slice() {
1099					last
1100				} else {
1101					prev
1102				}
1103			}
1104		});
1105	}
1106	horizon
1107}
1108
1109fn reverse_horizon(cursor: &MultiVersionRangeCursor) -> Option<EncodedKey> {
1110	let mut horizon: Option<EncodedKey> = None;
1111	for tier in [&cursor.commit, &cursor.persistent] {
1112		if tier.is_exhausted() {
1113			continue;
1114		}
1115		let last = match tier.last_key() {
1116			Some(k) => k.clone(),
1117			None => return None,
1118		};
1119		horizon = Some(match horizon {
1120			None => last,
1121			Some(prev) => {
1122				if last.as_slice() > prev.as_slice() {
1123					last
1124				} else {
1125					prev
1126				}
1127			}
1128		});
1129	}
1130	horizon
1131}
1132
1133fn rewind_over_advanced_forward(cursor: &mut MultiVersionRangeCursor, horizon: &EncodedKey) {
1134	for tier in [&mut cursor.commit, &mut cursor.persistent] {
1135		if let Some(last) = tier.last_key()
1136			&& last.as_slice() > horizon.as_slice()
1137		{
1138			tier.resume(horizon.clone());
1139		}
1140	}
1141}
1142
1143fn rewind_over_advanced_reverse(cursor: &mut MultiVersionRangeCursor, horizon: &EncodedKey) {
1144	for tier in [&mut cursor.commit, &mut cursor.persistent] {
1145		if let Some(last) = tier.last_key()
1146			&& last.as_slice() < horizon.as_slice()
1147		{
1148			tier.resume(horizon.clone());
1149		}
1150	}
1151}
1152
1153impl MultiVersionGetPrevious for StandardMultiStore {
1154	fn get_previous_version(
1155		&self,
1156		key: &TaggedKey,
1157		before_version: CommitVersion,
1158	) -> Result<Option<MultiVersionRow<TaggedKey>>> {
1159		if before_version.0 == 0 {
1160			return Ok(None);
1161		}
1162
1163		let (table, storage_key) = storage_key_of(key);
1164		let encoded = key.encode();
1165		reifydb_assertions! {
1166			assert!(
1167				before_version.0 >= 1,
1168				"the before_version==0 guard must precede this subtraction, otherwise before_version.0 - 1 \
1169				 wraps to u64::MAX and the probe reads the latest version instead of the previous one \
1170				 (before_version={})",
1171				before_version.0
1172			);
1173		}
1174		let prev_version = CommitVersion(before_version.0 - 1);
1175
1176		let found = if let Some(found) = self.previous_probe_commit(table, &encoded, prev_version)? {
1177			found
1178		} else if let Some(found) = self.previous_probe_read(table, storage_key, &encoded, prev_version) {
1179			found
1180		} else {
1181			self.previous_probe_persistent(table, storage_key, &encoded, prev_version)?.flatten()
1182		};
1183
1184		Ok(found.map(|row| MultiVersionRow {
1185			key: key.clone(),
1186			bytes: row.bytes,
1187			version: row.version,
1188		}))
1189	}
1190}
1191
1192impl StandardMultiStore {
1193	#[inline]
1194	fn previous_probe_commit(
1195		&self,
1196		table: EntryKind,
1197		key: &EncodedKey,
1198		prev_version: CommitVersion,
1199	) -> Result<Option<Option<MultiVersionRow>>> {
1200		Ok(match self.commit.get(table, key.as_ref(), prev_version)? {
1201			VersionedGetResult::Value {
1202				value,
1203				version,
1204			} => Some(Some(MultiVersionRow {
1205				key: key.clone(),
1206				bytes: EncodedBytes(CowVec::new(value.to_vec())),
1207				version,
1208			})),
1209			VersionedGetResult::Tombstone => Some(None),
1210			VersionedGetResult::NotFound => None,
1211		})
1212	}
1213
1214	#[inline]
1215	fn previous_probe_read(
1216		&self,
1217		table: EntryKind,
1218		storage_key: Option<StorageKey>,
1219		key: &EncodedKey,
1220		prev_version: CommitVersion,
1221	) -> Option<Option<MultiVersionRow>> {
1222		let point = self.point.as_ref()?;
1223		match point.get(table, storage_key, key, prev_version) {
1224			VersionedGetResult::Value {
1225				value,
1226				version,
1227			} => Some(Some(MultiVersionRow {
1228				key: key.clone(),
1229				bytes: EncodedBytes(CowVec::new(value.to_vec())),
1230				version,
1231			})),
1232			VersionedGetResult::Tombstone => Some(None),
1233			VersionedGetResult::NotFound => None,
1234		}
1235	}
1236
1237	#[inline]
1238	fn previous_probe_persistent(
1239		&self,
1240		table: EntryKind,
1241		storage_key: Option<StorageKey>,
1242		key: &EncodedKey,
1243		prev_version: CommitVersion,
1244	) -> Result<Option<Option<MultiVersionRow>>> {
1245		let Some(persistent) = &self.persistent else {
1246			return Ok(None);
1247		};
1248		if !persistent.filter().may_contain((table, key)) {
1249			return Ok(None);
1250		}
1251		self.persistent_probes.fetch_add(1, Ordering::Relaxed);
1252		Ok(match persistent.get(table, key.as_ref(), prev_version)? {
1253			VersionedGetResult::Value {
1254				value,
1255				version,
1256			} => {
1257				if let Some(point) = &self.point {
1258					point.insert(table, storage_key, key.clone(), version, Some(value.clone()));
1259				}
1260				Some(Some(MultiVersionRow {
1261					key: key.clone(),
1262					bytes: EncodedBytes(CowVec::new(value.to_vec())),
1263					version,
1264				}))
1265			}
1266			VersionedGetResult::Tombstone => Some(None),
1267			VersionedGetResult::NotFound => {
1268				self.persistent_absent.fetch_add(1, Ordering::Relaxed);
1269				None
1270			}
1271		})
1272	}
1273}
1274
1275impl MultiVersionStore for StandardMultiStore {}
1276
1277pub struct MultiVersionRangeIter {
1278	store: StandardMultiStore,
1279	cursor: MultiVersionRangeCursor,
1280	range: EncodedKeyRange,
1281	scope: MultiVersionScope,
1282	batch_size: usize,
1283	current_batch: vec::IntoIter<MultiVersionRow<TaggedKey>>,
1284}
1285
1286impl Iterator for MultiVersionRangeIter {
1287	type Item = Result<MultiVersionRow<TaggedKey>>;
1288
1289	fn next(&mut self) -> Option<Self::Item> {
1290		if let Some(item) = self.current_batch.next() {
1291			return Some(Ok(item));
1292		}
1293
1294		if self.cursor.exhausted {
1295			return None;
1296		}
1297
1298		match self.store.range_next(&mut self.cursor, self.range.clone(), self.scope, self.batch_size as u64) {
1299			Ok(batch) => {
1300				if batch.items.is_empty() {
1301					if self.cursor.exhausted {
1302						return None;
1303					}
1304					return self.next();
1305				}
1306				self.current_batch = batch.items.into_iter();
1307				self.next()
1308			}
1309			Err(e) => Some(Err(e)),
1310		}
1311	}
1312}
1313
1314pub struct NarrowRangeIter<L: PersistentRangeLayout> {
1315	store: StandardMultiStore,
1316	cursor: NarrowRangeCursor<L>,
1317	storage: StorageId,
1318	start: Bound<L>,
1319	end: Bound<L>,
1320	scope: MultiVersionScope,
1321	batch_size: usize,
1322	current_batch: vec::IntoIter<MultiVersionRow<L>>,
1323}
1324
1325pub type MultiVersionRowRangeIter = NarrowRangeIter<StorageRowKey>;
1326
1327pub type MultiVersionPartitionedRowRangeIter = NarrowRangeIter<StoragePartitionedRowKey>;
1328
1329impl<L: PersistentRangeLayout> Iterator for NarrowRangeIter<L> {
1330	type Item = Result<MultiVersionRow<L>>;
1331
1332	fn next(&mut self) -> Option<Self::Item> {
1333		if let Some(item) = self.current_batch.next() {
1334			return Some(Ok(item));
1335		}
1336
1337		if self.cursor.exhausted {
1338			return None;
1339		}
1340
1341		match self.store.range_next_narrow(
1342			&mut self.cursor,
1343			self.storage,
1344			self.start,
1345			self.end,
1346			self.scope,
1347			self.batch_size as u64,
1348		) {
1349			Ok(batch) => {
1350				if batch.items.is_empty() {
1351					if self.cursor.exhausted {
1352						return None;
1353					}
1354					return self.next();
1355				}
1356				self.current_batch = batch.items.into_iter();
1357				self.next()
1358			}
1359			Err(e) => Some(Err(e)),
1360		}
1361	}
1362}
1363
1364impl StandardMultiStore {
1365	pub fn range_row(
1366		&self,
1367		storage: StorageId,
1368		start: Bound<StorageRowKey>,
1369		end: Bound<StorageRowKey>,
1370		scope: MultiVersionScope,
1371		batch_size: usize,
1372	) -> MultiVersionRowRangeIter {
1373		self.range_narrow::<StorageRowKey>(storage, start, end, scope, batch_size)
1374	}
1375
1376	pub fn range_partitioned_row(
1377		&self,
1378		storage: StorageId,
1379		start: Bound<StoragePartitionedRowKey>,
1380		end: Bound<StoragePartitionedRowKey>,
1381		scope: MultiVersionScope,
1382		batch_size: usize,
1383	) -> MultiVersionPartitionedRowRangeIter {
1384		self.range_narrow::<StoragePartitionedRowKey>(storage, start, end, scope, batch_size)
1385	}
1386
1387	pub fn range_narrow<L: PersistentRangeLayout>(
1388		&self,
1389		storage: StorageId,
1390		start: Bound<L>,
1391		end: Bound<L>,
1392		scope: MultiVersionScope,
1393		batch_size: usize,
1394	) -> NarrowRangeIter<L> {
1395		NarrowRangeIter {
1396			store: self.clone(),
1397			cursor: NarrowRangeCursor::default(),
1398			storage,
1399			start,
1400			end,
1401			scope,
1402			batch_size,
1403			current_batch: Vec::new().into_iter(),
1404		}
1405	}
1406}
1407
1408pub struct MultiVersionRangeRevIter {
1409	store: StandardMultiStore,
1410	cursor: MultiVersionRangeCursor,
1411	range: EncodedKeyRange,
1412	scope: MultiVersionScope,
1413	batch_size: usize,
1414	current_batch: vec::IntoIter<MultiVersionRow<TaggedKey>>,
1415}
1416
1417impl Iterator for MultiVersionRangeRevIter {
1418	type Item = Result<MultiVersionRow<TaggedKey>>;
1419
1420	fn next(&mut self) -> Option<Self::Item> {
1421		if let Some(item) = self.current_batch.next() {
1422			return Some(Ok(item));
1423		}
1424
1425		if self.cursor.exhausted {
1426			return None;
1427		}
1428
1429		match self.store.range_rev_next(
1430			&mut self.cursor,
1431			self.range.clone(),
1432			self.scope,
1433			self.batch_size as u64,
1434		) {
1435			Ok(batch) => {
1436				if batch.items.is_empty() {
1437					if self.cursor.exhausted {
1438						return None;
1439					}
1440					return self.next();
1441				}
1442				self.current_batch = batch.items.into_iter();
1443				self.next()
1444			}
1445			Err(e) => Some(Err(e)),
1446		}
1447	}
1448}
1449
1450fn classify_key_range(range: &EncodedKeyRange) -> EntryKind {
1451	classify_range(range).unwrap_or(EntryKind::Multi)
1452}
1453
1454fn make_range_bounds(range: &EncodedKeyRange) -> (Vec<u8>, Vec<u8>) {
1455	let start = match &range.start {
1456		Bound::Included(key) => key.as_ref().to_vec(),
1457		Bound::Excluded(key) => key.as_ref().to_vec(),
1458		Bound::Unbounded => vec![],
1459	};
1460
1461	let end = match &range.end {
1462		Bound::Included(key) => key.as_ref().to_vec(),
1463		Bound::Excluded(key) => key.as_ref().to_vec(),
1464		Bound::Unbounded => vec![0xFFu8; 256],
1465	};
1466
1467	(start, end)
1468}
1469
1470#[cfg(all(test, feature = "sqlite", not(target_arch = "wasm32")))]
1471mod cache_tests {
1472	use std::{collections::HashMap, ops::Bound};
1473
1474	use reifydb_codec::{key::encoded::EncodedKey, row::bytes::EncodedBytes};
1475	use reifydb_core::{
1476		common::CommitVersion,
1477		delta::Delta,
1478		interface::{
1479			catalog::{flow::OperatorId, id::TableId, storage::StorageId},
1480			store::{
1481				EntryKind, EntryLayout, MultiVersionCommit, MultiVersionGet, classify_key, storage_key,
1482			},
1483		},
1484		key::{
1485			any::TaggedKey,
1486			operator::state::{GroupId, KeyspaceId, OperatorStateKey},
1487			row::{RowKey, RowKeyRange, StorageRowKey},
1488		},
1489	};
1490	use reifydb_store_commit::{MultiVersionScope, RangeStop, RawEntry, VersionedGetResult};
1491	use reifydb_value::{byte_size::ByteSize, cow_vec, util::cowvec::CowVec, value::row_number::RowNumber};
1492
1493	use super::{MultiVersionRangeCursor, RowRangeCursor};
1494	use crate::{
1495		store::StandardMultiStore,
1496		tier::{
1497			TierStorage,
1498			point::MultiPointConfig,
1499			range::{MultiRangeConfig, PartitionId},
1500		},
1501	};
1502
1503	const STORAGE: StorageId = StorageId::Table(TableId(1));
1504
1505	#[test]
1506	fn typed_row_scan_terminates_without_a_persistent_tier() {
1507		let store = StandardMultiStore::testing_memory();
1508		for n in 1..=5u64 {
1509			commit_row(&store, n, n);
1510		}
1511
1512		let mut cursor = RowRangeCursor::default();
1513		let batch = store
1514			.range_next_row(
1515				&mut cursor,
1516				STORAGE,
1517				Bound::Unbounded,
1518				Bound::Unbounded,
1519				MultiVersionScope::AsOf {
1520					read: CommitVersion(100),
1521				},
1522				16,
1523			)
1524			.unwrap();
1525
1526		assert_eq!(batch.items.len(), 5, "an in-memory store must serve its rows from the commit tier alone");
1527		assert!(!batch.has_more, "the scan must report itself over rather than spin on an unconfigured tier");
1528	}
1529
1530	#[test]
1531	fn typed_row_scan_returns_exactly_what_the_encoded_scan_returns() {
1532		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1533		for n in 1..=40u64 {
1534			commit_row(&store, n, n);
1535		}
1536		flush(&store, CommitVersion(20));
1537		for n in 41..=60u64 {
1538			commit_row(&store, n, n);
1539		}
1540
1541		let scope = MultiVersionScope::AsOf {
1542			read: CommitVersion(1000),
1543		};
1544
1545		let mut encoded_cursor = MultiVersionRangeCursor::new();
1546		let mut encoded: Vec<(u64, Vec<u8>, u64)> = Vec::new();
1547		loop {
1548			let batch = store
1549				.range_next(
1550					&mut encoded_cursor,
1551					RowKeyRange::scan_range(STORAGE, None).encode(),
1552					scope,
1553					16,
1554				)
1555				.unwrap();
1556			for item in &batch.items {
1557				let TaggedKey::Row(key) = &item.key else {
1558					panic!("the scan must yield row keys")
1559				};
1560				encoded.push((key.row.0, item.bytes.0.to_vec(), item.version.0));
1561			}
1562			if !batch.has_more {
1563				break;
1564			}
1565		}
1566
1567		let mut typed_cursor = RowRangeCursor::default();
1568		let mut typed: Vec<(u64, Vec<u8>, u64)> = Vec::new();
1569		loop {
1570			let batch = store
1571				.range_next_row(
1572					&mut typed_cursor,
1573					STORAGE,
1574					Bound::Unbounded,
1575					Bound::Unbounded,
1576					scope,
1577					16,
1578				)
1579				.unwrap();
1580			for item in &batch.items {
1581				typed.push((item.key.row().0, item.bytes.0.to_vec(), item.version.0));
1582			}
1583			if !batch.has_more {
1584				break;
1585			}
1586		}
1587
1588		assert_eq!(encoded.len(), 60, "the encoded scan must see every committed row or it is not a baseline");
1589		assert_eq!(typed, encoded, "the typed scan must be indistinguishable from the encoded one");
1590
1591		let resume = StorageRowKey::new(RowNumber(50));
1592		let mut resumed_cursor = RowRangeCursor::default();
1593		let mut resumed: Vec<u64> = Vec::new();
1594		loop {
1595			let batch = store
1596				.range_next_row(
1597					&mut resumed_cursor,
1598					STORAGE,
1599					Bound::Excluded(resume),
1600					Bound::Unbounded,
1601					scope,
1602					16,
1603				)
1604				.unwrap();
1605			for item in &batch.items {
1606				resumed.push(item.key.row().0);
1607			}
1608			if !batch.has_more {
1609				break;
1610			}
1611		}
1612
1613		let expected: Vec<u64> = (1..=49).rev().collect();
1614		assert_eq!(resumed, expected, "an excluded start must drop the key itself and every row before it");
1615	}
1616
1617	fn commit_row(store: &StandardMultiStore, n: u64, version: u64) {
1618		MultiVersionCommit::commit(
1619			store,
1620			cow_vec![Delta::Set {
1621				key: TaggedKey::from(RowKey::new(STORAGE, n)),
1622				bytes: EncodedBytes(CowVec::new(format!("v{n}").into_bytes())),
1623			}],
1624			CommitVersion(version),
1625		)
1626		.unwrap();
1627	}
1628
1629	fn flush(store: &StandardMultiStore, cutoff: CommitVersion) {
1630		let commit = store.commit();
1631		for kind in commit.list_all_entry_kinds().unwrap() {
1632			let (to_persist, to_compact, _consumed, _more) =
1633				commit.collect_evictable_below(kind, cutoff, ByteSize::from_bytes(u64::MAX));
1634			if to_compact.is_empty() {
1635				continue;
1636			}
1637			if !to_persist.is_empty() {
1638				let persistent = store.persistent().expect("persistent tier");
1639				let mut by_version: HashMap<
1640					CommitVersion,
1641					HashMap<EntryKind, Vec<(EncodedKey, Option<CowVec<u8>>)>>,
1642				> = HashMap::new();
1643				for (key, version, value) in to_persist {
1644					by_version
1645						.entry(version)
1646						.or_default()
1647						.entry(kind)
1648						.or_default()
1649						.push((key, value));
1650				}
1651				for (version, batch) in by_version {
1652					persistent.set(version, batch).unwrap();
1653				}
1654			}
1655			for evicted in &to_compact {
1656				store.invalidate_read_key(kind, &evicted.key);
1657			}
1658			commit.compact(HashMap::from([(
1659				kind,
1660				to_compact.into_iter().map(|e| (e.key, e.version)).collect(),
1661			)]))
1662			.unwrap();
1663		}
1664	}
1665
1666	#[test]
1667	fn a_full_scan_claims_every_bucket_it_walks_to_the_edge() {
1668		const HEAVY: u64 = 192;
1669		const LIGHT: u64 = 20;
1670		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1671
1672		for n in 1..=HEAVY {
1673			commit_row(&store, n, 1);
1674		}
1675		for n in 0..LIGHT {
1676			commit_row(&store, (1u64 << 16) + n, 1);
1677		}
1678		flush(&store, CommitVersion(1));
1679
1680		let range = store.range.clone().expect("range tier configured");
1681		let kind = EntryKind::Source(STORAGE, EntryLayout::Row);
1682		let heavy = PartitionId::of(kind, &StorageRowKey::new(RowNumber(1)));
1683		let light = PartitionId::of(kind, &StorageRowKey::new(RowNumber(1u64 << 16)));
1684		assert_ne!(heavy, light, "the two row groups must land in different buckets");
1685		assert_eq!(range.complete_partitions().iter().sum::<usize>(), 0, "nothing is claimed before the scan");
1686
1687		let scanned = store
1688			.range(
1689				RowKey::full_scan(STORAGE).encode(),
1690				MultiVersionScope::AsOf {
1691					read: CommitVersion(10),
1692				},
1693				32,
1694			)
1695			.collect::<Result<Vec<_>, _>>()
1696			.unwrap();
1697		assert_eq!(scanned.len() as u64, HEAVY + LIGHT, "the scan returns every row");
1698
1699		assert_eq!(
1700			range.complete_partitions().iter().sum::<usize>(),
1701			2,
1702			"both buckets the scan walked to their edge must be claimed"
1703		);
1704	}
1705
1706	#[test]
1707	fn operator_state_commit_does_not_populate_the_point_tier() {
1708		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1709		let point = store.point.clone().expect("point tier configured");
1710
1711		let opkey = OperatorStateKey::new(
1712			OperatorId(7),
1713			GroupId::ROOT,
1714			KeyspaceId::CUSTOM_NOT_CACHED,
1715			vec![1, 2, 3],
1716		);
1717		let encoded = opkey.encode();
1718		MultiVersionCommit::commit(
1719			&store,
1720			cow_vec![Delta::Set {
1721				key: TaggedKey::from(opkey.clone()),
1722				bytes: EncodedBytes(CowVec::new(b"state-v10".to_vec())),
1723			}],
1724			CommitVersion(10),
1725		)
1726		.unwrap();
1727
1728		assert!(
1729			matches!(
1730				point.get(classify_key(&encoded), storage_key(&encoded).1, &encoded, CommitVersion(10)),
1731				VersionedGetResult::NotFound
1732			),
1733			"an operator commit must not write through into the point tier"
1734		);
1735		assert_eq!(
1736			store.point_shard_metrics().iter().map(|shard| shard.entries).sum::<usize>(),
1737			0,
1738			"no operator row may become resident on commit"
1739		);
1740
1741		let row = MultiVersionGet::get(&store, &TaggedKey::from(opkey.clone()), CommitVersion(10))
1742			.unwrap()
1743			.expect("the committed operator state must still be readable through the store");
1744		assert_eq!(row.bytes.as_slice(), b"state-v10");
1745		assert_eq!(row.version, CommitVersion(10));
1746
1747		assert!(
1748			matches!(
1749				point.get(classify_key(&encoded), storage_key(&encoded).1, &encoded, CommitVersion(10)),
1750				VersionedGetResult::NotFound
1751			),
1752			"a store-level operator read must not back-populate the point tier"
1753		);
1754	}
1755
1756	#[test]
1757	fn source_row_write_clears_coverage_on_its_partition() {
1758		let (store, _g) = StandardMultiStore::testing_memory_with_persistent_sqlite();
1759		let range = store.range.clone().expect("range tier configured");
1760
1761		let neighbor = RowKey::encoded(STORAGE, 1);
1762		let kind = classify_key(&neighbor);
1763		let partition = PartitionId::of(kind, &StorageRowKey::new(RowNumber(1)));
1764		assert_eq!(
1765			PartitionId::of(kind, &StorageRowKey::new(RowNumber(2))),
1766			partition,
1767			"both source rows must share a partition for this test to exercise the retraction"
1768		);
1769		assert!(
1770			range.materialize_scanned_chunk(
1771				kind,
1772				&RowKey::storage_start(STORAGE),
1773				&RowKey::storage_end(STORAGE),
1774				&[RawEntry {
1775					key: neighbor,
1776					version: CommitVersion(1),
1777					value: Some(CowVec::new(b"neighbor".to_vec())),
1778				}],
1779			),
1780			"the seeding chunk must publish its claim"
1781		);
1782		assert_eq!(
1783			range.complete_partitions().iter().sum::<usize>(),
1784			1,
1785			"the partition must start range-complete, or the retraction below proves nothing"
1786		);
1787
1788		commit_row(&store, 2, 5);
1789
1790		assert_eq!(
1791			range.complete_partitions().iter().sum::<usize>(),
1792			0,
1793			"writing a source row into a covered partition must retract the claim, or a later read is \
1794			 answered from a partition that no longer holds every row it claims"
1795		);
1796	}
1797
1798	fn drain_forward(store: &StandardMultiStore, cursor: &mut MultiVersionRangeCursor, read: u64) -> Vec<u64> {
1799		let mut seen = Vec::new();
1800		loop {
1801			let batch = store
1802				.range_next(
1803					cursor,
1804					RowKey::full_scan(STORAGE).encode(),
1805					MultiVersionScope::AsOf {
1806						read: CommitVersion(read),
1807					},
1808					2,
1809				)
1810				.unwrap();
1811			seen.extend(batch.items.iter().map(|row| row.key.clone()));
1812			if !batch.has_more {
1813				return rows_of(seen);
1814			}
1815		}
1816	}
1817
1818	fn drain_reverse(store: &StandardMultiStore, cursor: &mut MultiVersionRangeCursor, read: u64) -> Vec<u64> {
1819		let mut seen = Vec::new();
1820		loop {
1821			let batch = store
1822				.range_rev_next(
1823					cursor,
1824					RowKey::full_scan(STORAGE).encode(),
1825					MultiVersionScope::AsOf {
1826						read: CommitVersion(read),
1827					},
1828					2,
1829				)
1830				.unwrap();
1831			seen.extend(batch.items.iter().map(|row| row.key.clone()));
1832			if !batch.has_more {
1833				return rows_of(seen);
1834			}
1835		}
1836	}
1837
1838	fn rows_of(keys: Vec<TaggedKey>) -> Vec<u64> {
1839		let mut rows: Vec<u64> = keys
1840			.iter()
1841			.map(|key| match key {
1842				TaggedKey::Row(row) => row.row.0,
1843				other => panic!("a row key, got {other:?}"),
1844			})
1845			.collect();
1846		rows.sort_unstable();
1847		rows.dedup();
1848		rows
1849	}
1850
1851	const ROWS: u64 = 200;
1852
1853	fn store_without_read_tier() -> (StandardMultiStore, impl Drop) {
1854		StandardMultiStore::testing_memory_with_persistent_sqlite_tiers(
1855			MultiPointConfig {
1856				shard_bytes: None,
1857				..MultiPointConfig::testing()
1858			},
1859			MultiRangeConfig {
1860				shard_bytes: None,
1861				..MultiRangeConfig::testing()
1862			},
1863		)
1864	}
1865
1866	const PERSISTED: u64 = 200;
1867	const BUFFERED: u64 = 200;
1868
1869	fn seed_both_tiers(store: &StandardMultiStore, persist_the_low_rows: bool) {
1870		let (persist, buffer) = if persist_the_low_rows {
1871			(1..=PERSISTED, PERSISTED + 1..=PERSISTED + BUFFERED)
1872		} else {
1873			(PERSISTED + 1..=PERSISTED + BUFFERED, 1..=BUFFERED)
1874		};
1875		for n in persist {
1876			commit_row(store, n, 1);
1877		}
1878		flush(store, CommitVersion(1));
1879		for n in buffer {
1880			commit_row(store, n, 2);
1881		}
1882	}
1883
1884	#[test]
1885	fn a_scan_reading_both_tiers_loses_nothing_when_a_flush_lands_under_it() {
1886		let (store, _g) = store_without_read_tier();
1887		seed_both_tiers(&store, true);
1888
1889		let mut cursor = MultiVersionRangeCursor::new();
1890		let first = store
1891			.range_next(
1892				&mut cursor,
1893				RowKey::full_scan(STORAGE).encode(),
1894				MultiVersionScope::AsOf {
1895					read: CommitVersion(2),
1896				},
1897				2,
1898			)
1899			.unwrap();
1900		assert!(first.has_more, "four hundred rows cannot fit in one tier chunk, so the scan must continue");
1901		assert_ne!(
1902			cursor.persistent.stop(),
1903			Some(&RangeStop::AbsentTable),
1904			"the persistent tier must hold a table here, or this is the empty-database case again"
1905		);
1906		assert!(
1907			cursor.persistent.last_key().is_some(),
1908			"the persistent tier contributed nothing, so the scan is not merging two live tiers"
1909		);
1910
1911		flush(&store, CommitVersion(2));
1912
1913		let mut seen = rows_of(first.items.iter().map(|row| row.key.clone()).collect());
1914		seen.extend(drain_forward(&store, &mut cursor, 2));
1915		seen.sort_unstable();
1916		seen.dedup();
1917
1918		assert_eq!(
1919			seen,
1920			(1..=PERSISTED + BUFFERED).collect::<Vec<_>>(),
1921			"the merge dropped rows the flush moved from the buffer into a tier the scan had already read past"
1922		);
1923	}
1924
1925	#[test]
1926	fn a_reverse_scan_reading_both_tiers_loses_nothing_when_a_flush_lands_under_it() {
1927		let (store, _g) = store_without_read_tier();
1928		seed_both_tiers(&store, false);
1929
1930		let mut cursor = MultiVersionRangeCursor::new();
1931		let first = store
1932			.range_rev_next(
1933				&mut cursor,
1934				RowKey::full_scan(STORAGE).encode(),
1935				MultiVersionScope::AsOf {
1936					read: CommitVersion(2),
1937				},
1938				2,
1939			)
1940			.unwrap();
1941		assert!(first.has_more, "four hundred rows cannot fit in one tier chunk, so the scan must continue");
1942		assert_ne!(
1943			cursor.persistent.stop(),
1944			Some(&RangeStop::AbsentTable),
1945			"the persistent tier must hold a table here, or this is the empty-database case again"
1946		);
1947		assert!(
1948			cursor.persistent.last_key().is_some(),
1949			"the persistent tier contributed nothing, so the scan is not merging two live tiers"
1950		);
1951
1952		flush(&store, CommitVersion(2));
1953
1954		let mut seen = rows_of(first.items.iter().map(|row| row.key.clone()).collect());
1955		seen.extend(drain_reverse(&store, &mut cursor, 2));
1956		seen.sort_unstable();
1957		seen.dedup();
1958
1959		assert_eq!(
1960			seen,
1961			(1..=PERSISTED + BUFFERED).collect::<Vec<_>>(),
1962			"the reverse merge dropped rows the flush moved from the buffer into a tier the scan had already read past"
1963		);
1964	}
1965
1966	#[test]
1967	fn a_scan_that_asked_persistent_before_its_table_existed_reads_it_again_once_the_buffer_drains() {
1968		let (store, _g) = store_without_read_tier();
1969		for n in 1..=ROWS {
1970			commit_row(&store, n, 1);
1971		}
1972
1973		let mut cursor = MultiVersionRangeCursor::new();
1974		let first = store
1975			.range_next(
1976				&mut cursor,
1977				RowKey::full_scan(STORAGE).encode(),
1978				MultiVersionScope::AsOf {
1979					read: CommitVersion(1),
1980				},
1981				2,
1982			)
1983			.unwrap();
1984		assert!(first.has_more, "two hundred rows cannot fit in one tier chunk, so the scan must continue");
1985		assert_eq!(
1986			cursor.persistent.stop(),
1987			Some(&RangeStop::AbsentTable),
1988			"nothing is flushed yet, so this test is not exercising the interleaving it exists for"
1989		);
1990
1991		flush(&store, CommitVersion(1));
1992
1993		let mut seen = rows_of(first.items.iter().map(|row| row.key.clone()).collect());
1994		seen.extend(drain_forward(&store, &mut cursor, 1));
1995		seen.sort_unstable();
1996		seen.dedup();
1997
1998		assert_eq!(
1999			seen,
2000			(1..=ROWS).collect::<Vec<_>>(),
2001			"the scan dropped rows the flush moved out from under it"
2002		);
2003	}
2004
2005	#[test]
2006	fn a_reverse_scan_that_asked_persistent_before_its_table_existed_reads_it_again_too() {
2007		let (store, _g) = store_without_read_tier();
2008		for n in 1..=ROWS {
2009			commit_row(&store, n, 1);
2010		}
2011
2012		let mut cursor = MultiVersionRangeCursor::new();
2013		let first = store
2014			.range_rev_next(
2015				&mut cursor,
2016				RowKey::full_scan(STORAGE).encode(),
2017				MultiVersionScope::AsOf {
2018					read: CommitVersion(1),
2019				},
2020				2,
2021			)
2022			.unwrap();
2023		assert!(first.has_more, "two hundred rows cannot fit in one tier chunk, so the scan must continue");
2024		assert_eq!(
2025			cursor.persistent.stop(),
2026			Some(&RangeStop::AbsentTable),
2027			"nothing is flushed yet, so this test is not exercising the interleaving it exists for"
2028		);
2029
2030		flush(&store, CommitVersion(1));
2031
2032		let mut seen = rows_of(first.items.iter().map(|row| row.key.clone()).collect());
2033		seen.extend(drain_reverse(&store, &mut cursor, 1));
2034		seen.sort_unstable();
2035		seen.dedup();
2036
2037		assert_eq!(
2038			seen,
2039			(1..=ROWS).collect::<Vec<_>>(),
2040			"the reverse scan dropped rows the flush moved out from under it"
2041		);
2042	}
2043}
2044
2045#[cfg(all(test, feature = "sqlite", not(target_arch = "wasm32")))]
2046mod probe_tests {
2047	use std::collections::HashMap;
2048
2049	use reifydb_codec::{key::encoded::EncodedKey, row::bytes::EncodedBytes};
2050	use reifydb_core::{
2051		common::CommitVersion,
2052		delta::Delta,
2053		event::EventBus,
2054		interface::{
2055			catalog::{id::TableId, storage::StorageId},
2056			store::{MultiVersionCommit, MultiVersionGet, MultiVersionGetPrevious, classify_key},
2057		},
2058		key::{
2059			any::TaggedKey,
2060			row::{PartitionedRowKey, RowKey},
2061		},
2062	};
2063	use reifydb_runtime::{actor::system::ActorSystem, context::clock::Clock, shutdown::Shutdown};
2064	use reifydb_sqlite::{SqliteConfig, SqliteTempPathGuard};
2065	use reifydb_store_commit::{MultiVersionScope, TierBatch, store::CommitStore};
2066	use reifydb_value::{
2067		cow_vec,
2068		util::cowvec::CowVec,
2069		value::{partition::Partition, row_number::RowNumber},
2070	};
2071
2072	use crate::{
2073		config::{CommitStoreConfig, MultiStoreConfig, PersistentConfig},
2074		store::StandardMultiStore,
2075		tier::{
2076			persistent::sqlite::storage::SqlitePersistentStorage, point::MultiPointConfig,
2077			range::MultiRangeConfig,
2078		},
2079	};
2080
2081	const STORAGE: StorageId = StorageId::Table(TableId(1));
2082
2083	fn probes(store: &StandardMultiStore) -> (u64, u64) {
2084		let m = store.persistent_probe_metrics().expect("persistent tier configured");
2085		(m.persistent_probes.as_u64(), m.persistent_absent.as_u64())
2086	}
2087
2088	fn seed_persistent(store: &StandardMultiStore, entries: Vec<(EncodedKey, Option<CowVec<u8>>)>) {
2089		let mut batch: TierBatch = HashMap::new();
2090		for (key, value) in entries {
2091			batch.entry(classify_key(&key)).or_default().push((key, value));
2092		}
2093		store.persistent()
2094			.expect("persistent tier configured")
2095			.persist_sweep(vec![(CommitVersion(1), batch)])
2096			.unwrap();
2097	}
2098
2099	fn value(text: &str) -> Option<CowVec<u8>> {
2100		Some(CowVec::new(text.as_bytes().to_vec()))
2101	}
2102
2103	fn store_over_populated_persistent() -> (StandardMultiStore, SqliteTempPathGuard) {
2104		let (sqlite_config, guard) = SqliteConfig::in_memory();
2105		{
2106			let storage = SqlitePersistentStorage::new(sqlite_config.clone());
2107			let seeded = RowKey::encoded(STORAGE, 999);
2108			let mut batch: TierBatch = HashMap::new();
2109			batch.insert(classify_key(&seeded), vec![(seeded, value("preexisting"))]);
2110			storage.set_collecting_accepted(CommitVersion(1), batch).unwrap();
2111			storage.shutdown();
2112		}
2113
2114		let clock = Clock::testing();
2115		let actor_system = ActorSystem::testing(clock.clone());
2116		let spawner = actor_system.spawner();
2117		let event_bus = EventBus::new(&spawner);
2118		let store = StandardMultiStore::new(MultiStoreConfig {
2119			commit: CommitStoreConfig {
2120				storage: CommitStore::new(),
2121			},
2122			persistent: Some(PersistentConfig::sqlite(sqlite_config)),
2123			point: Some(MultiPointConfig::testing()),
2124			range: Some(MultiRangeConfig::testing()),
2125			retention: Default::default(),
2126			merge_config: Default::default(),
2127			event_bus,
2128			spawner,
2129			clock,
2130		})
2131		.unwrap();
2132
2133		assert!(
2134			!store.persistent().expect("persistent tier configured").filter().metrics().enabled,
2135			"the filter came up armed over a populated database, so every lookup below is ruled out \
2136			 before it reaches sqlite and the probe counters measure nothing"
2137		);
2138		(store, guard)
2139	}
2140
2141	#[test]
2142	fn a_read_the_commit_buffer_answers_never_counts_a_persistent_probe() {
2143		let (store, _guard) = StandardMultiStore::testing_memory_with_persistent_sqlite();
2144
2145		let present = TaggedKey::from(RowKey::new(STORAGE, 1));
2146		MultiVersionCommit::commit(
2147			&store,
2148			cow_vec![Delta::Set {
2149				key: present.clone(),
2150				bytes: EncodedBytes(CowVec::new(b"v1".to_vec())),
2151			}],
2152			CommitVersion(2),
2153		)
2154		.unwrap();
2155		let removed = TaggedKey::from(RowKey::new(STORAGE, 2));
2156		MultiVersionCommit::commit(&store, cow_vec![Delta::remove_silent(removed.clone())], CommitVersion(3))
2157			.unwrap();
2158
2159		let before = probes(&store);
2160		assert!(store.get(&present, CommitVersion(9)).unwrap().is_some());
2161		assert!(store.get(&removed, CommitVersion(9)).unwrap().is_none());
2162
2163		assert_eq!(
2164			probes(&store),
2165			before,
2166			"the commit buffer answered both reads, including the tombstoned one. Counting them as \
2167			 persistent probes inflates the denominator with lookups no filter could ever have \
2168			 skipped, so the measured absent rate reads lower than the real ceiling"
2169		);
2170	}
2171
2172	#[test]
2173	fn a_persistent_read_that_finds_a_row_counts_a_probe_but_no_absence() {
2174		let (store, _guard) = StandardMultiStore::testing_memory_with_persistent_sqlite();
2175
2176		let k = TaggedKey::from(RowKey::new(STORAGE, 1));
2177		seed_persistent(&store, vec![(k.encode(), value("resident"))]);
2178
2179		let before = probes(&store);
2180		assert!(store.get(&k, CommitVersion(9)).unwrap().is_some());
2181		assert_eq!(
2182			probes(&store),
2183			(before.0 + 1, before.1),
2184			"a hit must raise the probe count and leave the absent count alone, otherwise the ratio \
2185			 claims a filter could skip reads that genuinely returned data"
2186		);
2187	}
2188
2189	#[test]
2190	fn a_persistent_read_that_finds_nothing_counts_a_probe_and_an_absence() {
2191		let (store, _guard) = store_over_populated_persistent();
2192
2193		let k = TaggedKey::from(RowKey::new(STORAGE, 77));
2194
2195		let before = probes(&store);
2196		assert!(store.get(&k, CommitVersion(9)).unwrap().is_none());
2197		assert_eq!(
2198			probes(&store),
2199			(before.0 + 1, before.1 + 1),
2200			"an absent point read must raise both counts, otherwise the ceiling on what a filter \
2201			 could save measures as zero and the decision is made on a number that cannot move"
2202		);
2203	}
2204
2205	#[test]
2206	fn a_deleted_key_counts_a_probe_and_an_absence() {
2207		let (store, _guard) = StandardMultiStore::testing_memory_with_persistent_sqlite();
2208
2209		let k = TaggedKey::from(RowKey::new(STORAGE, 5));
2210		seed_persistent(&store, vec![(k.encode(), value("doomed"))]);
2211		seed_persistent(&store, vec![(k.encode(), None)]);
2212
2213		let before = probes(&store);
2214		assert!(store.get(&k, CommitVersion(9)).unwrap().is_none(), "a deleted key reads as no row");
2215		assert_eq!(
2216			probes(&store),
2217			(before.0 + 1, before.1 + 1),
2218			"the delete removed the row, so sqlite came back with nothing and the read was wasted. \
2219			 Counting it as a hit would deny a filter a saving it can actually make"
2220		);
2221	}
2222
2223	#[test]
2224	fn a_batched_read_counts_one_probe_per_key_that_reached_the_persistent_tier() {
2225		let (store, _guard) = store_over_populated_persistent();
2226
2227		let resident = TaggedKey::from(RowKey::new(STORAGE, 1));
2228		let deleted = TaggedKey::from(RowKey::new(STORAGE, 2));
2229		let missing_a = TaggedKey::from(RowKey::new(STORAGE, 3));
2230		let missing_b = TaggedKey::from(RowKey::new(STORAGE, 4));
2231		let buffered = TaggedKey::from(RowKey::new(STORAGE, 5));
2232		seed_persistent(
2233			&store,
2234			vec![(resident.encode(), value("resident")), (deleted.encode(), value("doomed"))],
2235		);
2236		seed_persistent(&store, vec![(deleted.encode(), None)]);
2237		MultiVersionCommit::commit(
2238			&store,
2239			cow_vec![Delta::Set {
2240				key: buffered.clone(),
2241				bytes: EncodedBytes(CowVec::new(b"buffered".to_vec())),
2242			}],
2243			CommitVersion(2),
2244		)
2245		.unwrap();
2246
2247		let before = probes(&store);
2248		let found = store
2249			.get_many(
2250				&[
2251					resident.encode(),
2252					deleted.encode(),
2253					missing_a.encode(),
2254					missing_b.encode(),
2255					buffered.encode(),
2256				],
2257				CommitVersion(9),
2258			)
2259			.unwrap();
2260		assert_eq!(found.len(), 2, "only the resident and the buffered key carry a row");
2261
2262		assert_eq!(
2263			probes(&store),
2264			(before.0 + 4, before.1 + 3),
2265			"four of the five keys fell through to sqlite and three of those came back with nothing. \
2266			 The buffered key must not count at all, and the deleted key must count as a wasted probe"
2267		);
2268	}
2269
2270	#[test]
2271	fn a_previous_version_read_that_reaches_persistent_is_counted() {
2272		let (store, _guard) = store_over_populated_persistent();
2273
2274		let k = TaggedKey::from(RowKey::new(STORAGE, 42));
2275
2276		let before = probes(&store);
2277		assert!(store.get_previous_version(&k, CommitVersion(9)).unwrap().is_none());
2278		assert_eq!(
2279			probes(&store),
2280			(before.0 + 1, before.1 + 1),
2281			"an absent previous-version probe is exactly the sqlite read a filter would skip"
2282		);
2283	}
2284
2285	#[test]
2286	fn a_store_without_a_persistent_tier_reports_no_probe_metrics() {
2287		let store = StandardMultiStore::testing_memory();
2288		assert!(store.get(&TaggedKey::from(RowKey::new(STORAGE, 1)), CommitVersion(9)).unwrap().is_none());
2289		assert!(store.persistent_probe_metrics().is_none());
2290	}
2291
2292	#[test]
2293	fn a_paginated_range_over_many_partitions_reaches_every_row() {
2294		let (store, _guard) = StandardMultiStore::testing_memory_with_persistent_sqlite();
2295
2296		let mut entries = Vec::new();
2297		for p in 0u128..64 {
2298			let partition = Partition(p.wrapping_mul(0x9E3779B97F4A7C15) ^ 0xA5A5_A5A5_A5A5_A5A5);
2299			for r in 0u64..2 {
2300				let key = TaggedKey::from(PartitionedRowKey::new(STORAGE, partition, RowNumber(r + 1)));
2301				entries.push((key.encode(), value("v")));
2302			}
2303		}
2304		seed_persistent(&store, entries);
2305
2306		let range = PartitionedRowKey::full_scan(STORAGE).encode();
2307		let collected: Vec<_> = store
2308			.range(
2309				range,
2310				MultiVersionScope::AsOf {
2311					read: CommitVersion(9),
2312				},
2313				4,
2314			)
2315			.collect::<Result<Vec<_>, _>>()
2316			.unwrap();
2317		assert_eq!(collected.len(), 128, "a paginated scan across 64 partitions must reach every row");
2318	}
2319}
2320
2321pub struct NarrowRangeCursor<L: NarrowLayout> {
2322	pub commit: RangeCursor,
2323	pub persistent: Cursor<RangeStop, L>,
2324	pub exhausted: bool,
2325	commit_buf: Vec<RawEntry>,
2326}
2327
2328impl<L: NarrowLayout> Default for NarrowRangeCursor<L> {
2329	fn default() -> Self {
2330		Self {
2331			commit: RangeCursor::default(),
2332			persistent: Cursor::default(),
2333			exhausted: false,
2334			commit_buf: Vec::new(),
2335		}
2336	}
2337}
2338
2339pub type RowRangeCursor = NarrowRangeCursor<StorageRowKey>;
2340
2341impl StandardMultiStore {
2342	pub fn range_next_row(
2343		&self,
2344		cursor: &mut RowRangeCursor,
2345		storage: StorageId,
2346		start: Bound<StorageRowKey>,
2347		end: Bound<StorageRowKey>,
2348		scope: MultiVersionScope,
2349		batch_size: u64,
2350	) -> Result<MultiVersionBatch<StorageRowKey>> {
2351		self.range_next_narrow::<StorageRowKey>(cursor, storage, start, end, scope, batch_size)
2352	}
2353
2354	pub fn range_next_narrow<L: PersistentRangeLayout>(
2355		&self,
2356		cursor: &mut NarrowRangeCursor<L>,
2357		storage: StorageId,
2358		start: Bound<L>,
2359		end: Bound<L>,
2360		scope: MultiVersionScope,
2361		batch_size: u64,
2362	) -> Result<MultiVersionBatch<L>> {
2363		if cursor.exhausted {
2364			return Ok(MultiVersionBatch {
2365				items: Vec::new(),
2366				has_more: false,
2367			});
2368		}
2369
2370		if self.persistent.is_none() {
2371			cursor.persistent.finish();
2372		}
2373
2374		let table = L::kind(storage);
2375		let batch_size = batch_size as usize;
2376
2377		let enc_start = match &start {
2378			Bound::Included(k) | Bound::Excluded(k) => L::widen(storage, k),
2379			Bound::Unbounded => L::storage_start(storage),
2380		};
2381		let enc_end = match &end {
2382			Bound::Included(k) | Bound::Excluded(k) => L::widen(storage, k),
2383			Bound::Unbounded => L::storage_end(storage),
2384		};
2385		let commit_start = match &start {
2386			Bound::Excluded(_) => Bound::Excluded(enc_start.as_slice()),
2387			_ => Bound::Included(enc_start.as_slice()),
2388		};
2389		let commit_end = match &end {
2390			Bound::Excluded(_) => Bound::Excluded(enc_end.as_slice()),
2391			_ => Bound::Included(enc_end.as_slice()),
2392		};
2393
2394		let mut collected: BTreeMap<L, (CommitVersion, Option<CowVec<u8>>)> = BTreeMap::new();
2395
2396		while collected.len() < batch_size {
2397			if !cursor.commit.is_exhausted() {
2398				self.commit.range_next_into(
2399					table,
2400					&mut cursor.commit,
2401					commit_start,
2402					commit_end,
2403					scope,
2404					TIER_SCAN_CHUNK_SIZE,
2405					&mut cursor.commit_buf,
2406				)?;
2407				for entry in cursor.commit_buf.drain(..) {
2408					let Some(key) = L::narrow(table, &entry.key) else {
2409						continue;
2410					};
2411					merge_row(&mut collected, key, entry.version, entry.value);
2412				}
2413			}
2414
2415			if let Some(persistent) = &self.persistent
2416				&& !cursor.persistent.is_exhausted()
2417			{
2418				let batch = L::range_next(
2419					persistent,
2420					&mut cursor.persistent,
2421					NarrowRangeRequest {
2422						table,
2423						start: bound_ref(&start),
2424						end: bound_ref(&end),
2425						scope,
2426						batch_size: TIER_SCAN_CHUNK_SIZE,
2427						descending: false,
2428					},
2429				)?;
2430				for entry in batch.entries {
2431					merge_row(&mut collected, entry.key, entry.version, entry.value);
2432				}
2433			}
2434
2435			if cursor.commit.is_exhausted() && cursor.persistent.is_exhausted() {
2436				cursor.exhausted = true;
2437				break;
2438			}
2439		}
2440
2441		if let Some(h) = forward_horizon_narrow(table, cursor) {
2442			collected.retain(|k, _| *k <= h);
2443			if let Some(last) = cursor.commit.last_key()
2444				&& L::narrow(table, last).is_some_and(|n| n > h)
2445			{
2446				cursor.commit.resume(L::widen(storage, &h));
2447			}
2448			if cursor.persistent.last_key().is_some_and(|last| *last > h) {
2449				cursor.persistent.resume(h);
2450			}
2451		}
2452
2453		let items: Vec<MultiVersionRow<L>> = collected
2454			.into_iter()
2455			.filter_map(|(key, (v, value))| {
2456				value.map(|val| MultiVersionRow {
2457					key,
2458					bytes: EncodedBytes(val),
2459					version: v,
2460				})
2461			})
2462			.collect();
2463
2464		Ok(MultiVersionBatch {
2465			items,
2466			has_more: !cursor.exhausted,
2467		})
2468	}
2469}
2470
2471fn forward_horizon_narrow<L: NarrowLayout>(table: EntryKind, cursor: &NarrowRangeCursor<L>) -> Option<L> {
2472	let mut horizon: Option<L> = None;
2473	if !cursor.commit.is_exhausted() {
2474		horizon = Some(L::narrow(table, cursor.commit.last_key()?)?);
2475	}
2476	if !cursor.persistent.is_exhausted() {
2477		let last = *cursor.persistent.last_key()?;
2478		horizon = Some(match horizon {
2479			None => last,
2480			Some(prev) => {
2481				if last < prev {
2482					last
2483				} else {
2484					prev
2485				}
2486			}
2487		});
2488	}
2489	horizon
2490}
2491
2492fn bound_ref<K>(bound: &Bound<K>) -> Bound<&K> {
2493	match bound {
2494		Bound::Included(k) => Bound::Included(k),
2495		Bound::Excluded(k) => Bound::Excluded(k),
2496		Bound::Unbounded => Bound::Unbounded,
2497	}
2498}
2499
2500fn merge_row<K: Ord>(
2501	collected: &mut BTreeMap<K, (CommitVersion, Option<CowVec<u8>>)>,
2502	key: K,
2503	version: CommitVersion,
2504	value: Option<CowVec<u8>>,
2505) {
2506	match collected.entry(key) {
2507		Entry::Vacant(slot) => {
2508			slot.insert((version, value));
2509		}
2510		Entry::Occupied(mut slot) => {
2511			if version > slot.get().0 {
2512				slot.insert((version, value));
2513			}
2514		}
2515	}
2516}