Skip to main content

icydb_core/db/index/
store.rs

1//! Module: index::store
2//! Responsibility: journaled-or-heap index-entry storage behind the index-store boundary.
3//! Does not own: range-scan resolution, continuation semantics, or predicate execution.
4//! Boundary: scan/executor layers depend on this storage boundary.
5
6use crate::db::index::{IndexId, IndexKeyKind};
7use crate::db::{
8    direction::Direction,
9    index::{
10        IndexEntryValue,
11        cardinality::{IndexPrefixCardinality, IndexPrefixCardinalityDelta},
12        key::RawIndexStoreKey,
13    },
14    journal::FoldWatermark,
15    ordered_overlay::{OrderedOverlayEntry, ordered_overlay_entries},
16    positioned_overlay::{
17        JournalOverlayPosition, PositionedOverlayMetadata, PositionedOverlayRetirement,
18    },
19};
20
21use candid::CandidType;
22use ic_memory::RuntimeMemory;
23use ic_memory::ic_stable_structures::{BTreeMap as StableBTreeMap, DefaultMemoryImpl};
24use serde::Deserialize;
25#[cfg(test)]
26use std::cell::Cell;
27use std::collections::{BTreeMap as HeapBTreeMap, BTreeSet};
28use std::ops::Bound;
29
30#[cfg(test)]
31thread_local! {
32    static INDEX_STORE_ENTRY_READ_COUNT: Cell<u64> = const { Cell::new(0) };
33}
34
35#[cfg(test)]
36fn record_index_store_entry_read() {
37    INDEX_STORE_ENTRY_READ_COUNT.with(|count| {
38        count.set(count.get().saturating_add(1));
39    });
40}
41
42fn visit_index_store_entry<E>(
43    key: &RawIndexStoreKey,
44    value: &IndexEntryValue,
45    visit: &mut impl FnMut(&RawIndexStoreKey, &IndexEntryValue) -> Result<bool, E>,
46) -> Result<bool, E> {
47    #[cfg(test)]
48    record_index_store_entry_read();
49
50    visit(key, value)
51}
52
53//
54// IndexState
55//
56// Explicit lifecycle visibility state for one index store.
57// Visibility matters because planner-visible indexes must already be complete:
58// the index contents are fully built and query-visible for reads.
59//
60#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
61pub enum IndexState {
62    Building,
63    #[default]
64    Ready,
65}
66
67impl IndexState {
68    /// Return the stable lowercase text label for this lifecycle state.
69    #[must_use]
70    pub const fn as_str(self) -> &'static str {
71        match self {
72            Self::Building => "building",
73            Self::Ready => "ready",
74        }
75    }
76}
77
78///
79/// IndexStore
80///
81/// Thin persistence wrapper over one journaled or heap BTreeMap.
82///
83/// Invariant: callers provide already-validated `RawIndexStoreKey`/`IndexEntryValue`.
84///
85
86pub struct IndexStore {
87    pub(super) backend: IndexStoreBackend,
88    generation: u64,
89    state: IndexState,
90    access_state_revision: u64,
91    prefix_cardinality: IndexPrefixCardinality,
92}
93
94pub(super) enum IndexStoreBackend {
95    Heap(HeapBTreeMap<RawIndexStoreKey, IndexEntryValue>),
96    Journaled {
97        canonical:
98            StableBTreeMap<RawIndexStoreKey, IndexEntryValue, RuntimeMemory<DefaultMemoryImpl>>,
99        live: HeapBTreeMap<RawIndexStoreKey, IndexEntryValue>,
100        tombstones: BTreeSet<RawIndexStoreKey>,
101        positions: PositionedOverlayMetadata<RawIndexStoreKey>,
102        prefix_cardinality_delta: Box<IndexPrefixCardinalityDelta>,
103    },
104}
105
106/// Preflighted provenance publication for explicit journal index records.
107pub(in crate::db) struct PreparedIndexPositionPublication {
108    keys: Vec<RawIndexStoreKey>,
109    position: JournalOverlayPosition,
110}
111
112/// Preflighted exact retirement for one complete journal batch.
113pub(in crate::db) struct PreparedIndexPositionRetirement {
114    entries: Vec<(RawIndexStoreKey, PositionedOverlayRetirement)>,
115}
116
117/// Control-flow result for index-store traversal visitors.
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub(in crate::db) enum IndexStoreVisit {
120    Continue,
121    Stop,
122}
123
124impl IndexStoreVisit {
125    const fn should_stop(self) -> bool {
126        matches!(self, Self::Stop)
127    }
128}
129
130impl IndexStore {
131    /// Initialize a volatile heap-backed index store.
132    #[must_use]
133    pub const fn init_heap() -> Self {
134        Self {
135            backend: IndexStoreBackend::Heap(HeapBTreeMap::new()),
136            generation: 0,
137            state: IndexState::Ready,
138            access_state_revision: 1,
139            prefix_cardinality: IndexPrefixCardinality::synchronized_empty(),
140        }
141    }
142
143    /// Initialize a journaled cached-stable index store.
144    ///
145    /// Normal writes update only the live materialized projection. The
146    /// canonical stable index is updated by future fold/rebuild paths.
147    #[must_use]
148    pub fn init_journaled(memory: RuntimeMemory<DefaultMemoryImpl>) -> Self {
149        let canonical = StableBTreeMap::init(memory);
150        let prefix_cardinality = if canonical.is_empty() {
151            IndexPrefixCardinality::synchronized_empty()
152        } else {
153            IndexPrefixCardinality::unavailable()
154        };
155        Self {
156            backend: IndexStoreBackend::Journaled {
157                canonical,
158                live: HeapBTreeMap::new(),
159                tombstones: BTreeSet::new(),
160                positions: PositionedOverlayMetadata::new(),
161                prefix_cardinality_delta: Box::new(IndexPrefixCardinalityDelta::unbound_empty()),
162            },
163            generation: 0,
164            state: IndexState::Ready,
165            access_state_revision: 1,
166            // Exact zero cardinality is known for an empty canonical map;
167            // populated maps remain unavailable without a startup scan.
168            prefix_cardinality,
169        }
170    }
171
172    /// Visit all index entries in canonical store order without exposing the
173    /// backing stable-map iterator.
174    pub(in crate::db) fn visit_entries<E>(
175        &self,
176        mut visitor: impl FnMut(&RawIndexStoreKey, &IndexEntryValue) -> Result<IndexStoreVisit, E>,
177    ) -> Result<(), E> {
178        match &self.backend {
179            IndexStoreBackend::Heap(map) => {
180                for (key, value) in map {
181                    #[cfg(test)]
182                    record_index_store_entry_read();
183
184                    if visitor(key, value)?.should_stop() {
185                        return Ok(());
186                    }
187                }
188            }
189            IndexStoreBackend::Journaled { .. } => self.visit_journaled_entries_in_range(
190                (&Bound::Unbounded, &Bound::Unbounded),
191                Direction::Asc,
192                |key, value| visitor(key, value).map(IndexStoreVisit::should_stop),
193            )?,
194        }
195
196        Ok(())
197    }
198
199    pub(in crate::db) fn get(&self, key: &RawIndexStoreKey) -> Option<IndexEntryValue> {
200        match &self.backend {
201            IndexStoreBackend::Heap(map) => map.get(key).cloned(),
202            IndexStoreBackend::Journaled { .. } => Self::journaled_get(&self.backend, key),
203        }
204    }
205
206    /// Load one index entry from the canonical predecessor view.
207    pub(in crate::db) fn get_canonical(&self, key: &RawIndexStoreKey) -> Option<IndexEntryValue> {
208        match &self.backend {
209            IndexStoreBackend::Heap(map) => map.get(key).cloned(),
210            IndexStoreBackend::Journaled { canonical, .. } => canonical.get(key),
211        }
212    }
213
214    /// Return whether the canonical predecessor domain is physically empty.
215    ///
216    /// This bounded root observation never walks live overlays or materializes
217    /// index entries.
218    pub(in crate::db) fn canonical_is_empty(&self) -> Result<bool, crate::error::InternalError> {
219        match &self.backend {
220            IndexStoreBackend::Journaled { canonical, .. } => Ok(canonical.is_empty()),
221            IndexStoreBackend::Heap(_) => Err(crate::error::InternalError::store_invariant()),
222        }
223    }
224
225    pub fn len(&self) -> u64 {
226        match &self.backend {
227            IndexStoreBackend::Heap(map) => u64::try_from(map.len()).unwrap_or(u64::MAX),
228            IndexStoreBackend::Journaled { .. } => {
229                let mut count = 0_u64;
230                let _: Result<(), std::convert::Infallible> = self.visit_entries(|_key, _value| {
231                    count = count.saturating_add(1);
232                    Ok(IndexStoreVisit::Continue)
233                });
234                count
235            }
236        }
237    }
238
239    pub fn is_empty(&self) -> bool {
240        match &self.backend {
241            IndexStoreBackend::Heap(map) => map.is_empty(),
242            IndexStoreBackend::Journaled { .. } => {
243                let mut empty = true;
244                let _: Result<(), std::convert::Infallible> = self.visit_entries(|_key, _value| {
245                    empty = false;
246                    Ok(IndexStoreVisit::Stop)
247                });
248                empty
249            }
250        }
251    }
252
253    #[must_use]
254    pub(in crate::db) const fn generation(&self) -> u64 {
255        self.generation
256    }
257
258    /// Return the explicit lifecycle state for this index store.
259    #[must_use]
260    pub(in crate::db) const fn state(&self) -> IndexState {
261        self.state
262    }
263
264    /// Return the current physical access-readiness revision.
265    #[must_use]
266    pub(in crate::db) const fn access_state_revision(&self) -> u64 {
267        self.access_state_revision
268    }
269
270    /// Return an exact user-index prefix count when the index metadata is
271    /// synchronized with the caller's authoritative row-store generation.
272    #[must_use]
273    pub(in crate::db) fn exact_prefix_cardinality(
274        &self,
275        data_generation: u64,
276        key_kind: IndexKeyKind,
277        index_id: IndexId,
278        components: &[Vec<u8>],
279    ) -> Option<u64> {
280        self.prefix_cardinality
281            .exact_count(data_generation, key_kind, index_id, components)
282    }
283
284    /// Sum exact first-component multiplicities within the caller's bounded range and work cap.
285    pub(in crate::db) fn exact_first_component_range_cardinality(
286        &self,
287        data_generation: u64,
288        index_id: IndexId,
289        lower: &Bound<Vec<u8>>,
290        upper: &Bound<Vec<u8>>,
291        stop_after: u64,
292    ) -> Result<Option<(u64, u64, bool)>, crate::error::InternalError> {
293        self.prefix_cardinality.exact_first_component_range_count(
294            data_generation,
295            index_id,
296            lower,
297            upper,
298            stop_after,
299        )
300    }
301
302    pub(in crate::db) fn exact_first_component_numeric_fold(
303        &self,
304        data_generation: u64,
305        index_id: IndexId,
306        stop_after: u64,
307    ) -> Result<Option<(u64, i128, u64, bool)>, crate::error::InternalError> {
308        self.prefix_cardinality.exact_first_component_numeric_fold(
309            data_generation,
310            index_id,
311            stop_after,
312        )
313    }
314
315    /// Return the exact live-overlay delta from canonical for one user-index prefix.
316    #[must_use]
317    pub(in crate::db) fn exact_prefix_cardinality_delta(
318        &self,
319        key_kind: IndexKeyKind,
320        index_id: IndexId,
321        components: &[Vec<u8>],
322    ) -> Option<i64> {
323        match &self.backend {
324            IndexStoreBackend::Heap(_) => Some(0),
325            IndexStoreBackend::Journaled {
326                prefix_cardinality_delta,
327                ..
328            } => prefix_cardinality_delta.exact_delta(key_kind, index_id, components),
329        }
330    }
331
332    /// Return the canonical fold boundary owning the exact live-overlay delta.
333    #[must_use]
334    pub(in crate::db) fn exact_prefix_cardinality_delta_watermark(&self) -> Option<FoldWatermark> {
335        match &self.backend {
336            IndexStoreBackend::Heap(_) => None,
337            IndexStoreBackend::Journaled {
338                prefix_cardinality_delta,
339                ..
340            } => prefix_cardinality_delta.base_watermark(),
341        }
342    }
343
344    /// Return the sum of exact prefix counts for prefixes on the same index
345    /// when synchronized metadata can prove all requested counts.
346    #[must_use]
347    pub(in crate::db) fn exact_prefix_cardinality_sum<'a>(
348        &self,
349        data_generation: u64,
350        key_kind: IndexKeyKind,
351        index_id: IndexId,
352        component_prefixes: impl IntoIterator<Item = &'a [Vec<u8>]>,
353        stop_after: Option<u64>,
354    ) -> Option<u64> {
355        self.prefix_cardinality.exact_count_sum(
356            data_generation,
357            key_kind,
358            index_id,
359            component_prefixes,
360            stop_after,
361        )
362    }
363
364    /// Return non-empty exact child prefixes under a sparse set of already-encoded
365    /// parent prefixes when synchronized metadata can prove the bounded child set.
366    #[must_use]
367    pub(in crate::db) fn exact_child_prefixes_for_parent_set<'a>(
368        &self,
369        data_generation: u64,
370        key_kind: IndexKeyKind,
371        index_id: IndexId,
372        parent_component_prefixes: impl IntoIterator<Item = &'a [Vec<u8>]>,
373        max_children: usize,
374    ) -> Option<Vec<Vec<Vec<u8>>>> {
375        self.prefix_cardinality.exact_child_prefixes_for_parent_set(
376            data_generation,
377            key_kind,
378            index_id,
379            parent_component_prefixes,
380            max_children,
381        )
382    }
383
384    /// Mark prefix-cardinality metadata synchronized with the authoritative
385    /// row-store generation after a committed row/index transition.
386    pub(in crate::db) const fn mark_prefix_cardinality_data_generation(&mut self, generation: u64) {
387        self.prefix_cardinality.mark_synchronized(generation);
388    }
389
390    /// Mark this index store as in-progress and therefore ineligible for
391    /// planner visibility until a full authoritative rebuild ends.
392    pub(in crate::db) const fn set_access_state(&mut self, state: IndexState, revision: u64) {
393        self.state = state;
394        self.access_state_revision = revision;
395    }
396
397    pub(crate) fn insert(
398        &mut self,
399        key: RawIndexStoreKey,
400        entry: IndexEntryValue,
401    ) -> Option<IndexEntryValue> {
402        let previous_journaled = if matches!(self.backend, IndexStoreBackend::Journaled { .. }) {
403            self.get(&key)
404        } else {
405            None
406        };
407        let cardinality_key = key.clone();
408        let previous = match &mut self.backend {
409            IndexStoreBackend::Heap(map) => map.insert(key, entry.clone()),
410            IndexStoreBackend::Journaled {
411                live, tombstones, ..
412            } => {
413                tombstones.remove(&key);
414                live.insert(key, entry.clone());
415                previous_journaled
416            }
417        };
418        self.prefix_cardinality
419            .apply_insert(&cardinality_key, previous.as_ref(), &entry);
420        self.apply_prefix_overlay_delta(&cardinality_key, previous.as_ref(), Some(&entry));
421        self.bump_generation();
422        previous
423    }
424
425    /// Insert one key whose absence was proved by complete-domain staging.
426    ///
427    /// Accepted-schema replacement first removes its complete current user
428    /// domain. Raw index identity makes every final key owner-local, so no
429    /// canonical point lookup can add information during mechanical Apply.
430    pub(in crate::db) fn insert_preflighted_absent(
431        &mut self,
432        key: RawIndexStoreKey,
433        entry: IndexEntryValue,
434    ) {
435        let cardinality_key = key.clone();
436        match &mut self.backend {
437            IndexStoreBackend::Heap(map) => {
438                map.insert(key, entry.clone());
439            }
440            IndexStoreBackend::Journaled {
441                live, tombstones, ..
442            } => {
443                tombstones.remove(&key);
444                live.insert(key, entry.clone());
445            }
446        }
447        self.prefix_cardinality
448            .apply_insert(&cardinality_key, None, &entry);
449        self.apply_prefix_overlay_delta(&cardinality_key, None, Some(&entry));
450        self.bump_generation();
451    }
452
453    pub(crate) fn remove(&mut self, key: &RawIndexStoreKey) -> Option<IndexEntryValue> {
454        let previous_journaled = if matches!(self.backend, IndexStoreBackend::Journaled { .. }) {
455            self.get(key)
456        } else {
457            None
458        };
459        let previous = match &mut self.backend {
460            IndexStoreBackend::Heap(map) => map.remove(key),
461            IndexStoreBackend::Journaled {
462                live, tombstones, ..
463            } => {
464                live.remove(key);
465                tombstones.insert(key.clone());
466                previous_journaled
467            }
468        };
469        self.prefix_cardinality.apply_remove(key, previous.as_ref());
470        self.apply_prefix_overlay_delta(key, previous.as_ref(), None);
471        self.bump_generation();
472        previous
473    }
474
475    /// Reset the disposable journaled index overlay without traversing or
476    /// mutating the canonical stable index.
477    pub(in crate::db) fn reset_journaled_live_projection(
478        &mut self,
479        data_generation: u64,
480        fold_watermark: FoldWatermark,
481    ) -> Result<(), crate::error::InternalError> {
482        let IndexStoreBackend::Journaled {
483            canonical,
484            live,
485            tombstones,
486            positions,
487            prefix_cardinality_delta,
488        } = &mut self.backend
489        else {
490            return Err(crate::error::InternalError::store_invariant());
491        };
492
493        live.clear();
494        tombstones.clear();
495        positions.clear();
496        prefix_cardinality_delta.reset(fold_watermark);
497        self.prefix_cardinality = if canonical.is_empty() {
498            let mut cardinality = IndexPrefixCardinality::synchronized_empty();
499            cardinality.mark_synchronized(data_generation);
500            cardinality
501        } else {
502            IndexPrefixCardinality::unavailable()
503        };
504        self.bump_generation();
505
506        Ok(())
507    }
508
509    /// Preflight movement of the exact delta's canonical base boundary.
510    pub(in crate::db) fn preflight_prefix_cardinality_delta_watermark(
511        &self,
512        current: FoldWatermark,
513    ) -> Result<(), crate::error::InternalError> {
514        (self.exact_prefix_cardinality_delta_watermark() == Some(current))
515            .then_some(())
516            .ok_or_else(crate::error::InternalError::store_corruption)
517    }
518
519    /// Publish a preflighted canonical base movement after the complete fold.
520    pub(in crate::db) fn apply_prefix_cardinality_delta_watermark(
521        &mut self,
522        current: FoldWatermark,
523        next: FoldWatermark,
524    ) {
525        if let IndexStoreBackend::Journaled {
526            prefix_cardinality_delta,
527            ..
528        } = &mut self.backend
529        {
530            prefix_cardinality_delta.advance_watermark(current, next);
531        }
532    }
533
534    /// Publish one preflighted positioned derived or explicit index effect.
535    pub(in crate::db) fn publish_preflighted_journal_entry(
536        &mut self,
537        key: RawIndexStoreKey,
538        value: Option<IndexEntryValue>,
539        position: JournalOverlayPosition,
540    ) -> Result<Option<IndexEntryValue>, crate::error::InternalError> {
541        let IndexStoreBackend::Journaled {
542            canonical,
543            live,
544            tombstones,
545            positions,
546            prefix_cardinality_delta,
547        } = &mut self.backend
548        else {
549            return Err(crate::error::InternalError::store_invariant());
550        };
551        let previous = if tombstones.contains(&key) {
552            None
553        } else {
554            live.get(&key).cloned().or_else(|| canonical.get(&key))
555        };
556        let cardinality_key = key.clone();
557        let next_value = value.clone();
558
559        if let Some(value) = value {
560            tombstones.remove(&key);
561            live.insert(key.clone(), value.clone());
562            self.prefix_cardinality
563                .apply_insert(&cardinality_key, previous.as_ref(), &value);
564        } else {
565            live.remove(&key);
566            tombstones.insert(key.clone());
567            self.prefix_cardinality
568                .apply_remove(&cardinality_key, previous.as_ref());
569        }
570        prefix_cardinality_delta.apply_transition(
571            &cardinality_key,
572            previous.as_ref(),
573            next_value.as_ref(),
574        );
575        positions.publish_preflighted(key, position);
576        self.bump_generation();
577
578        Ok(previous)
579    }
580
581    /// Validate and publish one positioned index effect for direct store tests.
582    #[cfg(test)]
583    pub(in crate::db) fn publish_positioned_journal_entry(
584        &mut self,
585        key: RawIndexStoreKey,
586        value: Option<IndexEntryValue>,
587        position: JournalOverlayPosition,
588    ) -> Result<Option<IndexEntryValue>, crate::error::InternalError> {
589        self.preflight_positioned_journal_entry(&key, position)?;
590        self.publish_preflighted_journal_entry(key, value, position)
591    }
592
593    /// Preflight index provenance before marker publication.
594    pub(in crate::db) fn preflight_positioned_journal_entry(
595        &self,
596        key: &RawIndexStoreKey,
597        position: JournalOverlayPosition,
598    ) -> Result<(), crate::error::InternalError> {
599        let IndexStoreBackend::Journaled { positions, .. } = &self.backend else {
600            return Err(crate::error::InternalError::store_invariant());
601        };
602        positions.preflight_publish(key, position)
603    }
604
605    /// Preflight explicit index provenance before marker publication.
606    pub(in crate::db) fn prepare_position_publication(
607        &self,
608        keys: impl IntoIterator<Item = RawIndexStoreKey>,
609        position: JournalOverlayPosition,
610    ) -> Result<PreparedIndexPositionPublication, crate::error::InternalError> {
611        let IndexStoreBackend::Journaled { positions, .. } = &self.backend else {
612            return Err(crate::error::InternalError::store_invariant());
613        };
614        let keys = keys.into_iter().collect::<BTreeSet<_>>();
615        for key in &keys {
616            positions.preflight_publish(key, position)?;
617        }
618        Ok(PreparedIndexPositionPublication {
619            keys: keys.into_iter().collect(),
620            position,
621        })
622    }
623
624    /// Publish explicit index provenance after its values have been applied.
625    pub(in crate::db) fn publish_prepared_positions(
626        &mut self,
627        prepared: PreparedIndexPositionPublication,
628    ) {
629        let IndexStoreBackend::Journaled { positions, .. } = &mut self.backend else {
630            debug_assert!(
631                false,
632                "preflighted index positions require a journaled store"
633            );
634            return;
635        };
636        for key in prepared.keys {
637            positions.publish_preflighted(key, prepared.position);
638        }
639    }
640
641    /// Preflight exact index-overlay retirement before canonical mutation.
642    pub(in crate::db) fn prepare_position_retirement(
643        &self,
644        keys: impl IntoIterator<Item = RawIndexStoreKey>,
645        position: JournalOverlayPosition,
646    ) -> Result<PreparedIndexPositionRetirement, crate::error::InternalError> {
647        let IndexStoreBackend::Journaled { positions, .. } = &self.backend else {
648            return Err(crate::error::InternalError::store_invariant());
649        };
650        let entries = keys
651            .into_iter()
652            .collect::<BTreeSet<_>>()
653            .into_iter()
654            .map(|key| {
655                positions
656                    .preflight_retirement(&key, position)
657                    .map(|retirement| (key, retirement))
658            })
659            .collect::<Result<Vec<_>, _>>()?;
660        Ok(PreparedIndexPositionRetirement { entries })
661    }
662
663    /// Retire only exact index overlays after canonical mutation succeeds.
664    pub(in crate::db) fn apply_prepared_position_retirement(
665        &mut self,
666        prepared: PreparedIndexPositionRetirement,
667    ) {
668        let IndexStoreBackend::Journaled {
669            live,
670            tombstones,
671            positions,
672            ..
673        } = &mut self.backend
674        else {
675            debug_assert!(
676                false,
677                "preflighted index retirement requires a journaled store"
678            );
679            return;
680        };
681        for (key, retirement) in prepared.entries {
682            if retirement == PositionedOverlayRetirement::Exact {
683                live.remove(&key);
684                tombstones.remove(&key);
685                positions.retire_preflighted(&key, retirement);
686            }
687        }
688    }
689
690    /// Apply one exact index entry directly to canonical stable storage.
691    ///
692    /// Journal folding and planner-invisible migration staging share this
693    /// primitive. Callers own key authority, journal watermarks and position
694    /// retirement; unrelated live entries and their prefix deltas stay intact.
695    pub(in crate::db) fn apply_canonical_entry(
696        &mut self,
697        key: RawIndexStoreKey,
698        value: Option<IndexEntryValue>,
699    ) -> Result<(), crate::error::InternalError> {
700        let IndexStoreBackend::Journaled {
701            canonical,
702            live,
703            tombstones,
704            ..
705        } = &mut self.backend
706        else {
707            return Err(crate::error::InternalError::store_invariant());
708        };
709
710        let visible = !live.contains_key(&key) && !tombstones.contains(&key);
711        let cardinality_key = key.clone();
712        let previous = if let Some(value) = value.as_ref() {
713            canonical.insert(key, value.clone())
714        } else {
715            canonical.remove(&key)
716        };
717        if visible {
718            if let Some(value) = value.as_ref() {
719                self.prefix_cardinality
720                    .apply_insert(&cardinality_key, previous.as_ref(), value);
721            } else {
722                self.prefix_cardinality
723                    .apply_remove(&cardinality_key, previous.as_ref());
724            }
725        } else {
726            self.apply_prefix_overlay_delta(&cardinality_key, value.as_ref(), previous.as_ref());
727        }
728        self.bump_generation();
729
730        Ok(())
731    }
732
733    /// Prove that recovered journal entries can be folded into canonical storage.
734    pub(in crate::db) fn preflight_fold_recovered_journal(
735        &self,
736    ) -> Result<(), crate::error::InternalError> {
737        match self.backend {
738            IndexStoreBackend::Journaled { .. } => Ok(()),
739            IndexStoreBackend::Heap(_) => Err(crate::error::InternalError::store_invariant()),
740        }
741    }
742
743    pub fn clear(&mut self) {
744        match &mut self.backend {
745            IndexStoreBackend::Heap(map) => map.clear(),
746            IndexStoreBackend::Journaled {
747                canonical,
748                live,
749                tombstones,
750                prefix_cardinality_delta,
751                ..
752            } => {
753                live.clear();
754                tombstones.clear();
755                for entry in canonical.iter() {
756                    tombstones.insert(entry.key().clone());
757                }
758                prefix_cardinality_delta.clear_unavailable();
759            }
760        }
761        self.prefix_cardinality.clear_unsynchronized();
762        self.bump_generation();
763    }
764
765    /// Sum of bytes used by all stored index entries.
766    pub fn memory_bytes(&self) -> u64 {
767        let mut bytes = 0u64;
768        let _: Result<(), std::convert::Infallible> = self.visit_entries(|key, value| {
769            bytes = bytes.saturating_add(key.as_bytes().len() as u64 + value.len() as u64);
770            Ok(IndexStoreVisit::Continue)
771        });
772        bytes
773    }
774
775    /// Return the monotonic perf-only count of index entries yielded by traversal.
776    #[cfg(test)]
777    pub(in crate::db) fn current_entry_read_count() -> u64 {
778        INDEX_STORE_ENTRY_READ_COUNT.with(Cell::get)
779    }
780
781    const fn bump_generation(&mut self) {
782        self.generation = self.generation.saturating_add(1);
783    }
784
785    fn apply_prefix_overlay_delta(
786        &mut self,
787        key: &RawIndexStoreKey,
788        previous: Option<&IndexEntryValue>,
789        next: Option<&IndexEntryValue>,
790    ) {
791        let IndexStoreBackend::Journaled {
792            prefix_cardinality_delta,
793            ..
794        } = &mut self.backend
795        else {
796            return;
797        };
798        prefix_cardinality_delta.apply_transition(key, previous, next);
799    }
800
801    fn journaled_get(
802        backend: &IndexStoreBackend,
803        key: &RawIndexStoreKey,
804    ) -> Option<IndexEntryValue> {
805        let IndexStoreBackend::Journaled {
806            canonical,
807            live,
808            tombstones,
809            ..
810        } = backend
811        else {
812            return None;
813        };
814
815        if tombstones.contains(key) {
816            return None;
817        }
818        live.get(key).cloned().or_else(|| canonical.get(key))
819    }
820
821    pub(super) fn visit_journaled_entries_in_range<E>(
822        &self,
823        bounds: (&Bound<RawIndexStoreKey>, &Bound<RawIndexStoreKey>),
824        direction: Direction,
825        mut visit: impl FnMut(&RawIndexStoreKey, &IndexEntryValue) -> Result<bool, E>,
826    ) -> Result<(), E> {
827        let IndexStoreBackend::Journaled {
828            canonical,
829            live,
830            tombstones,
831            ..
832        } = &self.backend
833        else {
834            return Ok(());
835        };
836
837        let lower = bounds.0.as_ref();
838        let upper = bounds.1.as_ref();
839        match direction {
840            Direction::Asc if canonical.is_empty() => {
841                for (key, value) in live.range((lower, upper)) {
842                    if visit_index_store_entry(key, value, &mut visit)? {
843                        return Ok(());
844                    }
845                }
846            }
847            Direction::Desc if canonical.is_empty() => {
848                for (key, value) in live.range((lower, upper)).rev() {
849                    if visit_index_store_entry(key, value, &mut visit)? {
850                        return Ok(());
851                    }
852                }
853            }
854            Direction::Asc if live.is_empty() && tombstones.is_empty() => {
855                for entry in canonical.range((lower, upper)) {
856                    if visit_index_store_entry(entry.key(), &entry.value(), &mut visit)? {
857                        return Ok(());
858                    }
859                }
860            }
861            Direction::Desc if live.is_empty() && tombstones.is_empty() => {
862                for entry in canonical.range((lower, upper)).rev() {
863                    if visit_index_store_entry(entry.key(), &entry.value(), &mut visit)? {
864                        return Ok(());
865                    }
866                }
867            }
868            Direction::Asc => {
869                for entry in ordered_overlay_entries(
870                    canonical.range((lower, upper)),
871                    live.range((lower, upper)),
872                    direction,
873                    |entry| entry.key(),
874                    |entry| entry.0,
875                    tombstones,
876                ) {
877                    let should_stop = match entry {
878                        OrderedOverlayEntry::Canonical(canonical_entry) => visit_index_store_entry(
879                            canonical_entry.key(),
880                            &canonical_entry.value(),
881                            &mut visit,
882                        )?,
883                        OrderedOverlayEntry::Live((key, value)) => {
884                            visit_index_store_entry(key, value, &mut visit)?
885                        }
886                    };
887                    if should_stop {
888                        return Ok(());
889                    }
890                }
891            }
892            Direction::Desc => {
893                for entry in ordered_overlay_entries(
894                    canonical.range((lower, upper)).rev(),
895                    live.range((lower, upper)).rev(),
896                    direction,
897                    |entry| entry.key(),
898                    |entry| entry.0,
899                    tombstones,
900                ) {
901                    let should_stop = match entry {
902                        OrderedOverlayEntry::Canonical(canonical_entry) => visit_index_store_entry(
903                            canonical_entry.key(),
904                            &canonical_entry.value(),
905                            &mut visit,
906                        )?,
907                        OrderedOverlayEntry::Live((key, value)) => {
908                            visit_index_store_entry(key, value, &mut visit)?
909                        }
910                    };
911                    if should_stop {
912                        return Ok(());
913                    }
914                }
915            }
916        }
917
918        Ok(())
919    }
920}
921
922#[cfg(test)]
923mod tests {
924    use super::*;
925    use crate::{
926        db::{
927            direction::Direction,
928            index::{IndexId, IndexKey, IndexKeyKind},
929            journal::JournalSequence,
930            key_taxonomy::{PrimaryKeyComponent, PrimaryKeyValue},
931            positioned_overlay::{JournalOverlayPosition, PositionedOverlayRetirement},
932            registry::StoreAllocationIdentity,
933        },
934        testing::test_memory,
935        types::EntityTag,
936    };
937    use ic_memory::ic_stable_structures::Storable;
938    use std::{borrow::Cow, convert::Infallible};
939
940    fn raw_key(value: u8) -> RawIndexStoreKey {
941        <RawIndexStoreKey as Storable>::from_bytes(Cow::Owned(vec![value]))
942    }
943
944    fn overlay_position(sequence: u64) -> JournalOverlayPosition {
945        JournalOverlayPosition::new(
946            StoreAllocationIdentity::new(231, "test::index"),
947            JournalSequence::new(sequence),
948        )
949    }
950
951    fn indexed_raw_key(
952        index_id: &IndexId,
953        components: Vec<Vec<u8>>,
954        primary_key: u64,
955    ) -> RawIndexStoreKey {
956        indexed_raw_key_with_kind(index_id, IndexKeyKind::User, components, primary_key)
957    }
958
959    fn indexed_raw_key_with_kind(
960        index_id: &IndexId,
961        key_kind: IndexKeyKind,
962        components: Vec<Vec<u8>>,
963        primary_key: u64,
964    ) -> RawIndexStoreKey {
965        IndexKey::new_from_components_with_primary_key_value(
966            index_id,
967            key_kind,
968            components.as_slice(),
969            &PrimaryKeyValue::from(PrimaryKeyComponent::Nat64(primary_key)),
970        )
971        .expect("test index key should build")
972        .to_raw()
973        .expect("test index key should encode")
974    }
975
976    fn malformed_index_entry_value() -> IndexEntryValue {
977        <IndexEntryValue as Storable>::from_bytes(Cow::Owned(vec![0xFF]))
978    }
979
980    fn missing_index_entry_value() -> IndexEntryValue {
981        <IndexEntryValue as Storable>::from_bytes(Cow::Owned(vec![1]))
982    }
983
984    #[test]
985    fn index_prefix_cardinality_requires_explicit_data_generation_sync() {
986        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
987        let collection = b"collection-a".to_vec();
988        let draft = b"Draft".to_vec();
989        let review = b"Review".to_vec();
990        let mut store = IndexStore::init_heap();
991
992        store.insert(
993            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
994            IndexEntryValue::presence(),
995        );
996        store.insert(
997            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 2),
998            IndexEntryValue::presence(),
999        );
1000        store.insert(
1001            indexed_raw_key(&index_id, vec![collection.clone(), review.clone()], 3),
1002            IndexEntryValue::presence(),
1003        );
1004
1005        assert_eq!(
1006            store.exact_prefix_cardinality(
1007                0,
1008                IndexKeyKind::User,
1009                index_id,
1010                std::slice::from_ref(&collection),
1011            ),
1012            None,
1013            "raw index mutations must not be trusted until row generation sync is stamped",
1014        );
1015
1016        store.mark_prefix_cardinality_data_generation(7);
1017
1018        assert_eq!(
1019            store.exact_prefix_cardinality(
1020                7,
1021                IndexKeyKind::User,
1022                index_id,
1023                std::slice::from_ref(&collection),
1024            ),
1025            Some(3),
1026        );
1027        assert_eq!(
1028            store.exact_prefix_cardinality(
1029                7,
1030                IndexKeyKind::User,
1031                index_id,
1032                &[collection.clone(), draft],
1033            ),
1034            Some(2),
1035        );
1036        assert_eq!(
1037            store.exact_prefix_cardinality(8, IndexKeyKind::User, index_id, &[collection, review],),
1038            None,
1039            "row generation drift should force the caller to use the existing-row fallback",
1040        );
1041    }
1042
1043    #[test]
1044    fn first_component_range_cardinality_is_exact_bounded_and_generation_matched() {
1045        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1046        let alpha = b"alpha".to_vec();
1047        let beta = b"beta".to_vec();
1048        let alpha_one = indexed_raw_key(&index_id, vec![alpha.clone()], 1);
1049        let alpha_two = indexed_raw_key(&index_id, vec![alpha], 2);
1050        let beta_one = indexed_raw_key(&index_id, vec![beta], 3);
1051        let mut store = IndexStore::init_heap();
1052        let bound = Bound::Unbounded;
1053
1054        assert_eq!(
1055            store
1056                .exact_first_component_range_cardinality(0, index_id, &bound, &bound, 1)
1057                .expect("initialized metadata should be structurally valid"),
1058            Some((0, 0, true)),
1059            "initialized empty metadata must positively prove exact zero",
1060        );
1061        store.insert(alpha_one.clone(), IndexEntryValue::presence());
1062        store.insert(alpha_two.clone(), IndexEntryValue::presence());
1063        store.insert(beta_one.clone(), IndexEntryValue::presence());
1064        assert_eq!(
1065            store
1066                .exact_first_component_range_cardinality(0, index_id, &bound, &bound, 3)
1067                .expect("invalidated metadata should remain structurally valid"),
1068            None,
1069            "an unstamped mutation must make optional metadata unavailable",
1070        );
1071
1072        store.mark_prefix_cardinality_data_generation(7);
1073        assert_eq!(
1074            store
1075                .exact_first_component_range_cardinality(7, index_id, &bound, &bound, 3)
1076                .expect("synchronized metadata should be structurally valid"),
1077            Some((3, 2, true)),
1078            "duplicate physical entries contribute multiplicity but one leading-component visit",
1079        );
1080        assert_eq!(
1081            store
1082                .exact_first_component_range_cardinality(7, index_id, &bound, &bound, 1)
1083                .expect("bounded metadata should be structurally valid"),
1084            Some((2, 1, false)),
1085            "stop-after bounds leading-component visits while retaining their multiplicities",
1086        );
1087        assert_eq!(
1088            store
1089                .exact_first_component_range_cardinality(8, index_id, &bound, &bound, 3)
1090                .expect("stale metadata should remain structurally valid"),
1091            None,
1092            "row-generation drift must fail closed",
1093        );
1094
1095        store.remove(&alpha_one);
1096        store.remove(&alpha_two);
1097        store.remove(&beta_one);
1098        store.mark_prefix_cardinality_data_generation(8);
1099        assert_eq!(
1100            store
1101                .exact_first_component_range_cardinality(8, index_id, &bound, &bound, 1)
1102                .expect("empty metadata should be structurally valid"),
1103            Some((0, 0, true)),
1104            "deleting every value must restore a positive exact-zero proof",
1105        );
1106    }
1107
1108    #[test]
1109    fn index_prefix_cardinality_enumerates_bounded_child_prefixes() {
1110        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1111        let collection = b"collection-a".to_vec();
1112        let other_collection = b"collection-b".to_vec();
1113        let draft = b"Draft".to_vec();
1114        let review = b"Review".to_vec();
1115        let published = b"Published".to_vec();
1116        let mut store = IndexStore::init_heap();
1117
1118        store.insert(
1119            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
1120            IndexEntryValue::presence(),
1121        );
1122        store.insert(
1123            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 2),
1124            IndexEntryValue::presence(),
1125        );
1126        store.insert(
1127            indexed_raw_key(&index_id, vec![collection.clone(), review.clone()], 3),
1128            IndexEntryValue::presence(),
1129        );
1130        store.insert(
1131            indexed_raw_key(
1132                &index_id,
1133                vec![other_collection.clone(), published.clone()],
1134                4,
1135            ),
1136            IndexEntryValue::presence(),
1137        );
1138        store.mark_prefix_cardinality_data_generation(7);
1139
1140        assert_eq!(
1141            store.exact_child_prefixes_for_parent_set(
1142                7,
1143                IndexKeyKind::User,
1144                index_id,
1145                [std::slice::from_ref(&collection)],
1146                4,
1147            ),
1148            Some(vec![
1149                vec![collection.clone(), draft],
1150                vec![collection.clone(), review],
1151            ]),
1152            "child-prefix enumeration should return deterministic unique children under the requested parent",
1153        );
1154        assert_eq!(
1155            store.exact_child_prefixes_for_parent_set(
1156                7,
1157                IndexKeyKind::User,
1158                index_id,
1159                [std::slice::from_ref(&other_collection)],
1160                4,
1161            ),
1162            Some(vec![vec![other_collection, published]]),
1163            "child-prefix enumeration must stay scoped to the requested parent prefix",
1164        );
1165        assert_eq!(
1166            store.exact_child_prefixes_for_parent_set(
1167                8,
1168                IndexKeyKind::User,
1169                index_id,
1170                [std::slice::from_ref(&collection)],
1171                4,
1172            ),
1173            None,
1174            "row generation drift should keep child-prefix expansion fail-closed",
1175        );
1176        assert_eq!(
1177            store.exact_child_prefixes_for_parent_set(
1178                7,
1179                IndexKeyKind::User,
1180                index_id,
1181                [std::slice::from_ref(&collection)],
1182                1,
1183            ),
1184            None,
1185            "over-cap child-prefix expansion should fall back to the existing route",
1186        );
1187    }
1188
1189    #[test]
1190    fn index_prefix_cardinality_batches_sparse_child_prefixes() {
1191        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1192        let collection = b"collection-a".to_vec();
1193        let other_collection = b"collection-b".to_vec();
1194        let missing_a = b"missing-a".to_vec();
1195        let missing_b = b"missing-b".to_vec();
1196        let draft = b"Draft".to_vec();
1197        let review = b"Review".to_vec();
1198        let published = b"Published".to_vec();
1199        let mut store = IndexStore::init_heap();
1200
1201        store.insert(
1202            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
1203            IndexEntryValue::presence(),
1204        );
1205        store.insert(
1206            indexed_raw_key(&index_id, vec![collection.clone(), review.clone()], 2),
1207            IndexEntryValue::presence(),
1208        );
1209        store.insert(
1210            indexed_raw_key(
1211                &index_id,
1212                vec![other_collection.clone(), published.clone()],
1213                3,
1214            ),
1215            IndexEntryValue::presence(),
1216        );
1217        store.mark_prefix_cardinality_data_generation(7);
1218
1219        let parents = [
1220            std::slice::from_ref(&missing_a),
1221            std::slice::from_ref(&collection),
1222            std::slice::from_ref(&missing_b),
1223            std::slice::from_ref(&other_collection),
1224            std::slice::from_ref(&collection),
1225        ];
1226        assert_eq!(
1227            store.exact_child_prefixes_for_parent_set(7, IndexKeyKind::User, index_id, parents, 3,),
1228            Some(vec![
1229                vec![collection.clone(), draft],
1230                vec![collection.clone(), review],
1231                vec![other_collection.clone(), published],
1232            ]),
1233            "batched child-prefix enumeration should skip missing sparse parents and return deterministic real children",
1234        );
1235        assert_eq!(
1236            store.exact_child_prefixes_for_parent_set(
1237                7,
1238                IndexKeyKind::User,
1239                index_id,
1240                [
1241                    std::slice::from_ref(&missing_a),
1242                    std::slice::from_ref(&missing_b)
1243                ],
1244                4,
1245            ),
1246            Some(Vec::new()),
1247            "missing-only sparse parent sets should be proven empty when cardinality is synchronized",
1248        );
1249        assert_eq!(
1250            store.exact_child_prefixes_for_parent_set(
1251                7,
1252                IndexKeyKind::User,
1253                index_id,
1254                [
1255                    std::slice::from_ref(&collection),
1256                    std::slice::from_ref(&other_collection)
1257                ],
1258                2,
1259            ),
1260            None,
1261            "over-cap sparse parent-set expansion should fail closed",
1262        );
1263        assert_eq!(
1264            store.exact_child_prefixes_for_parent_set(
1265                8,
1266                IndexKeyKind::User,
1267                index_id,
1268                [std::slice::from_ref(&collection)],
1269                4,
1270            ),
1271            None,
1272            "generation drift should keep batched child-prefix expansion fail-closed",
1273        );
1274    }
1275
1276    #[test]
1277    fn index_prefix_cardinality_ignores_system_index_mutations() {
1278        let user_index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1279        let system_index_id = IndexId::new(EntityTag::new(0xCA7D), 2);
1280        let collection = b"collection-a".to_vec();
1281        let draft = b"Draft".to_vec();
1282        let system_component = b"reverse-edge".to_vec();
1283        let mut store = IndexStore::init_heap();
1284
1285        store.insert(
1286            indexed_raw_key(&user_index_id, vec![collection.clone(), draft.clone()], 1),
1287            IndexEntryValue::presence(),
1288        );
1289        store.mark_prefix_cardinality_data_generation(7);
1290
1291        assert_eq!(
1292            store.exact_prefix_cardinality(
1293                7,
1294                IndexKeyKind::User,
1295                user_index_id,
1296                &[collection.clone(), draft.clone()],
1297            ),
1298            Some(1),
1299        );
1300
1301        let system_key = indexed_raw_key_with_kind(
1302            &system_index_id,
1303            IndexKeyKind::System,
1304            vec![system_component],
1305            1,
1306        );
1307        store.insert(system_key.clone(), IndexEntryValue::presence());
1308        assert_eq!(
1309            store.exact_prefix_cardinality(
1310                7,
1311                IndexKeyKind::User,
1312                user_index_id,
1313                &[collection.clone(), draft.clone()],
1314            ),
1315            Some(1),
1316            "system index writes must not invalidate synchronized user-prefix cardinality",
1317        );
1318
1319        store.remove(&system_key);
1320        assert_eq!(
1321            store.exact_prefix_cardinality(
1322                7,
1323                IndexKeyKind::User,
1324                user_index_id,
1325                &[collection.clone(), draft.clone()],
1326            ),
1327            Some(1),
1328            "system index removals must not invalidate synchronized user-prefix cardinality",
1329        );
1330
1331        let malformed_system_key = indexed_raw_key_with_kind(
1332            &system_index_id,
1333            IndexKeyKind::System,
1334            vec![b"malformed-reverse-edge".to_vec()],
1335            2,
1336        );
1337        store.insert(malformed_system_key.clone(), malformed_index_entry_value());
1338        assert_eq!(
1339            store.exact_prefix_cardinality(
1340                7,
1341                IndexKeyKind::User,
1342                user_index_id,
1343                &[collection.clone(), draft.clone()],
1344            ),
1345            Some(1),
1346            "malformed system index payloads must not invalidate user-prefix cardinality",
1347        );
1348
1349        store.remove(&malformed_system_key);
1350        assert_eq!(
1351            store.exact_prefix_cardinality(
1352                7,
1353                IndexKeyKind::User,
1354                user_index_id,
1355                &[collection.clone(), draft],
1356            ),
1357            Some(1),
1358            "malformed system index removals must not invalidate user-prefix cardinality",
1359        );
1360
1361        let review = b"Review".to_vec();
1362        store.insert(
1363            indexed_raw_key(&user_index_id, vec![collection.clone(), review.clone()], 2),
1364            IndexEntryValue::presence(),
1365        );
1366        assert_eq!(
1367            store.exact_prefix_cardinality(
1368                7,
1369                IndexKeyKind::User,
1370                user_index_id,
1371                &[collection, review]
1372            ),
1373            None,
1374            "user-prefix count changes must still require a fresh row-generation stamp",
1375        );
1376    }
1377
1378    #[test]
1379    fn index_prefix_cardinality_ignores_missing_user_index_mutations() {
1380        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1381        let collection = b"collection-a".to_vec();
1382        let draft = b"Draft".to_vec();
1383        let mut store = IndexStore::init_heap();
1384
1385        store.insert(
1386            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
1387            IndexEntryValue::presence(),
1388        );
1389        store.mark_prefix_cardinality_data_generation(7);
1390
1391        let stale_key = indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 2);
1392        store.insert(stale_key.clone(), missing_index_entry_value());
1393        assert_eq!(
1394            store.exact_prefix_cardinality(
1395                7,
1396                IndexKeyKind::User,
1397                index_id,
1398                &[collection.clone(), draft.clone()],
1399            ),
1400            Some(1),
1401            "missing user index entries must not affect synchronized prefix cardinality",
1402        );
1403
1404        store.remove(&stale_key);
1405        assert_eq!(
1406            store.exact_prefix_cardinality(7, IndexKeyKind::User, index_id, &[collection, draft],),
1407            Some(1),
1408            "missing user index removals must not affect synchronized prefix cardinality",
1409        );
1410    }
1411
1412    #[test]
1413    fn journaled_mixed_index_range_traversal_preserves_order_and_early_stop() {
1414        let mut store = IndexStore::init_journaled(test_memory(93));
1415        for value in [1_u8, 3, 5] {
1416            store
1417                .apply_canonical_entry(raw_key(value), Some(IndexEntryValue::presence()))
1418                .expect("canonical index seed should apply");
1419        }
1420
1421        store.insert(raw_key(0), IndexEntryValue::presence());
1422        store.insert(raw_key(4), IndexEntryValue::presence());
1423        store.insert(raw_key(5), IndexEntryValue::presence());
1424        store.remove(&raw_key(1));
1425
1426        let lower = Bound::Included(raw_key(0));
1427        let upper = Bound::Included(raw_key(5));
1428
1429        let mut asc = Vec::new();
1430        store
1431            .visit_journaled_entries_in_range((&lower, &upper), Direction::Asc, |key, _value| {
1432                asc.push(key.as_bytes()[0]);
1433                Ok::<_, Infallible>(asc.len() == 2)
1434            })
1435            .expect("asc journaled index range traversal should succeed");
1436        assert_eq!(asc, vec![0, 3]);
1437
1438        let mut desc = Vec::new();
1439        store
1440            .visit_journaled_entries_in_range((&lower, &upper), Direction::Desc, |key, _value| {
1441                desc.push(key.as_bytes()[0]);
1442                Ok::<_, Infallible>(desc.len() == 2)
1443            })
1444            .expect("desc journaled index range traversal should succeed");
1445        assert_eq!(desc, vec![5, 4]);
1446    }
1447
1448    #[test]
1449    fn journaled_index_store_reopens_without_materializing_prefix_cardinality() {
1450        let memory = test_memory(94);
1451        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1452        let collection = b"collection-a".to_vec();
1453        let mut store = IndexStore::init_journaled(memory.clone());
1454        let key = indexed_raw_key(&index_id, vec![collection.clone()], 1);
1455        store
1456            .apply_canonical_entry(key.clone(), Some(IndexEntryValue::presence()))
1457            .expect("canonical index seed should apply");
1458        drop(store);
1459
1460        let mut reopened = IndexStore::init_journaled(memory);
1461        let bound = Bound::Unbounded;
1462
1463        assert_eq!(reopened.get(&key), Some(IndexEntryValue::presence()));
1464        assert_eq!(
1465            reopened.exact_prefix_cardinality(
1466                0,
1467                IndexKeyKind::User,
1468                index_id,
1469                std::slice::from_ref(&collection),
1470            ),
1471            None,
1472            "startup must leave optional prefix cardinality unavailable without scanning stable entries",
1473        );
1474        assert_eq!(
1475            reopened
1476                .exact_first_component_range_cardinality(0, index_id, &bound, &bound, 2)
1477                .expect("unmaterialized metadata should remain structurally valid"),
1478            None,
1479            "startup must not treat an absent materialized leading-component map as exact evidence",
1480        );
1481        assert_eq!(
1482            reopened.exact_prefix_cardinality_delta(
1483                IndexKeyKind::User,
1484                index_id,
1485                std::slice::from_ref(&collection),
1486            ),
1487            Some(0),
1488        );
1489        let second = indexed_raw_key(&index_id, vec![collection.clone()], 2);
1490        reopened.insert(second.clone(), IndexEntryValue::presence());
1491        assert_eq!(
1492            reopened.exact_prefix_cardinality_delta(
1493                IndexKeyKind::User,
1494                index_id,
1495                std::slice::from_ref(&collection),
1496            ),
1497            Some(1),
1498        );
1499        reopened
1500            .apply_canonical_entry(second, Some(IndexEntryValue::presence()))
1501            .expect("matching canonical index entry should fold");
1502        assert_eq!(
1503            reopened.exact_prefix_cardinality_delta(
1504                IndexKeyKind::User,
1505                index_id,
1506                std::slice::from_ref(&collection),
1507            ),
1508            Some(0),
1509            "canonical fold must consume only its exact overlay prefix contribution",
1510        );
1511    }
1512
1513    #[test]
1514    fn empty_journaled_index_store_retains_exact_prefix_cardinality_without_scanning() {
1515        let memory = test_memory(95);
1516        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1517        let collection = b"collection-a".to_vec();
1518        let mut store = IndexStore::init_journaled(memory.clone());
1519
1520        assert_eq!(
1521            store.exact_prefix_cardinality(
1522                0,
1523                IndexKeyKind::User,
1524                index_id,
1525                std::slice::from_ref(&collection),
1526            ),
1527            Some(0),
1528        );
1529        store
1530            .reset_journaled_live_projection(7, FoldWatermark::initial())
1531            .expect("empty projection reset should succeed");
1532        assert_eq!(
1533            store.exact_prefix_cardinality(
1534                7,
1535                IndexKeyKind::User,
1536                index_id,
1537                std::slice::from_ref(&collection),
1538            ),
1539            Some(0),
1540        );
1541        drop(store);
1542
1543        let reopened = IndexStore::init_journaled(memory);
1544        assert_eq!(
1545            reopened.exact_prefix_cardinality(
1546                0,
1547                IndexKeyKind::User,
1548                index_id,
1549                std::slice::from_ref(&collection),
1550            ),
1551            Some(0),
1552        );
1553    }
1554
1555    #[test]
1556    fn journaled_prefix_delta_binds_and_advances_its_canonical_watermark() {
1557        let mut store = IndexStore::init_journaled(test_memory(99));
1558        assert_eq!(store.exact_prefix_cardinality_delta_watermark(), None);
1559
1560        let current = FoldWatermark::new(JournalSequence::new(7), 3);
1561        let next = FoldWatermark::new(JournalSequence::new(8), 4);
1562        store
1563            .reset_journaled_live_projection(0, current)
1564            .expect("delta reset should bind the current canonical watermark");
1565        assert_eq!(
1566            store.exact_prefix_cardinality_delta_watermark(),
1567            Some(current)
1568        );
1569        store
1570            .preflight_prefix_cardinality_delta_watermark(current)
1571            .expect("matching watermark should preflight");
1572        assert!(
1573            store
1574                .preflight_prefix_cardinality_delta_watermark(next)
1575                .is_err(),
1576            "a stale or future base must fail closed",
1577        );
1578
1579        store.apply_prefix_cardinality_delta_watermark(current, next);
1580        assert_eq!(store.exact_prefix_cardinality_delta_watermark(), Some(next));
1581    }
1582
1583    #[test]
1584    fn recovered_index_fold_maintains_available_prefix_cardinality() {
1585        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1586        let collection = b"collection-a".to_vec();
1587        let key = indexed_raw_key(&index_id, vec![collection.clone()], 1);
1588        let mut store = IndexStore::init_journaled(test_memory(96));
1589
1590        store
1591            .apply_canonical_entry(key.clone(), Some(IndexEntryValue::presence()))
1592            .expect("recovered index put should fold");
1593        store.mark_prefix_cardinality_data_generation(1);
1594        assert_eq!(
1595            store.exact_prefix_cardinality(
1596                1,
1597                IndexKeyKind::User,
1598                index_id,
1599                std::slice::from_ref(&collection),
1600            ),
1601            Some(1),
1602        );
1603
1604        store
1605            .apply_canonical_entry(key, None)
1606            .expect("recovered index delete should fold");
1607        store.mark_prefix_cardinality_data_generation(2);
1608        assert_eq!(
1609            store.exact_prefix_cardinality(
1610                2,
1611                IndexKeyKind::User,
1612                index_id,
1613                std::slice::from_ref(&collection),
1614            ),
1615            Some(0),
1616        );
1617    }
1618
1619    #[test]
1620    fn positioned_index_overlay_preserves_later_membership_until_exact_retirement() {
1621        let key = raw_key(7);
1622        let mut store = IndexStore::init_journaled(test_memory(97));
1623        store
1624            .apply_canonical_entry(key.clone(), Some(IndexEntryValue::presence()))
1625            .expect("canonical membership should seed");
1626
1627        store
1628            .publish_positioned_journal_entry(key.clone(), None, overlay_position(1))
1629            .expect("positioned tombstone should publish");
1630        store
1631            .publish_positioned_journal_entry(
1632                key.clone(),
1633                Some(IndexEntryValue::presence()),
1634                overlay_position(2),
1635            )
1636            .expect("later membership should supersede the tombstone");
1637        let prepared = store
1638            .prepare_position_retirement([key.clone()], overlay_position(1))
1639            .expect("older retirement should preserve later membership");
1640        assert_eq!(
1641            prepared.entries,
1642            [(key.clone(), PositionedOverlayRetirement::Superseded)]
1643        );
1644        store
1645            .apply_canonical_entry(key.clone(), None)
1646            .expect("tombstone batch should become canonical");
1647        store.apply_prepared_position_retirement(prepared);
1648        assert_eq!(store.get(&key), Some(IndexEntryValue::presence()));
1649
1650        let prepared = store
1651            .prepare_position_retirement([key.clone()], overlay_position(2))
1652            .expect("latest retirement should be exact");
1653        assert_eq!(
1654            prepared.entries,
1655            [(key.clone(), PositionedOverlayRetirement::Exact)]
1656        );
1657        store
1658            .apply_canonical_entry(key.clone(), Some(IndexEntryValue::presence()))
1659            .expect("membership batch should become canonical");
1660        store.apply_prepared_position_retirement(prepared);
1661        assert_eq!(store.get(&key), Some(IndexEntryValue::presence()));
1662
1663        let mut visible = Vec::new();
1664        store
1665            .visit_entries(|visited_key, value| {
1666                visible.push((visited_key.clone(), value.clone()));
1667                Ok::<_, Infallible>(IndexStoreVisit::Continue)
1668            })
1669            .expect("positioned index should remain range-visible");
1670        assert_eq!(visible, vec![(key, IndexEntryValue::presence())]);
1671    }
1672
1673    #[test]
1674    fn positioned_index_overlay_coalesces_repeated_same_batch_target() {
1675        let key = raw_key(8);
1676        let position = overlay_position(3);
1677        let mut store = IndexStore::init_journaled(test_memory(98));
1678
1679        store
1680            .publish_positioned_journal_entry(
1681                key.clone(),
1682                Some(IndexEntryValue::presence()),
1683                position,
1684            )
1685            .expect("first same-batch effect should publish");
1686        store
1687            .publish_positioned_journal_entry(key.clone(), None, position)
1688            .expect("final same-batch effect should coalesce by logical target");
1689        let prepared = store
1690            .prepare_position_retirement([key.clone()], position)
1691            .expect("coalesced target should retire once");
1692        assert_eq!(
1693            prepared.entries,
1694            [(key.clone(), PositionedOverlayRetirement::Exact)]
1695        );
1696        store
1697            .apply_canonical_entry(key.clone(), None)
1698            .expect("coalesced final effect should become canonical");
1699        store.apply_prepared_position_retirement(prepared);
1700        assert!(store.get(&key).is_none());
1701    }
1702}