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