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