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