1use 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}