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