Skip to main content

icydb_core/db/data/
store.rs

1//! Module: data::store
2//! Responsibility: journaled-or-heap row storage behind the data-store boundary.
3//! Does not own: key/row validation policy beyond type boundaries.
4//! Boundary: commit/executor call into this layer after prevalidation.
5
6use crate::{
7    db::{
8        data::{CanonicalRow, RawDataStoreKey, RawRow},
9        direction::Direction,
10        ordered_overlay::{OrderedOverlayEntry, ordered_overlay_entries},
11        positioned_overlay::{
12            JournalOverlayPosition, PositionedOverlayMetadata, PositionedOverlayRetirement,
13        },
14    },
15    types::EntityTag,
16};
17use ic_memory::{DefaultMemoryImpl, Memory, RuntimeMemory};
18use ic_storage::adapter::StableBTreeMap;
19#[cfg(test)]
20use std::cell::Cell;
21use std::collections::{BTreeMap as HeapBTreeMap, BTreeSet, btree_map::Entry};
22use std::convert::Infallible;
23use std::ops::{Bound, RangeBounds};
24
25#[cfg(test)]
26thread_local! {
27    static DATA_STORE_GET_CALL_COUNT: Cell<u64> = const { Cell::new(0) };
28}
29
30#[cfg(test)]
31fn record_data_store_get_call() {
32    DATA_STORE_GET_CALL_COUNT.with(|count| {
33        count.set(count.get().saturating_add(1));
34    });
35}
36
37///
38/// DataStore
39///
40/// Thin persistence wrapper over one journaled or heap BTreeMap.
41///
42/// Invariant: callers provide already-validated `RawDataStoreKey` and canonical row bytes.
43/// This type intentionally does not enforce commit-phase ordering.
44///
45
46pub struct DataStore {
47    backend: DataStoreBackend,
48    generation: u64,
49    entity_cardinality: EntityCardinality,
50}
51
52enum DataStoreBackend {
53    Heap(HeapBTreeMap<RawDataStoreKey, RawRow>),
54    Journaled {
55        canonical: StableBTreeMap<RawDataStoreKey, RawRow, DefaultMemoryImpl>,
56        live: HeapBTreeMap<RawDataStoreKey, RawRow>,
57        tombstones: BTreeSet<RawDataStoreKey>,
58        positions: PositionedOverlayMetadata<RawDataStoreKey>,
59        entity_cardinality_delta: EntityCardinalityDelta,
60    },
61}
62
63/// Preflighted provenance publication for direct journal record families.
64#[cfg(feature = "migration")]
65pub(in crate::db) struct PreparedDataPositionPublication {
66    keys: Vec<RawDataStoreKey>,
67    position: JournalOverlayPosition,
68}
69
70/// Preflighted exact retirement for one complete journal batch.
71pub(in crate::db) struct PreparedDataPositionRetirement {
72    entries: Vec<(RawDataStoreKey, PositionedOverlayRetirement)>,
73}
74
75/// One visible row read that borrows heap/live state and owns stable state.
76///
77/// Callers that only need selected fields can evaluate a borrowed row while
78/// the store handle is active. Stable-structure reads remain owned because
79/// that backend cannot expose a value reference beyond its storage call.
80pub(in crate::db) enum StoredRowRead<'a> {
81    Missing,
82    Borrowed(&'a RawRow),
83    Owned(RawRow),
84}
85
86impl StoredRowRead<'_> {
87    /// Borrow the visible row regardless of its physical backing.
88    #[must_use]
89    pub(in crate::db) const fn as_row(&self) -> Option<&RawRow> {
90        match self {
91            Self::Missing => None,
92            Self::Borrowed(row) => Some(row),
93            Self::Owned(row) => Some(row),
94        }
95    }
96
97    /// Convert the visible row into the existing owned get contract.
98    #[must_use]
99    fn into_owned(self) -> Option<RawRow> {
100        match self {
101            Self::Missing => None,
102            Self::Borrowed(row) => Some(row.clone()),
103            Self::Owned(row) => Some(row),
104        }
105    }
106}
107
108/// Control-flow result for store traversal visitors.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub(in crate::db) enum StoreVisit {
111    Continue,
112    Stop,
113}
114
115impl StoreVisit {
116    const fn should_stop(self) -> bool {
117        matches!(self, Self::Stop)
118    }
119}
120
121impl DataStore {
122    /// Initialize a volatile heap-backed data store.
123    #[must_use]
124    pub const fn init_heap() -> Self {
125        Self {
126            backend: DataStoreBackend::Heap(HeapBTreeMap::new()),
127            generation: 0,
128            entity_cardinality: EntityCardinality::empty(),
129        }
130    }
131
132    /// Initialize a journaled cached-stable data store.
133    ///
134    /// Normal writes update only the live projection. The canonical stable map
135    /// is the future fold target and is not mutated by this wrapper's write
136    /// methods.
137    ///
138    /// # Panics
139    /// Panics if retained storage has an invalid header or unsupported layout,
140    /// or the backing storage fails. Invalid retained contents are not replaced.
141    #[must_use]
142    pub fn init_journaled(memory: RuntimeMemory<DefaultMemoryImpl>) -> Self {
143        // Retained maps must never be replaced when their header is corrupt.
144        let canonical = if memory.size() == 0 {
145            StableBTreeMap::init(memory)
146        } else {
147            StableBTreeMap::load(memory)
148        };
149        let entity_cardinality = if canonical.is_empty() {
150            EntityCardinality::empty()
151        } else {
152            EntityCardinality::unavailable()
153        };
154        Self {
155            backend: DataStoreBackend::Journaled {
156                canonical,
157                live: HeapBTreeMap::new(),
158                tombstones: BTreeSet::new(),
159                positions: PositionedOverlayMetadata::new(),
160                entity_cardinality_delta: EntityCardinalityDelta::empty(),
161            },
162            generation: 0,
163            // Stable rows remain authoritative after reinitialization. Exact
164            // zero cardinality is still known for an empty canonical map;
165            // populated maps remain unavailable without a startup scan.
166            entity_cardinality,
167        }
168    }
169
170    /// Insert or replace one row by raw key.
171    pub(in crate::db) fn insert(
172        &mut self,
173        key: RawDataStoreKey,
174        row: CanonicalRow,
175    ) -> Option<RawRow> {
176        self.insert_raw(key, row.into_raw_row())
177    }
178
179    // One insertion owner for admitted writes and corruption-fixture raw rows.
180    fn insert_raw(&mut self, key: RawDataStoreKey, row: RawRow) -> Option<RawRow> {
181        // Counts retain only entity identity, so copy that fact before the map
182        // takes the row key instead of retaining a second key buffer.
183        let entity = key.entity_tag_prefix();
184        let previous = match &mut self.backend {
185            DataStoreBackend::Heap(map) => map.insert(key, row),
186            DataStoreBackend::Journaled {
187                canonical,
188                live,
189                tombstones,
190                ..
191            } => {
192                let hidden = tombstones.remove(&key);
193                // Move a live predecessor; read stable bytes only when visible
194                // and not overridden. Neither path clones the live row payload.
195                match live.entry(key) {
196                    Entry::Occupied(mut entry) => {
197                        let previous = entry.insert(row);
198                        (!hidden).then_some(previous)
199                    }
200                    Entry::Vacant(entry) => {
201                        let previous = if hidden {
202                            None
203                        } else {
204                            canonical.get(entry.key())
205                        };
206                        entry.insert(row);
207                        previous
208                    }
209                }
210            }
211        };
212        self.entity_cardinality
213            .apply_insert(entity, previous.is_some());
214        self.apply_entity_overlay_delta(entity, previous.is_some(), true);
215        self.bump_generation();
216        previous
217    }
218
219    /// Insert one raw row directly for corruption-focused test setup only.
220    #[cfg(test)]
221    pub(in crate::db) fn insert_raw_for_test(
222        &mut self,
223        key: RawDataStoreKey,
224        row: RawRow,
225    ) -> Option<RawRow> {
226        self.insert_raw(key, row)
227    }
228
229    /// Remove one row by raw key.
230    pub(in crate::db) fn remove(&mut self, key: &RawDataStoreKey) -> Option<RawRow> {
231        let previous = match &mut self.backend {
232            DataStoreBackend::Heap(map) => map.remove(key),
233            DataStoreBackend::Journaled {
234                canonical,
235                live,
236                tombstones,
237                ..
238            } => {
239                let previous = live.remove(key);
240                let previous = if tombstones.contains(key) {
241                    None
242                } else {
243                    previous.or_else(|| canonical.get(key))
244                };
245                tombstones.insert(key.clone());
246                previous
247            }
248        };
249        self.entity_cardinality
250            .apply_remove(key.entity_tag_prefix(), previous.is_some());
251        self.apply_entity_overlay_delta(key.entity_tag_prefix(), previous.is_some(), false);
252        self.bump_generation();
253        previous
254    }
255
256    /// Reset the volatile projection for journaled recovery without mutating
257    /// the canonical stable base.
258    pub(in crate::db) fn reset_journaled_live_projection(
259        &mut self,
260    ) -> Result<(), crate::error::InternalError> {
261        let DataStoreBackend::Journaled {
262            canonical,
263            live,
264            tombstones,
265            positions,
266            entity_cardinality_delta,
267        } = &mut self.backend
268        else {
269            return Err(crate::error::InternalError::store_invariant());
270        };
271
272        live.clear();
273        tombstones.clear();
274        positions.clear();
275        *entity_cardinality_delta = EntityCardinalityDelta::empty();
276        self.entity_cardinality = if canonical.is_empty() {
277            EntityCardinality::empty()
278        } else {
279            EntityCardinality::unavailable()
280        };
281        self.bump_generation();
282
283        Ok(())
284    }
285
286    /// Apply one recovered journal row put into the volatile projection.
287    #[cfg(any(test, feature = "migration"))]
288    pub(in crate::db) fn apply_recovered_journal_put(
289        &mut self,
290        key: RawDataStoreKey,
291        row: RawRow,
292    ) -> Result<(), crate::error::InternalError> {
293        // Replay callers discard predecessor bytes; cardinality needs presence only.
294        let previous_present = self.contains(&key);
295        let DataStoreBackend::Journaled {
296            live, tombstones, ..
297        } = &mut self.backend
298        else {
299            return Err(crate::error::InternalError::store_invariant());
300        };
301
302        tombstones.remove(&key);
303        let entity = key.entity_tag_prefix();
304        live.insert(key, row);
305        self.entity_cardinality
306            .apply_insert(entity, previous_present);
307        self.apply_entity_overlay_delta(entity, previous_present, true);
308        self.bump_generation();
309
310        Ok(())
311    }
312
313    /// Publish one preflighted positioned row value or tombstone.
314    pub(in crate::db) fn publish_preflighted_journal_entry(
315        &mut self,
316        key: RawDataStoreKey,
317        row: Option<RawRow>,
318        position: JournalOverlayPosition,
319    ) -> Result<(), crate::error::InternalError> {
320        // Cardinality needs presence only; never materialize a previous row.
321        let previous_present = self.contains(&key);
322        let DataStoreBackend::Journaled {
323            live,
324            tombstones,
325            positions,
326            entity_cardinality_delta,
327            ..
328        } = &mut self.backend
329        else {
330            return Err(crate::error::InternalError::store_invariant());
331        };
332        let next_present = row.is_some();
333        let entity = key.entity_tag_prefix();
334        if let Some(row) = row {
335            tombstones.remove(&key);
336            live.insert(key.clone(), row);
337            self.entity_cardinality
338                .apply_insert(entity, previous_present);
339        } else {
340            live.remove(&key);
341            tombstones.insert(key.clone());
342            self.entity_cardinality
343                .apply_remove(entity, previous_present);
344        }
345        entity_cardinality_delta.apply_presence_transition(entity, previous_present, next_present);
346        positions.publish_preflighted(key, position);
347        self.bump_generation();
348
349        Ok(())
350    }
351
352    /// Validate and publish one positioned row for direct store tests.
353    #[cfg(test)]
354    pub(in crate::db) fn publish_positioned_journal_entry(
355        &mut self,
356        key: RawDataStoreKey,
357        row: Option<RawRow>,
358        position: JournalOverlayPosition,
359    ) -> Result<(), crate::error::InternalError> {
360        self.preflight_positioned_journal_entry(&key, position)?;
361        self.publish_preflighted_journal_entry(key, row, position)
362    }
363
364    /// Preflight row provenance before marker publication.
365    pub(in crate::db) fn preflight_positioned_journal_entry(
366        &self,
367        key: &RawDataStoreKey,
368        position: JournalOverlayPosition,
369    ) -> Result<(), crate::error::InternalError> {
370        let DataStoreBackend::Journaled { positions, .. } = &self.backend else {
371            return Err(crate::error::InternalError::store_invariant());
372        };
373        positions.preflight_publish(key, position)
374    }
375
376    /// Preflight direct row provenance before marker publication.
377    #[cfg(feature = "migration")]
378    pub(in crate::db) fn prepare_position_publication(
379        &self,
380        keys: impl IntoIterator<Item = RawDataStoreKey>,
381        position: JournalOverlayPosition,
382    ) -> Result<PreparedDataPositionPublication, crate::error::InternalError> {
383        let DataStoreBackend::Journaled { positions, .. } = &self.backend else {
384            return Err(crate::error::InternalError::store_invariant());
385        };
386        let keys = keys.into_iter().collect::<BTreeSet<_>>();
387        for key in &keys {
388            positions.preflight_publish(key, position)?;
389        }
390        Ok(PreparedDataPositionPublication {
391            keys: keys.into_iter().collect(),
392            position,
393        })
394    }
395
396    /// Publish direct row provenance after its values have been applied.
397    #[cfg(feature = "migration")]
398    pub(in crate::db) fn publish_prepared_positions(
399        &mut self,
400        prepared: PreparedDataPositionPublication,
401    ) {
402        let DataStoreBackend::Journaled { positions, .. } = &mut self.backend else {
403            debug_assert!(false, "preflighted row positions require a journaled store");
404            return;
405        };
406        for key in prepared.keys {
407            positions.publish_preflighted(key, prepared.position);
408        }
409    }
410
411    /// Preflight exact row-overlay retirement before canonical mutation.
412    pub(in crate::db) fn prepare_position_retirement(
413        &self,
414        keys: impl IntoIterator<Item = RawDataStoreKey>,
415        position: JournalOverlayPosition,
416    ) -> Result<PreparedDataPositionRetirement, crate::error::InternalError> {
417        let DataStoreBackend::Journaled { positions, .. } = &self.backend else {
418            return Err(crate::error::InternalError::store_invariant());
419        };
420        let entries = keys
421            .into_iter()
422            .collect::<BTreeSet<_>>()
423            .into_iter()
424            .map(|key| {
425                positions
426                    .preflight_retirement(&key, position)
427                    .map(|retirement| (key, retirement))
428            })
429            .collect::<Result<Vec<_>, _>>()?;
430        Ok(PreparedDataPositionRetirement { entries })
431    }
432
433    /// Retire only exact row overlays after canonical mutation succeeds.
434    pub(in crate::db) fn apply_prepared_position_retirement(
435        &mut self,
436        prepared: PreparedDataPositionRetirement,
437    ) {
438        let DataStoreBackend::Journaled {
439            live,
440            tombstones,
441            positions,
442            ..
443        } = &mut self.backend
444        else {
445            debug_assert!(
446                false,
447                "preflighted row retirement requires a journaled store"
448            );
449            return;
450        };
451        for (key, retirement) in prepared.entries {
452            if retirement == PositionedOverlayRetirement::Exact {
453                live.remove(&key);
454                tombstones.remove(&key);
455                positions.retire_preflighted(&key, retirement);
456            }
457        }
458    }
459
460    /// Apply one folded journal row put into the canonical stable base.
461    pub(in crate::db) fn fold_recovered_journal_put(
462        &mut self,
463        key: RawDataStoreKey,
464        row: RawRow,
465    ) -> Result<Option<RawRow>, crate::error::InternalError> {
466        let DataStoreBackend::Journaled {
467            canonical,
468            live,
469            tombstones,
470            ..
471        } = &mut self.backend
472        else {
473            return Err(crate::error::InternalError::store_invariant());
474        };
475
476        let visible = !live.contains_key(&key) && !tombstones.contains(&key);
477        let entity = key.entity_tag_prefix();
478        let previous = canonical.insert(key, row);
479        if visible {
480            self.entity_cardinality
481                .apply_insert(entity, previous.is_some());
482        } else if previous.is_none() {
483            self.apply_entity_overlay_delta(entity, true, false);
484        }
485        self.bump_generation();
486
487        Ok(previous)
488    }
489
490    /// Apply one folded journal row delete into the canonical stable base.
491    pub(in crate::db) fn fold_recovered_journal_delete(
492        &mut self,
493        key: &RawDataStoreKey,
494    ) -> Result<Option<RawRow>, crate::error::InternalError> {
495        let DataStoreBackend::Journaled {
496            canonical,
497            live,
498            tombstones,
499            ..
500        } = &mut self.backend
501        else {
502            return Err(crate::error::InternalError::store_invariant());
503        };
504
505        let visible = !live.contains_key(key) && !tombstones.contains(key);
506        let previous = canonical.remove(key);
507        if visible {
508            self.entity_cardinality
509                .apply_remove(key.entity_tag_prefix(), previous.is_some());
510        } else if previous.is_some() {
511            self.apply_entity_overlay_delta(key.entity_tag_prefix(), false, true);
512        }
513        self.bump_generation();
514
515        Ok(previous)
516    }
517
518    /// Prove that recovered journal rows can be folded into canonical storage.
519    pub(in crate::db) fn preflight_fold_recovered_journal(
520        &self,
521    ) -> Result<(), crate::error::InternalError> {
522        match self.backend {
523            DataStoreBackend::Journaled { .. } => Ok(()),
524            DataStoreBackend::Heap(_) => Err(crate::error::InternalError::store_invariant()),
525        }
526    }
527
528    /// Load one row by raw key.
529    pub(in crate::db) fn get(&self, key: &RawDataStoreKey) -> Option<RawRow> {
530        self.read(key).into_owned()
531    }
532
533    /// Load one row from the canonical predecessor view.
534    ///
535    /// Online journal folding uses this view so a newer positioned live effect
536    /// cannot replace the predecessor evidence for the older batch being
537    /// canonicalized.
538    pub(in crate::db) fn get_canonical(&self, key: &RawDataStoreKey) -> Option<RawRow> {
539        match &self.backend {
540            DataStoreBackend::Heap(map) => map.get(key).cloned(),
541            DataStoreBackend::Journaled { canonical, .. } => canonical.get(key),
542        }
543    }
544
545    /// Test canonical predecessor presence without loading its row payload.
546    /// Positioned live updates and tombstones do not change this view.
547    #[must_use]
548    pub(in crate::db) fn contains_canonical(&self, key: &RawDataStoreKey) -> bool {
549        match &self.backend {
550            DataStoreBackend::Heap(map) => map.contains_key(key),
551            DataStoreBackend::Journaled { canonical, .. } => canonical.contains_key(key),
552        }
553    }
554
555    /// Read one visible row without cloning heap/live payloads.
556    pub(in crate::db) fn read<'a>(&'a self, key: &RawDataStoreKey) -> StoredRowRead<'a> {
557        #[cfg(test)]
558        record_data_store_get_call();
559
560        match &self.backend {
561            DataStoreBackend::Heap(map) => map
562                .get(key)
563                .map_or(StoredRowRead::Missing, StoredRowRead::Borrowed),
564            DataStoreBackend::Journaled {
565                canonical,
566                live,
567                tombstones,
568                ..
569            } => {
570                if tombstones.contains(key) {
571                    StoredRowRead::Missing
572                } else if let Some(row) = live.get(key) {
573                    StoredRowRead::Borrowed(row)
574                } else {
575                    canonical
576                        .get(key)
577                        .map_or(StoredRowRead::Missing, StoredRowRead::Owned)
578                }
579            }
580        }
581    }
582
583    /// Return whether one raw key exists without cloning the row payload.
584    #[must_use]
585    pub(in crate::db) fn contains(&self, key: &RawDataStoreKey) -> bool {
586        match &self.backend {
587            DataStoreBackend::Heap(map) => map.contains_key(key),
588            DataStoreBackend::Journaled {
589                canonical,
590                live,
591                tombstones,
592                ..
593            } => {
594                !tombstones.contains(key) && (live.contains_key(key) || canonical.contains_key(key))
595            }
596        }
597    }
598
599    /// Return whether the effective row domain is empty without reading payloads.
600    ///
601    /// Stop at the first visible key. Journal tombstones may hide canonical
602    /// keys, so physical canonical emptiness alone is not sufficient.
603    #[must_use]
604    pub(in crate::db) fn is_empty(&self) -> bool {
605        let mut empty = true;
606        let _: Result<(), Infallible> = self.visit_key_range(.., |_key| {
607            empty = false;
608            Ok(StoreVisit::Stop)
609        });
610        empty
611    }
612
613    /// Return an exact row count for tests without exposing the backing map.
614    #[cfg(test)]
615    #[must_use]
616    pub(in crate::db) fn len(&self) -> u64 {
617        match &self.backend {
618            DataStoreBackend::Heap(map) => u64::try_from(map.len()).unwrap_or(u64::MAX),
619            DataStoreBackend::Journaled { .. } => {
620                let mut count = 0_u64;
621                let _: Result<(), Infallible> = self.visit_entries(|_key, _row| {
622                    count = count.saturating_add(1);
623                    Ok(StoreVisit::Continue)
624                });
625                count
626            }
627        }
628    }
629
630    /// Return the row-store generation used to prove index metadata freshness.
631    #[must_use]
632    pub(in crate::db) const fn generation(&self) -> u64 {
633        self.generation
634    }
635
636    /// Return an exact current row count for one entity when store metadata is valid.
637    #[must_use]
638    pub(in crate::db) fn exact_entity_count(&self, entity: EntityTag) -> Option<u64> {
639        self.entity_cardinality.exact_count(entity)
640    }
641
642    /// Return the exact live-overlay delta from the canonical base for one entity.
643    #[must_use]
644    pub(in crate::db) fn exact_entity_cardinality_delta(&self, entity: EntityTag) -> Option<i64> {
645        match &self.backend {
646            DataStoreBackend::Heap(_) => Some(0),
647            DataStoreBackend::Journaled {
648                entity_cardinality_delta,
649                ..
650            } => entity_cardinality_delta.exact_delta(entity),
651        }
652    }
653
654    /// Visit raw row entries in canonical storage order.
655    pub(in crate::db) fn visit_entries<E>(
656        &self,
657        mut visitor: impl FnMut(&RawDataStoreKey, &RawRow) -> Result<StoreVisit, E>,
658    ) -> Result<(), E> {
659        match &self.backend {
660            DataStoreBackend::Heap(map) => {
661                for (key, row) in map {
662                    if visitor(key, row)?.should_stop() {
663                        break;
664                    }
665                }
666            }
667            DataStoreBackend::Journaled { .. } => Self::visit_journaled_entries_in_bounds(
668                &self.backend,
669                (Bound::Unbounded, Bound::Unbounded),
670                &mut visitor,
671            )?,
672        }
673
674        Ok(())
675    }
676
677    /// Visit canonical predecessor rows after one exclusive physical checkpoint.
678    ///
679    /// Optional derived-evidence builders use this view so newer live overlays
680    /// cannot contaminate a generation bound to the canonical fold watermark.
681    pub(in crate::db) fn visit_canonical_entries_after(
682        &self,
683        checkpoint: Option<&RawDataStoreKey>,
684        mut visitor: impl FnMut(&RawDataStoreKey, &RawRow) -> Result<bool, crate::error::InternalError>,
685    ) -> Result<(), crate::error::InternalError> {
686        let lower = checkpoint.map_or(Bound::Unbounded, Bound::Excluded);
687        match &self.backend {
688            DataStoreBackend::Heap(map) => {
689                for (key, row) in map.range((lower, Bound::Unbounded)) {
690                    if visitor(key, row)? {
691                        break;
692                    }
693                }
694            }
695            DataStoreBackend::Journaled { canonical, .. } => {
696                for entry in canonical.range((lower, Bound::Unbounded)) {
697                    if visitor(entry.key(), &entry.value())? {
698                        break;
699                    }
700                }
701            }
702        }
703        Ok(())
704    }
705
706    /// Return whether the canonical predecessor domain is physically empty.
707    ///
708    /// This bounded root observation never walks live overlays or materializes
709    /// row payloads.
710    pub(in crate::db) fn canonical_is_empty(&self) -> Result<bool, crate::error::InternalError> {
711        match &self.backend {
712            DataStoreBackend::Journaled { canonical, .. } => Ok(canonical.is_empty()),
713            DataStoreBackend::Heap(_) => Err(crate::error::InternalError::store_invariant()),
714        }
715    }
716
717    /// Visit raw row entries whose keys belong to the provided storage range.
718    pub(in crate::db) fn visit_range<E>(
719        &self,
720        key_range: impl RangeBounds<RawDataStoreKey>,
721        mut visitor: impl FnMut(&RawDataStoreKey, &RawRow) -> Result<StoreVisit, E>,
722    ) -> Result<(), E> {
723        let bounds = (key_range.start_bound(), key_range.end_bound());
724        match &self.backend {
725            DataStoreBackend::Heap(map) => {
726                for (key, row) in map.range(bounds) {
727                    if visitor(key, row)?.should_stop() {
728                        break;
729                    }
730                }
731            }
732            DataStoreBackend::Journaled { .. } => {
733                Self::visit_journaled_entries_in_bounds(&self.backend, bounds, &mut visitor)?;
734            }
735        }
736
737        Ok(())
738    }
739
740    /// Visit one ascending row range while allowing the caller to stop after
741    /// seeing the key but before a stable row payload is materialized.
742    ///
743    /// Mixed journal overlays retain the ordinary key-then-point-read path;
744    /// one physical backing can keep the range iterator open for the complete
745    /// scan and avoid a second tree lookup per row.
746    pub(in crate::db) fn try_visit_range_with_row_preflight<E>(
747        &self,
748        key_range: impl RangeBounds<RawDataStoreKey>,
749        mut preflight: impl FnMut(&RawDataStoreKey) -> Result<StoreVisit, E>,
750        mut visitor: impl FnMut(&RawDataStoreKey, &RawRow) -> Result<StoreVisit, E>,
751    ) -> Result<Option<bool>, E> {
752        let bounds = (key_range.start_bound(), key_range.end_bound());
753        let mut stopped = false;
754        match &self.backend {
755            DataStoreBackend::Heap(map) => {
756                for (key, row) in map.range(bounds) {
757                    if preflight(key)?.should_stop() {
758                        stopped = true;
759                        break;
760                    }
761                    if visitor(key, row)?.should_stop() {
762                        stopped = true;
763                        break;
764                    }
765                }
766            }
767            DataStoreBackend::Journaled {
768                canonical,
769                live,
770                tombstones,
771                ..
772            } if canonical.is_empty() => {
773                for (key, row) in live.range(bounds) {
774                    if tombstones.contains(key) {
775                        continue;
776                    }
777                    if preflight(key)?.should_stop() {
778                        stopped = true;
779                        break;
780                    }
781                    if visitor(key, row)?.should_stop() {
782                        stopped = true;
783                        break;
784                    }
785                }
786            }
787            DataStoreBackend::Journaled {
788                canonical,
789                live,
790                tombstones,
791                ..
792            } if live.is_empty() && tombstones.is_empty() => {
793                for entry in canonical.range(bounds) {
794                    if preflight(entry.key())?.should_stop() {
795                        stopped = true;
796                        break;
797                    }
798                    if visitor(entry.key(), &entry.value())?.should_stop() {
799                        stopped = true;
800                        break;
801                    }
802                }
803            }
804            DataStoreBackend::Journaled { .. } => return Ok(None),
805        }
806
807        Ok(Some(!stopped))
808    }
809
810    /// Visit only raw keys in storage order without fetching row payloads.
811    ///
812    /// Primary-key access streams use this boundary to discover candidate
813    /// identities before the terminal row runtime decides whether the payload
814    /// is needed. Journaled traversal merges canonical and live keys while
815    /// preserving live overrides and tombstones without reading stable values.
816    pub(in crate::db) fn visit_key_range<E>(
817        &self,
818        key_range: impl RangeBounds<RawDataStoreKey>,
819        visitor: impl FnMut(&RawDataStoreKey) -> Result<StoreVisit, E>,
820    ) -> Result<(), E> {
821        self.visit_keys_in_bounds(
822            (key_range.start_bound(), key_range.end_bound()),
823            false,
824            visitor,
825        )
826    }
827
828    /// Visit only raw keys in reverse storage order without fetching row payloads.
829    pub(in crate::db) fn visit_key_range_rev<E>(
830        &self,
831        key_range: impl RangeBounds<RawDataStoreKey>,
832        visitor: impl FnMut(&RawDataStoreKey) -> Result<StoreVisit, E>,
833    ) -> Result<(), E> {
834        self.visit_keys_in_bounds(
835            (key_range.start_bound(), key_range.end_bound()),
836            true,
837            visitor,
838        )
839    }
840
841    /// Independent physical byte baseline for storage-report tests.
842    #[cfg(test)]
843    pub(in crate::db) fn memory_bytes(&self) -> u64 {
844        // Report map footprint as key bytes + row bytes per entry.
845        let mut bytes = 0u64;
846        let _: Result<(), Infallible> = self.visit_entries(|key, row| {
847            bytes = bytes.saturating_add(key.as_bytes().len() as u64 + row.len() as u64);
848            Ok(StoreVisit::Continue)
849        });
850        bytes
851    }
852
853    const fn bump_generation(&mut self) {
854        self.generation = self.generation.saturating_add(1);
855    }
856
857    #[cfg(test)]
858    fn rebuild_entity_cardinality_from_entries(&mut self) {
859        let mut cardinality = EntityCardinality::empty();
860        let _: Result<(), Infallible> = self.visit_entries(|key, _row| {
861            cardinality.apply_present_entity(key.entity_tag_prefix());
862            Ok(StoreVisit::Continue)
863        });
864        self.entity_cardinality = cardinality;
865    }
866
867    fn apply_entity_overlay_delta(
868        &mut self,
869        entity: Option<EntityTag>,
870        previous_present: bool,
871        next_present: bool,
872    ) {
873        let DataStoreBackend::Journaled {
874            entity_cardinality_delta,
875            ..
876        } = &mut self.backend
877        else {
878            return;
879        };
880        entity_cardinality_delta.apply_presence_transition(entity, previous_present, next_present);
881    }
882
883    /// Return the monotonic perf-only count of stable row fetches seen by this process.
884    #[cfg(test)]
885    pub(in crate::db) fn current_get_call_count() -> u64 {
886        DATA_STORE_GET_CALL_COUNT.with(Cell::get)
887    }
888
889    fn visit_keys_in_bounds<E>(
890        &self,
891        bounds: (Bound<&RawDataStoreKey>, Bound<&RawDataStoreKey>),
892        reverse: bool,
893        mut visitor: impl FnMut(&RawDataStoreKey) -> Result<StoreVisit, E>,
894    ) -> Result<(), E> {
895        match &self.backend {
896            DataStoreBackend::Heap(map) => {
897                if reverse {
898                    for (key, _row) in map.range(bounds).rev() {
899                        if visitor(key)?.should_stop() {
900                            break;
901                        }
902                    }
903                } else {
904                    for (key, _row) in map.range(bounds) {
905                        if visitor(key)?.should_stop() {
906                            break;
907                        }
908                    }
909                }
910            }
911            DataStoreBackend::Journaled { .. } => {
912                Self::visit_journaled_keys_in_bounds(&self.backend, bounds, reverse, &mut visitor)?;
913            }
914        }
915
916        Ok(())
917    }
918
919    // Share journaled traversal across callback types while retaining typed
920    // caller errors. Key-only traversal must never materialize row payloads.
921    fn visit_journaled_keys_in_bounds<E>(
922        backend: &DataStoreBackend,
923        bounds: (Bound<&RawDataStoreKey>, Bound<&RawDataStoreKey>),
924        reverse: bool,
925        visitor: &mut dyn FnMut(&RawDataStoreKey) -> Result<StoreVisit, E>,
926    ) -> Result<(), E> {
927        let DataStoreBackend::Journaled {
928            canonical,
929            live,
930            tombstones,
931            ..
932        } = backend
933        else {
934            return Ok(());
935        };
936
937        if canonical.is_empty() {
938            if reverse {
939                for (key, _row) in live.range(bounds).rev() {
940                    if visitor(key)?.should_stop() {
941                        return Ok(());
942                    }
943                }
944            } else {
945                for (key, _row) in live.range(bounds) {
946                    if visitor(key)?.should_stop() {
947                        return Ok(());
948                    }
949                }
950            }
951            return Ok(());
952        }
953
954        if live.is_empty() && tombstones.is_empty() {
955            if reverse {
956                for entry in canonical.range(bounds).rev() {
957                    if visitor(entry.key())?.should_stop() {
958                        return Ok(());
959                    }
960                }
961            } else {
962                for entry in canonical.range(bounds) {
963                    if visitor(entry.key())?.should_stop() {
964                        return Ok(());
965                    }
966                }
967            }
968            return Ok(());
969        }
970
971        let direction = if reverse {
972            Direction::Desc
973        } else {
974            Direction::Asc
975        };
976        match direction {
977            Direction::Asc => {
978                for entry in ordered_overlay_entries(
979                    canonical.range(bounds),
980                    live.range(bounds),
981                    direction,
982                    |entry| entry.key(),
983                    |entry| entry.0,
984                    tombstones,
985                ) {
986                    let visit = match entry {
987                        OrderedOverlayEntry::Canonical(canonical_entry) => {
988                            visitor(canonical_entry.key())?
989                        }
990                        OrderedOverlayEntry::Live((key, _row)) => visitor(key)?,
991                    };
992                    if visit.should_stop() {
993                        return Ok(());
994                    }
995                }
996            }
997            Direction::Desc => {
998                for entry in ordered_overlay_entries(
999                    canonical.range(bounds).rev(),
1000                    live.range(bounds).rev(),
1001                    direction,
1002                    |entry| entry.key(),
1003                    |entry| entry.0,
1004                    tombstones,
1005                ) {
1006                    let visit = match entry {
1007                        OrderedOverlayEntry::Canonical(canonical_entry) => {
1008                            visitor(canonical_entry.key())?
1009                        }
1010                        OrderedOverlayEntry::Live((key, _row)) => visitor(key)?,
1011                    };
1012                    if visit.should_stop() {
1013                        return Ok(());
1014                    }
1015                }
1016            }
1017        }
1018
1019        Ok(())
1020    }
1021
1022    // Keep one journaled row traversal body per error type, independently of
1023    // each caller's closure. The ordered overlay remains the merge authority.
1024    fn visit_journaled_entries_in_bounds<E>(
1025        backend: &DataStoreBackend,
1026        bounds: (Bound<&RawDataStoreKey>, Bound<&RawDataStoreKey>),
1027        visitor: &mut dyn FnMut(&RawDataStoreKey, &RawRow) -> Result<StoreVisit, E>,
1028    ) -> Result<(), E> {
1029        let DataStoreBackend::Journaled {
1030            canonical,
1031            live,
1032            tombstones,
1033            ..
1034        } = backend
1035        else {
1036            return Ok(());
1037        };
1038
1039        if canonical.is_empty() {
1040            for (key, row) in live.range(bounds) {
1041                if visitor(key, row)?.should_stop() {
1042                    return Ok(());
1043                }
1044            }
1045            return Ok(());
1046        }
1047
1048        if live.is_empty() && tombstones.is_empty() {
1049            for entry in canonical.range(bounds) {
1050                if visitor(entry.key(), &entry.value())?.should_stop() {
1051                    return Ok(());
1052                }
1053            }
1054            return Ok(());
1055        }
1056
1057        for entry in ordered_overlay_entries(
1058            canonical.range(bounds),
1059            live.range(bounds),
1060            Direction::Asc,
1061            |entry| entry.key(),
1062            |entry| entry.0,
1063            tombstones,
1064        ) {
1065            let visit = match entry {
1066                OrderedOverlayEntry::Canonical(canonical_entry) => {
1067                    visitor(canonical_entry.key(), &canonical_entry.value())?
1068                }
1069                OrderedOverlayEntry::Live((key, row)) => visitor(key, row)?,
1070            };
1071            if visit.should_stop() {
1072                return Ok(());
1073            }
1074        }
1075
1076        Ok(())
1077    }
1078}
1079
1080#[derive(Clone, Debug)]
1081struct EntityCardinality {
1082    counts: HeapBTreeMap<EntityTag, u64>,
1083    decodable: bool,
1084}
1085
1086#[derive(Clone, Debug)]
1087struct EntityCardinalityDelta {
1088    counts: HeapBTreeMap<EntityTag, i64>,
1089    decodable: bool,
1090}
1091
1092impl EntityCardinalityDelta {
1093    const fn empty() -> Self {
1094        Self {
1095            counts: HeapBTreeMap::new(),
1096            decodable: true,
1097        }
1098    }
1099
1100    fn exact_delta(&self, entity: EntityTag) -> Option<i64> {
1101        self.decodable
1102            .then(|| self.counts.get(&entity).copied().unwrap_or(0))
1103    }
1104
1105    fn apply_presence_transition(
1106        &mut self,
1107        entity: Option<EntityTag>,
1108        previous_present: bool,
1109        next_present: bool,
1110    ) {
1111        if !self.decodable || previous_present == next_present {
1112            return;
1113        }
1114        let Some(entity) = entity else {
1115            self.counts.clear();
1116            self.decodable = false;
1117            return;
1118        };
1119        let delta = if next_present { 1_i64 } else { -1_i64 };
1120        let count = self.counts.entry(entity).or_insert(0);
1121        let Some(next) = count.checked_add(delta) else {
1122            self.counts.clear();
1123            self.decodable = false;
1124            return;
1125        };
1126        *count = next;
1127        if *count == 0 {
1128            self.counts.remove(&entity);
1129        }
1130    }
1131}
1132
1133impl EntityCardinality {
1134    const fn empty() -> Self {
1135        Self {
1136            counts: HeapBTreeMap::new(),
1137            decodable: true,
1138        }
1139    }
1140
1141    const fn unavailable() -> Self {
1142        Self {
1143            counts: HeapBTreeMap::new(),
1144            decodable: false,
1145        }
1146    }
1147
1148    fn exact_count(&self, entity: EntityTag) -> Option<u64> {
1149        self.decodable
1150            .then(|| self.counts.get(&entity).copied().unwrap_or(0))
1151    }
1152
1153    fn apply_insert(&mut self, entity: Option<EntityTag>, previous_present: bool) {
1154        if previous_present {
1155            return;
1156        }
1157        self.apply_present_entity(entity);
1158    }
1159
1160    fn apply_remove(&mut self, entity: Option<EntityTag>, previous_present: bool) {
1161        if !previous_present {
1162            return;
1163        }
1164        self.apply_removed_entity(entity);
1165    }
1166
1167    fn apply_present_entity(&mut self, entity: Option<EntityTag>) {
1168        if !self.decodable {
1169            return;
1170        }
1171        let Some(entity) = entity else {
1172            self.invalidate();
1173            return;
1174        };
1175
1176        let count = self.counts.entry(entity).or_insert(0);
1177        *count = count.saturating_add(1);
1178    }
1179
1180    fn apply_removed_entity(&mut self, entity: Option<EntityTag>) {
1181        if !self.decodable {
1182            return;
1183        }
1184        let Some(entity) = entity else {
1185            self.invalidate();
1186            return;
1187        };
1188
1189        if let Some(count) = self.counts.get_mut(&entity) {
1190            *count = count.saturating_sub(1);
1191            if *count == 0 {
1192                self.counts.remove(&entity);
1193            }
1194        }
1195    }
1196
1197    fn invalidate(&mut self) {
1198        self.counts.clear();
1199        self.decodable = false;
1200    }
1201}
1202
1203#[cfg(test)]
1204mod tests;