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    /// Preflight movement of the exact delta's canonical base boundary.
541    pub(in crate::db) fn preflight_prefix_cardinality_delta_watermark(
542        &self,
543        current: FoldWatermark,
544    ) -> Result<(), crate::error::InternalError> {
545        (self.exact_prefix_cardinality_delta_watermark() == Some(current))
546            .then_some(())
547            .ok_or_else(crate::error::InternalError::store_corruption)
548    }
549
550    /// Publish a preflighted canonical base movement after the complete fold.
551    pub(in crate::db) fn apply_prefix_cardinality_delta_watermark(
552        &mut self,
553        current: FoldWatermark,
554        next: FoldWatermark,
555    ) {
556        if let IndexStoreBackend::Journaled {
557            prefix_cardinality_delta,
558            ..
559        } = &mut self.backend
560        {
561            prefix_cardinality_delta.advance_watermark(current, next);
562        }
563    }
564
565    /// Publish one preflighted positioned derived or explicit index effect.
566    pub(in crate::db) fn publish_preflighted_journal_entry(
567        &mut self,
568        key: RawIndexStoreKey,
569        value: Option<IndexEntryValue>,
570        position: JournalOverlayPosition,
571    ) -> Result<Option<IndexEntryValue>, crate::error::InternalError> {
572        let IndexStoreBackend::Journaled {
573            canonical,
574            live,
575            tombstones,
576            positions,
577            prefix_cardinality_delta,
578        } = &mut self.backend
579        else {
580            return Err(crate::error::InternalError::store_invariant());
581        };
582        let previous = if tombstones.contains(&key) {
583            None
584        } else {
585            live.get(&key).cloned().or_else(|| canonical.get(&key))
586        };
587        let next_value = value.clone();
588
589        if let Some(value) = value {
590            tombstones.remove(&key);
591            live.insert(key.clone(), value.clone());
592            self.prefix_cardinality
593                .apply_insert(&key, previous.as_ref(), &value);
594        } else {
595            live.remove(&key);
596            tombstones.insert(key.clone());
597            self.prefix_cardinality
598                .apply_remove(&key, previous.as_ref());
599        }
600        prefix_cardinality_delta.apply_transition(&key, previous.as_ref(), next_value.as_ref());
601        positions.publish_preflighted(key, position);
602        self.bump_generation();
603
604        Ok(previous)
605    }
606
607    /// Validate and publish one positioned index effect for direct store tests.
608    #[cfg(test)]
609    pub(in crate::db) fn publish_positioned_journal_entry(
610        &mut self,
611        key: RawIndexStoreKey,
612        value: Option<IndexEntryValue>,
613        position: JournalOverlayPosition,
614    ) -> Result<Option<IndexEntryValue>, crate::error::InternalError> {
615        self.preflight_positioned_journal_entry(&key, position)?;
616        self.publish_preflighted_journal_entry(key, value, position)
617    }
618
619    /// Preflight index provenance before marker publication.
620    pub(in crate::db) fn preflight_positioned_journal_entry(
621        &self,
622        key: &RawIndexStoreKey,
623        position: JournalOverlayPosition,
624    ) -> Result<(), crate::error::InternalError> {
625        let IndexStoreBackend::Journaled { positions, .. } = &self.backend else {
626            return Err(crate::error::InternalError::store_invariant());
627        };
628        positions.preflight_publish(key, position)
629    }
630
631    /// Preflight explicit index provenance before marker publication.
632    pub(in crate::db) fn prepare_position_publication(
633        &self,
634        keys: impl IntoIterator<Item = RawIndexStoreKey>,
635        position: JournalOverlayPosition,
636    ) -> Result<PreparedIndexPositionPublication, crate::error::InternalError> {
637        let IndexStoreBackend::Journaled { positions, .. } = &self.backend else {
638            return Err(crate::error::InternalError::store_invariant());
639        };
640        let keys = keys.into_iter().collect::<BTreeSet<_>>();
641        for key in &keys {
642            positions.preflight_publish(key, position)?;
643        }
644        Ok(PreparedIndexPositionPublication {
645            keys: keys.into_iter().collect(),
646            position,
647        })
648    }
649
650    /// Publish explicit index provenance after its values have been applied.
651    pub(in crate::db) fn publish_prepared_positions(
652        &mut self,
653        prepared: PreparedIndexPositionPublication,
654    ) {
655        let IndexStoreBackend::Journaled { positions, .. } = &mut self.backend else {
656            debug_assert!(
657                false,
658                "preflighted index positions require a journaled store"
659            );
660            return;
661        };
662        for key in prepared.keys {
663            positions.publish_preflighted(key, prepared.position);
664        }
665    }
666
667    /// Preflight exact index-overlay retirement before canonical mutation.
668    pub(in crate::db) fn prepare_position_retirement(
669        &self,
670        keys: impl IntoIterator<Item = RawIndexStoreKey>,
671        position: JournalOverlayPosition,
672    ) -> Result<PreparedIndexPositionRetirement, crate::error::InternalError> {
673        let IndexStoreBackend::Journaled { positions, .. } = &self.backend else {
674            return Err(crate::error::InternalError::store_invariant());
675        };
676        let entries = keys
677            .into_iter()
678            .collect::<BTreeSet<_>>()
679            .into_iter()
680            .map(|key| {
681                positions
682                    .preflight_retirement(&key, position)
683                    .map(|retirement| (key, retirement))
684            })
685            .collect::<Result<Vec<_>, _>>()?;
686        Ok(PreparedIndexPositionRetirement { entries })
687    }
688
689    /// Retire only exact index overlays after canonical mutation succeeds.
690    pub(in crate::db) fn apply_prepared_position_retirement(
691        &mut self,
692        prepared: PreparedIndexPositionRetirement,
693    ) {
694        let IndexStoreBackend::Journaled {
695            live,
696            tombstones,
697            positions,
698            ..
699        } = &mut self.backend
700        else {
701            debug_assert!(
702                false,
703                "preflighted index retirement requires a journaled store"
704            );
705            return;
706        };
707        for (key, retirement) in prepared.entries {
708            if retirement == PositionedOverlayRetirement::Exact {
709                live.remove(&key);
710                tombstones.remove(&key);
711                positions.retire_preflighted(&key, retirement);
712            }
713        }
714    }
715
716    /// Apply one exact index entry directly to canonical stable storage.
717    ///
718    /// Journal folding and planner-invisible migration staging share this
719    /// primitive. Callers own key authority, journal watermarks and position
720    /// retirement; unrelated live entries and their prefix deltas stay intact.
721    pub(in crate::db) fn apply_canonical_entry(
722        &mut self,
723        key: RawIndexStoreKey,
724        value: Option<IndexEntryValue>,
725    ) -> Result<(), crate::error::InternalError> {
726        let IndexStoreBackend::Journaled {
727            canonical,
728            live,
729            tombstones,
730            ..
731        } = &mut self.backend
732        else {
733            return Err(crate::error::InternalError::store_invariant());
734        };
735
736        let visible = !live.contains_key(&key) && !tombstones.contains(&key);
737        let (previous, key) = if let Some(value) = value.as_ref() {
738            // The published stable insertion API consumes its key before
739            // returning the previous entry; cardinality still needs this copy.
740            let cardinality_key = key.clone();
741            (canonical.insert(key, value.clone()), cardinality_key)
742        } else {
743            (canonical.remove(&key), key)
744        };
745        if visible {
746            if let Some(value) = value.as_ref() {
747                self.prefix_cardinality
748                    .apply_insert(&key, previous.as_ref(), value);
749            } else {
750                self.prefix_cardinality
751                    .apply_remove(&key, previous.as_ref());
752            }
753        } else {
754            self.apply_prefix_overlay_delta(&key, value.as_ref(), previous.as_ref());
755        }
756        self.bump_generation();
757
758        Ok(())
759    }
760
761    /// Prove that recovered journal entries can be folded into canonical storage.
762    pub(in crate::db) fn preflight_fold_recovered_journal(
763        &self,
764    ) -> Result<(), crate::error::InternalError> {
765        match self.backend {
766            IndexStoreBackend::Journaled { .. } => Ok(()),
767            IndexStoreBackend::Heap(_) => Err(crate::error::InternalError::store_invariant()),
768        }
769    }
770
771    /// Sum of bytes used by all stored index entries.
772    pub fn memory_bytes(&self) -> u64 {
773        let mut bytes = 0u64;
774        let _: Result<(), std::convert::Infallible> = self.visit_entries(|key, value| {
775            bytes = bytes.saturating_add(key.as_bytes().len() as u64 + value.len() as u64);
776            Ok(IndexStoreVisit::Continue)
777        });
778        bytes
779    }
780
781    /// Return the monotonic perf-only count of index entries yielded by traversal.
782    #[cfg(test)]
783    pub(in crate::db) fn current_entry_read_count() -> u64 {
784        INDEX_STORE_ENTRY_READ_COUNT.with(Cell::get)
785    }
786
787    const fn bump_generation(&mut self) {
788        self.generation = self.generation.saturating_add(1);
789    }
790
791    fn apply_prefix_overlay_delta(
792        &mut self,
793        key: &RawIndexStoreKey,
794        previous: Option<&IndexEntryValue>,
795        next: Option<&IndexEntryValue>,
796    ) {
797        let IndexStoreBackend::Journaled {
798            prefix_cardinality_delta,
799            ..
800        } = &mut self.backend
801        else {
802            return;
803        };
804        prefix_cardinality_delta.apply_transition(key, previous, next);
805    }
806
807    fn journaled_get(
808        backend: &IndexStoreBackend,
809        key: &RawIndexStoreKey,
810    ) -> Option<IndexEntryValue> {
811        let IndexStoreBackend::Journaled {
812            canonical,
813            live,
814            tombstones,
815            ..
816        } = backend
817        else {
818            return None;
819        };
820
821        if tombstones.contains(key) {
822            return None;
823        }
824        live.get(key).cloned().or_else(|| canonical.get(key))
825    }
826
827    // Share stable iteration and overlay merging across visitor closures. Error
828    // types remain generic so store callers retain their original typed failures.
829    pub(super) fn visit_journaled_entries_in_range<E>(
830        &self,
831        bounds: (Bound<&RawIndexStoreKey>, Bound<&RawIndexStoreKey>),
832        direction: Direction,
833        mut visit: &mut dyn FnMut(&RawIndexStoreKey, &IndexEntryValue) -> Result<bool, E>,
834    ) -> Result<(), E> {
835        let IndexStoreBackend::Journaled {
836            canonical,
837            live,
838            tombstones,
839            ..
840        } = &self.backend
841        else {
842            return Ok(());
843        };
844
845        let (lower, upper) = bounds;
846        match direction {
847            Direction::Asc if canonical.is_empty() => {
848                for (key, value) in live.range((lower, upper)) {
849                    if visit_index_store_entry(key, value, &mut visit)? {
850                        return Ok(());
851                    }
852                }
853            }
854            Direction::Desc if canonical.is_empty() => {
855                for (key, value) in live.range((lower, upper)).rev() {
856                    if visit_index_store_entry(key, value, &mut visit)? {
857                        return Ok(());
858                    }
859                }
860            }
861            Direction::Asc if live.is_empty() && tombstones.is_empty() => {
862                for entry in canonical.range((lower, upper)) {
863                    if visit_index_store_entry(entry.key(), &entry.value(), &mut visit)? {
864                        return Ok(());
865                    }
866                }
867            }
868            Direction::Desc if live.is_empty() && tombstones.is_empty() => {
869                for entry in canonical.range((lower, upper)).rev() {
870                    if visit_index_store_entry(entry.key(), &entry.value(), &mut visit)? {
871                        return Ok(());
872                    }
873                }
874            }
875            Direction::Asc => {
876                for entry in ordered_overlay_entries(
877                    canonical.range((lower, upper)),
878                    live.range((lower, upper)),
879                    direction,
880                    |entry| entry.key(),
881                    |entry| entry.0,
882                    tombstones,
883                ) {
884                    let should_stop = match entry {
885                        OrderedOverlayEntry::Canonical(canonical_entry) => visit_index_store_entry(
886                            canonical_entry.key(),
887                            &canonical_entry.value(),
888                            &mut visit,
889                        )?,
890                        OrderedOverlayEntry::Live((key, value)) => {
891                            visit_index_store_entry(key, value, &mut visit)?
892                        }
893                    };
894                    if should_stop {
895                        return Ok(());
896                    }
897                }
898            }
899            Direction::Desc => {
900                for entry in ordered_overlay_entries(
901                    canonical.range((lower, upper)).rev(),
902                    live.range((lower, upper)).rev(),
903                    direction,
904                    |entry| entry.key(),
905                    |entry| entry.0,
906                    tombstones,
907                ) {
908                    let should_stop = match entry {
909                        OrderedOverlayEntry::Canonical(canonical_entry) => visit_index_store_entry(
910                            canonical_entry.key(),
911                            &canonical_entry.value(),
912                            &mut visit,
913                        )?,
914                        OrderedOverlayEntry::Live((key, value)) => {
915                            visit_index_store_entry(key, value, &mut visit)?
916                        }
917                    };
918                    if should_stop {
919                        return Ok(());
920                    }
921                }
922            }
923        }
924
925        Ok(())
926    }
927}
928
929#[cfg(test)]
930mod tests {
931    use super::*;
932    use crate::{
933        db::{
934            direction::Direction,
935            index::{IndexId, IndexKey, IndexKeyKind},
936            journal::JournalSequence,
937            key_taxonomy::{PrimaryKeyComponent, PrimaryKeyValue},
938            positioned_overlay::{JournalOverlayPosition, PositionedOverlayRetirement},
939            registry::StoreAllocationIdentity,
940        },
941        testing::test_memory,
942        types::EntityTag,
943    };
944    use ic_storage::adapter::Storable;
945    use std::{borrow::Cow, convert::Infallible};
946
947    #[test]
948    fn retained_store_corruption_does_not_initialize_a_replacement() {
949        crate::testing::assert_retained_store_header_preserved(IndexStore::init_journaled);
950    }
951
952    fn raw_key(value: u8) -> RawIndexStoreKey {
953        <RawIndexStoreKey as Storable>::from_bytes(Cow::Owned(vec![value]))
954    }
955
956    fn overlay_position(sequence: u64) -> JournalOverlayPosition {
957        JournalOverlayPosition::new(
958            StoreAllocationIdentity::new(231, "test::index"),
959            JournalSequence::new(sequence),
960        )
961    }
962
963    fn indexed_raw_key(
964        index_id: &IndexId,
965        components: Vec<Vec<u8>>,
966        primary_key: u64,
967    ) -> RawIndexStoreKey {
968        indexed_raw_key_with_kind(index_id, IndexKeyKind::User, components, primary_key)
969    }
970
971    fn indexed_raw_key_with_kind(
972        index_id: &IndexId,
973        key_kind: IndexKeyKind,
974        components: Vec<Vec<u8>>,
975        primary_key: u64,
976    ) -> RawIndexStoreKey {
977        IndexKey::new_from_components_with_primary_key_value(
978            index_id,
979            key_kind,
980            components.as_slice(),
981            &PrimaryKeyValue::from(PrimaryKeyComponent::Nat64(primary_key)),
982        )
983        .expect("test index key should build")
984        .to_raw()
985        .expect("test index key should encode")
986    }
987
988    fn malformed_index_entry_value() -> IndexEntryValue {
989        <IndexEntryValue as Storable>::from_bytes(Cow::Owned(vec![0xFF]))
990    }
991
992    fn missing_index_entry_value() -> IndexEntryValue {
993        <IndexEntryValue as Storable>::from_bytes(Cow::Owned(vec![1]))
994    }
995
996    #[test]
997    fn index_insert_paths_preserve_previous_values_and_prefix_counts() {
998        let id = IndexId::new(EntityTag::new(42), 7);
999        let components = vec![b"collection".to_vec(), b"draft".to_vec()];
1000        let key = indexed_raw_key(&id, components.clone(), 1);
1001        for mut store in [
1002            IndexStore::init_heap(),
1003            IndexStore::init_journaled(test_memory(94)),
1004        ] {
1005            let journaled = matches!(store.backend, IndexStoreBackend::Journaled { .. });
1006            let mut previous = None;
1007            for next in [
1008                Some(IndexEntryValue::presence()),
1009                Some(missing_index_entry_value()),
1010                Some(IndexEntryValue::presence()),
1011                Some(IndexEntryValue::presence()),
1012                None,
1013                None,
1014            ] {
1015                let returned = if let Some(value) = next.as_ref() {
1016                    store.insert(key.clone(), value.clone())
1017                } else {
1018                    store.remove(&key)
1019                };
1020                assert_eq!(returned, previous);
1021                assert_eq!(store.get(&key), next);
1022                let count = u64::from(next == Some(IndexEntryValue::presence()));
1023                store.mark_prefix_cardinality_data_generation(7);
1024                assert_eq!(
1025                    store.exact_prefix_cardinality(7, IndexKeyKind::User, id, &components),
1026                    Some(count)
1027                );
1028                assert_eq!(
1029                    store.exact_prefix_cardinality_delta(IndexKeyKind::User, id, &components),
1030                    Some(if journaled {
1031                        i64::try_from(count).unwrap()
1032                    } else {
1033                        0
1034                    })
1035                );
1036                previous = next;
1037            }
1038            store.insert_preflighted_absent(key.clone(), IndexEntryValue::presence());
1039            store.mark_prefix_cardinality_data_generation(8);
1040            assert_eq!(
1041                store.exact_prefix_cardinality(8, IndexKeyKind::User, id, &components),
1042                Some(1)
1043            );
1044            if journaled {
1045                for (sequence, next) in [
1046                    Some(missing_index_entry_value()),
1047                    Some(IndexEntryValue::presence()),
1048                    None,
1049                ]
1050                .into_iter()
1051                .enumerate()
1052                {
1053                    store
1054                        .publish_positioned_journal_entry(
1055                            key.clone(),
1056                            next.clone(),
1057                            overlay_position(sequence as u64 + 1),
1058                        )
1059                        .unwrap();
1060                    assert_eq!(store.get(&key), next);
1061                    let count = u64::from(next == Some(IndexEntryValue::presence()));
1062                    store.mark_prefix_cardinality_data_generation(9);
1063                    assert_eq!(
1064                        store.exact_prefix_cardinality(9, IndexKeyKind::User, id, &components),
1065                        Some(count)
1066                    );
1067                    assert_eq!(
1068                        store.exact_prefix_cardinality_delta(IndexKeyKind::User, id, &components),
1069                        Some(i64::try_from(count).unwrap())
1070                    );
1071                }
1072            }
1073        }
1074    }
1075
1076    #[test]
1077    fn index_prefix_cardinality_requires_explicit_data_generation_sync() {
1078        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1079        let collection = b"collection-a".to_vec();
1080        let draft = b"Draft".to_vec();
1081        let review = b"Review".to_vec();
1082        let mut store = IndexStore::init_heap();
1083
1084        store.insert(
1085            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
1086            IndexEntryValue::presence(),
1087        );
1088        store.insert(
1089            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 2),
1090            IndexEntryValue::presence(),
1091        );
1092        store.insert(
1093            indexed_raw_key(&index_id, vec![collection.clone(), review.clone()], 3),
1094            IndexEntryValue::presence(),
1095        );
1096
1097        assert_eq!(
1098            store.exact_prefix_cardinality(
1099                0,
1100                IndexKeyKind::User,
1101                index_id,
1102                std::slice::from_ref(&collection),
1103            ),
1104            None,
1105            "raw index mutations must not be trusted until row generation sync is stamped",
1106        );
1107
1108        store.mark_prefix_cardinality_data_generation(7);
1109
1110        assert_eq!(
1111            store.exact_prefix_cardinality(
1112                7,
1113                IndexKeyKind::User,
1114                index_id,
1115                std::slice::from_ref(&collection),
1116            ),
1117            Some(3),
1118        );
1119        assert_eq!(
1120            store.exact_prefix_cardinality(
1121                7,
1122                IndexKeyKind::User,
1123                index_id,
1124                &[collection.clone(), draft],
1125            ),
1126            Some(2),
1127        );
1128        assert_eq!(
1129            store.exact_prefix_cardinality(8, IndexKeyKind::User, index_id, &[collection, review],),
1130            None,
1131            "row generation drift should force the caller to use the existing-row fallback",
1132        );
1133    }
1134
1135    #[test]
1136    fn first_component_range_cardinality_is_exact_bounded_and_generation_matched() {
1137        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1138        let alpha = b"alpha".to_vec();
1139        let beta = b"beta".to_vec();
1140        let alpha_one = indexed_raw_key(&index_id, vec![alpha.clone()], 1);
1141        let alpha_two = indexed_raw_key(&index_id, vec![alpha], 2);
1142        let beta_one = indexed_raw_key(&index_id, vec![beta], 3);
1143        let mut store = IndexStore::init_heap();
1144        let bound = Bound::Unbounded;
1145
1146        assert_eq!(
1147            store
1148                .exact_first_component_range_cardinality(0, index_id, &bound, &bound, 1)
1149                .expect("initialized metadata should be structurally valid"),
1150            Some((0, 0, true)),
1151            "initialized empty metadata must positively prove exact zero",
1152        );
1153        store.insert(alpha_one.clone(), IndexEntryValue::presence());
1154        store.insert(alpha_two.clone(), IndexEntryValue::presence());
1155        store.insert(beta_one.clone(), IndexEntryValue::presence());
1156        assert_eq!(
1157            store
1158                .exact_first_component_range_cardinality(0, index_id, &bound, &bound, 3)
1159                .expect("invalidated metadata should remain structurally valid"),
1160            None,
1161            "an unstamped mutation must make optional metadata unavailable",
1162        );
1163
1164        store.mark_prefix_cardinality_data_generation(7);
1165        assert_eq!(
1166            store
1167                .exact_first_component_range_cardinality(7, index_id, &bound, &bound, 3)
1168                .expect("synchronized metadata should be structurally valid"),
1169            Some((3, 2, true)),
1170            "duplicate physical entries contribute multiplicity but one leading-component visit",
1171        );
1172        assert_eq!(
1173            store
1174                .exact_first_component_range_cardinality(7, index_id, &bound, &bound, 1)
1175                .expect("bounded metadata should be structurally valid"),
1176            Some((2, 1, false)),
1177            "stop-after bounds leading-component visits while retaining their multiplicities",
1178        );
1179        assert_eq!(
1180            store
1181                .exact_first_component_range_cardinality(8, index_id, &bound, &bound, 3)
1182                .expect("stale metadata should remain structurally valid"),
1183            None,
1184            "row-generation drift must fail closed",
1185        );
1186
1187        store.remove(&alpha_one);
1188        store.remove(&alpha_two);
1189        store.remove(&beta_one);
1190        store.mark_prefix_cardinality_data_generation(8);
1191        assert_eq!(
1192            store
1193                .exact_first_component_range_cardinality(8, index_id, &bound, &bound, 1)
1194                .expect("empty metadata should be structurally valid"),
1195            Some((0, 0, true)),
1196            "deleting every value must restore a positive exact-zero proof",
1197        );
1198    }
1199
1200    #[test]
1201    fn index_prefix_cardinality_seeks_parents_and_charges_examined_metadata() {
1202        use crate::db::resource::budget::{
1203            HardExecutionBudget, HardExecutionContext, HardExecutionFailureHeadroom,
1204            current_execution_budget_usage, with_execution_budget_for_tests,
1205        };
1206        use icydb_diagnostic_code::{
1207            DiagnosticExecutionBudgetResource, DiagnosticExecutionBudgetScope,
1208            DiagnosticExecutionLane,
1209        };
1210
1211        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1212        let parent = vec![vec![128]];
1213        let mut store = IndexStore::init_heap();
1214        for prefix in 0..=255_u8 {
1215            for child in 0..2_u8 {
1216                store.insert(
1217                    indexed_raw_key(
1218                        &index_id,
1219                        vec![vec![prefix], vec![child], vec![0]],
1220                        u64::from(prefix) * 2 + u64::from(child),
1221                    ),
1222                    IndexEntryValue::presence(),
1223                );
1224            }
1225        }
1226        store.mark_prefix_cardinality_data_generation(7);
1227
1228        for limit in [5, 4] {
1229            let budget = HardExecutionBudget::uniform_for_tests(
1230                u64::MAX,
1231                HardExecutionFailureHeadroom::new(500_000_000, 64 * 1024),
1232            )
1233            .with_limit_for_tests(
1234                DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited,
1235                limit,
1236            );
1237            let context = HardExecutionContext::new(
1238                DiagnosticExecutionBudgetScope::Execution,
1239                DiagnosticExecutionLane::TrustedRead,
1240                0,
1241            );
1242            let result = with_execution_budget_for_tests(
1243                budget,
1244                context,
1245                || {
1246                    let children = store.exact_child_prefixes_for_parent_set(
1247                        7,
1248                        IndexKeyKind::User,
1249                        index_id,
1250                        [parent.as_slice(), parent.as_slice()],
1251                        2,
1252                    )?;
1253                    assert_eq!(
1254                        children,
1255                        Some(vec![vec![vec![128], vec![0]], vec![vec![128], vec![1]]])
1256                    );
1257                    // Two children, their deeper descendants and one range boundary;
1258                    // unrelated prefixes and duplicate parents do not multiply work.
1259                    assert_eq!(
1260                        current_execution_budget_usage()?
1261                            .observed(DiagnosticExecutionBudgetResource::KeyIndexEntriesVisited),
1262                        5
1263                    );
1264                    Ok::<_, InternalError>(())
1265                },
1266                std::convert::identity,
1267            );
1268            assert_eq!(result.is_ok(), limit == 5);
1269            if let Err(error) = result {
1270                assert!(matches!(
1271                    error.diagnostic().detail(),
1272                    Some(icydb_diagnostic_code::DiagnosticDetail::RuntimeBoundary {
1273                        boundary:
1274                            icydb_diagnostic_code::RuntimeBoundaryCode::ExecutionBudgetExceeded,
1275                    })
1276                ));
1277            }
1278        }
1279    }
1280
1281    #[test]
1282    fn index_prefix_cardinality_enumerates_bounded_child_prefixes() {
1283        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1284        let collection = b"collection-a".to_vec();
1285        let other_collection = b"collection-b".to_vec();
1286        let draft = b"Draft".to_vec();
1287        let review = b"Review".to_vec();
1288        let published = b"Published".to_vec();
1289        let mut store = IndexStore::init_heap();
1290
1291        store.insert(
1292            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
1293            IndexEntryValue::presence(),
1294        );
1295        store.insert(
1296            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 2),
1297            IndexEntryValue::presence(),
1298        );
1299        store.insert(
1300            indexed_raw_key(&index_id, vec![collection.clone(), review.clone()], 3),
1301            IndexEntryValue::presence(),
1302        );
1303        store.insert(
1304            indexed_raw_key(
1305                &index_id,
1306                vec![other_collection.clone(), published.clone()],
1307                4,
1308            ),
1309            IndexEntryValue::presence(),
1310        );
1311        store.mark_prefix_cardinality_data_generation(7);
1312
1313        assert_eq!(
1314            store
1315                .exact_child_prefixes_for_parent_set(
1316                    7,
1317                    IndexKeyKind::User,
1318                    index_id,
1319                    [std::slice::from_ref(&collection)],
1320                    4,
1321                )
1322                .unwrap(),
1323            Some(vec![
1324                vec![collection.clone(), draft],
1325                vec![collection.clone(), review],
1326            ]),
1327            "child-prefix enumeration should return deterministic unique children under the requested parent",
1328        );
1329        assert_eq!(
1330            store
1331                .exact_child_prefixes_for_parent_set(
1332                    7,
1333                    IndexKeyKind::User,
1334                    index_id,
1335                    [std::slice::from_ref(&other_collection)],
1336                    4,
1337                )
1338                .unwrap(),
1339            Some(vec![vec![other_collection, published]]),
1340            "child-prefix enumeration must stay scoped to the requested parent prefix",
1341        );
1342        assert_eq!(
1343            store
1344                .exact_child_prefixes_for_parent_set(
1345                    8,
1346                    IndexKeyKind::User,
1347                    index_id,
1348                    [std::slice::from_ref(&collection)],
1349                    4,
1350                )
1351                .unwrap(),
1352            None,
1353            "row generation drift should keep child-prefix expansion fail-closed",
1354        );
1355        assert_eq!(
1356            store
1357                .exact_child_prefixes_for_parent_set(
1358                    7,
1359                    IndexKeyKind::User,
1360                    index_id,
1361                    [std::slice::from_ref(&collection)],
1362                    1,
1363                )
1364                .unwrap(),
1365            None,
1366            "over-cap child-prefix expansion should fall back to the existing route",
1367        );
1368    }
1369
1370    #[test]
1371    fn index_prefix_cardinality_batches_sparse_child_prefixes() {
1372        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1373        let collection = b"collection-a".to_vec();
1374        let other_collection = b"collection-b".to_vec();
1375        let missing_a = b"missing-a".to_vec();
1376        let missing_b = b"missing-b".to_vec();
1377        let draft = b"Draft".to_vec();
1378        let review = b"Review".to_vec();
1379        let published = b"Published".to_vec();
1380        let mut store = IndexStore::init_heap();
1381
1382        store.insert(
1383            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
1384            IndexEntryValue::presence(),
1385        );
1386        store.insert(
1387            indexed_raw_key(&index_id, vec![collection.clone(), review.clone()], 2),
1388            IndexEntryValue::presence(),
1389        );
1390        store.insert(
1391            indexed_raw_key(
1392                &index_id,
1393                vec![other_collection.clone(), published.clone()],
1394                3,
1395            ),
1396            IndexEntryValue::presence(),
1397        );
1398        store.mark_prefix_cardinality_data_generation(7);
1399
1400        let parents = [
1401            std::slice::from_ref(&missing_a),
1402            std::slice::from_ref(&collection),
1403            std::slice::from_ref(&missing_b),
1404            std::slice::from_ref(&other_collection),
1405            std::slice::from_ref(&collection),
1406        ];
1407        assert_eq!(
1408            store
1409                .exact_child_prefixes_for_parent_set(7, IndexKeyKind::User, index_id, parents, 3,)
1410                .unwrap(),
1411            Some(vec![
1412                vec![collection.clone(), draft],
1413                vec![collection.clone(), review],
1414                vec![other_collection.clone(), published],
1415            ]),
1416            "batched child-prefix enumeration should skip missing sparse parents and return deterministic real children",
1417        );
1418        assert_eq!(
1419            store
1420                .exact_child_prefixes_for_parent_set(
1421                    7,
1422                    IndexKeyKind::User,
1423                    index_id,
1424                    [
1425                        std::slice::from_ref(&missing_a),
1426                        std::slice::from_ref(&missing_b)
1427                    ],
1428                    4,
1429                )
1430                .unwrap(),
1431            Some(Vec::new()),
1432            "missing-only sparse parent sets should be proven empty when cardinality is synchronized",
1433        );
1434        assert_eq!(
1435            store
1436                .exact_child_prefixes_for_parent_set(
1437                    7,
1438                    IndexKeyKind::User,
1439                    index_id,
1440                    [
1441                        std::slice::from_ref(&collection),
1442                        std::slice::from_ref(&other_collection)
1443                    ],
1444                    2,
1445                )
1446                .unwrap(),
1447            None,
1448            "over-cap sparse parent-set expansion should fail closed",
1449        );
1450        assert_eq!(
1451            store
1452                .exact_child_prefixes_for_parent_set(
1453                    8,
1454                    IndexKeyKind::User,
1455                    index_id,
1456                    [std::slice::from_ref(&collection)],
1457                    4,
1458                )
1459                .unwrap(),
1460            None,
1461            "generation drift should keep batched child-prefix expansion fail-closed",
1462        );
1463    }
1464
1465    #[test]
1466    fn index_prefix_cardinality_ignores_system_index_mutations() {
1467        let user_index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1468        let system_index_id = IndexId::new(EntityTag::new(0xCA7D), 2);
1469        let collection = b"collection-a".to_vec();
1470        let draft = b"Draft".to_vec();
1471        let system_component = b"reverse-edge".to_vec();
1472        let mut store = IndexStore::init_heap();
1473
1474        store.insert(
1475            indexed_raw_key(&user_index_id, vec![collection.clone(), draft.clone()], 1),
1476            IndexEntryValue::presence(),
1477        );
1478        store.mark_prefix_cardinality_data_generation(7);
1479
1480        assert_eq!(
1481            store.exact_prefix_cardinality(
1482                7,
1483                IndexKeyKind::User,
1484                user_index_id,
1485                &[collection.clone(), draft.clone()],
1486            ),
1487            Some(1),
1488        );
1489
1490        let system_key = indexed_raw_key_with_kind(
1491            &system_index_id,
1492            IndexKeyKind::System,
1493            vec![system_component],
1494            1,
1495        );
1496        store.insert(system_key.clone(), IndexEntryValue::presence());
1497        assert_eq!(
1498            store.exact_prefix_cardinality(
1499                7,
1500                IndexKeyKind::User,
1501                user_index_id,
1502                &[collection.clone(), draft.clone()],
1503            ),
1504            Some(1),
1505            "system index writes must not invalidate synchronized user-prefix cardinality",
1506        );
1507
1508        store.remove(&system_key);
1509        assert_eq!(
1510            store.exact_prefix_cardinality(
1511                7,
1512                IndexKeyKind::User,
1513                user_index_id,
1514                &[collection.clone(), draft.clone()],
1515            ),
1516            Some(1),
1517            "system index removals must not invalidate synchronized user-prefix cardinality",
1518        );
1519
1520        let malformed_system_key = indexed_raw_key_with_kind(
1521            &system_index_id,
1522            IndexKeyKind::System,
1523            vec![b"malformed-reverse-edge".to_vec()],
1524            2,
1525        );
1526        store.insert(malformed_system_key.clone(), malformed_index_entry_value());
1527        assert_eq!(
1528            store.exact_prefix_cardinality(
1529                7,
1530                IndexKeyKind::User,
1531                user_index_id,
1532                &[collection.clone(), draft.clone()],
1533            ),
1534            Some(1),
1535            "malformed system index payloads must not invalidate user-prefix cardinality",
1536        );
1537
1538        store.remove(&malformed_system_key);
1539        assert_eq!(
1540            store.exact_prefix_cardinality(
1541                7,
1542                IndexKeyKind::User,
1543                user_index_id,
1544                &[collection.clone(), draft],
1545            ),
1546            Some(1),
1547            "malformed system index removals must not invalidate user-prefix cardinality",
1548        );
1549
1550        let review = b"Review".to_vec();
1551        store.insert(
1552            indexed_raw_key(&user_index_id, vec![collection.clone(), review.clone()], 2),
1553            IndexEntryValue::presence(),
1554        );
1555        assert_eq!(
1556            store.exact_prefix_cardinality(
1557                7,
1558                IndexKeyKind::User,
1559                user_index_id,
1560                &[collection, review]
1561            ),
1562            None,
1563            "user-prefix count changes must still require a fresh row-generation stamp",
1564        );
1565    }
1566
1567    #[test]
1568    fn index_prefix_cardinality_ignores_missing_user_index_mutations() {
1569        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1570        let collection = b"collection-a".to_vec();
1571        let draft = b"Draft".to_vec();
1572        let mut store = IndexStore::init_heap();
1573
1574        store.insert(
1575            indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
1576            IndexEntryValue::presence(),
1577        );
1578        store.mark_prefix_cardinality_data_generation(7);
1579
1580        let stale_key = indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 2);
1581        store.insert(stale_key.clone(), missing_index_entry_value());
1582        assert_eq!(
1583            store.exact_prefix_cardinality(
1584                7,
1585                IndexKeyKind::User,
1586                index_id,
1587                &[collection.clone(), draft.clone()],
1588            ),
1589            Some(1),
1590            "missing user index entries must not affect synchronized prefix cardinality",
1591        );
1592
1593        store.remove(&stale_key);
1594        assert_eq!(
1595            store.exact_prefix_cardinality(7, IndexKeyKind::User, index_id, &[collection, draft],),
1596            Some(1),
1597            "missing user index removals must not affect synchronized prefix cardinality",
1598        );
1599    }
1600
1601    #[test]
1602    fn journaled_mixed_index_range_traversal_preserves_order_and_early_stop() {
1603        let mut store = IndexStore::init_journaled(test_memory(93));
1604        for value in [1_u8, 3, 5] {
1605            store
1606                .apply_canonical_entry(raw_key(value), Some(IndexEntryValue::presence()))
1607                .expect("canonical index seed should apply");
1608        }
1609
1610        store.insert(raw_key(0), IndexEntryValue::presence());
1611        store.insert(raw_key(4), IndexEntryValue::presence());
1612        store.insert(raw_key(5), IndexEntryValue::presence());
1613        store.remove(&raw_key(1));
1614
1615        let lower = Bound::Included(raw_key(0));
1616        let upper = Bound::Included(raw_key(5));
1617
1618        let mut asc = Vec::new();
1619        store
1620            .visit_journaled_entries_in_range(
1621                (lower.as_ref(), upper.as_ref()),
1622                Direction::Asc,
1623                &mut |key, _value| {
1624                    asc.push(key.as_bytes()[0]);
1625                    Ok::<_, Infallible>(asc.len() == 2)
1626                },
1627            )
1628            .expect("asc journaled index range traversal should succeed");
1629        assert_eq!(asc, vec![0, 3]);
1630
1631        let mut desc = Vec::new();
1632        store
1633            .visit_journaled_entries_in_range(
1634                (lower.as_ref(), upper.as_ref()),
1635                Direction::Desc,
1636                &mut |key, _value| {
1637                    desc.push(key.as_bytes()[0]);
1638                    Ok::<_, Infallible>(desc.len() == 2)
1639                },
1640            )
1641            .expect("desc journaled index range traversal should succeed");
1642        assert_eq!(desc, vec![5, 4]);
1643    }
1644
1645    #[test]
1646    fn journaled_index_range_preserves_typed_failure_and_stops_immediately() {
1647        // Exercise canonical-only, live-only and mixed/tombstoned fast paths.
1648        for backend in 0..3 {
1649            let mut store = IndexStore::init_journaled(test_memory(93));
1650            if backend != 1 {
1651                for value in [1, 3, 5] {
1652                    store
1653                        .apply_canonical_entry(raw_key(value), Some(IndexEntryValue::presence()))
1654                        .expect("canonical seed should apply");
1655                }
1656            }
1657            if backend != 0 {
1658                for value in [0, 4, 5] {
1659                    store.insert(raw_key(value), IndexEntryValue::presence());
1660                }
1661                store.remove(&raw_key(1));
1662            }
1663            let lower = Bound::Included(raw_key(0));
1664            let upper = Bound::Included(raw_key(5));
1665            for direction in [Direction::Asc, Direction::Desc] {
1666                let mut visited = Vec::new();
1667                let result = store.visit_journaled_entries_in_range(
1668                    (lower.as_ref(), upper.as_ref()),
1669                    direction,
1670                    &mut |key, _| {
1671                        visited.push(key.as_bytes()[0]);
1672                        Err::<bool, u8>(7)
1673                    },
1674                );
1675                assert_eq!(result, Err(7));
1676                let first = match direction {
1677                    Direction::Asc if backend == 0 => 1,
1678                    Direction::Asc => 0,
1679                    Direction::Desc => 5,
1680                };
1681                assert_eq!(visited, vec![first]);
1682            }
1683        }
1684    }
1685
1686    #[test]
1687    fn journaled_index_store_reopens_without_materializing_prefix_cardinality() {
1688        let memory = crate::testing::test_memory_owner(94);
1689        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1690        let collection = b"collection-a".to_vec();
1691        let mut store = IndexStore::init_journaled(memory.open());
1692        let key = indexed_raw_key(&index_id, vec![collection.clone()], 1);
1693        store
1694            .apply_canonical_entry(key.clone(), Some(IndexEntryValue::presence()))
1695            .expect("canonical index seed should apply");
1696        drop(store);
1697
1698        let mut reopened = IndexStore::init_journaled(memory.open());
1699        let bound = Bound::Unbounded;
1700
1701        assert_eq!(reopened.get(&key), Some(IndexEntryValue::presence()));
1702        assert_eq!(
1703            reopened.exact_prefix_cardinality(
1704                0,
1705                IndexKeyKind::User,
1706                index_id,
1707                std::slice::from_ref(&collection),
1708            ),
1709            None,
1710            "startup must leave optional prefix cardinality unavailable without scanning stable entries",
1711        );
1712        assert_eq!(
1713            reopened
1714                .exact_first_component_range_cardinality(0, index_id, &bound, &bound, 2)
1715                .expect("unmaterialized metadata should remain structurally valid"),
1716            None,
1717            "startup must not treat an absent materialized leading-component map as exact evidence",
1718        );
1719        assert_eq!(
1720            reopened.exact_prefix_cardinality_delta(
1721                IndexKeyKind::User,
1722                index_id,
1723                std::slice::from_ref(&collection),
1724            ),
1725            Some(0),
1726        );
1727        let second = indexed_raw_key(&index_id, vec![collection.clone()], 2);
1728        reopened.insert(second.clone(), IndexEntryValue::presence());
1729        assert_eq!(
1730            reopened.exact_prefix_cardinality_delta(
1731                IndexKeyKind::User,
1732                index_id,
1733                std::slice::from_ref(&collection),
1734            ),
1735            Some(1),
1736        );
1737        reopened
1738            .apply_canonical_entry(second, Some(IndexEntryValue::presence()))
1739            .expect("matching canonical index entry should fold");
1740        assert_eq!(
1741            reopened.exact_prefix_cardinality_delta(
1742                IndexKeyKind::User,
1743                index_id,
1744                std::slice::from_ref(&collection),
1745            ),
1746            Some(0),
1747            "canonical fold must consume only its exact overlay prefix contribution",
1748        );
1749    }
1750
1751    #[test]
1752    fn empty_journaled_index_store_retains_exact_prefix_cardinality_without_scanning() {
1753        let memory = crate::testing::test_memory_owner(95);
1754        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1755        let collection = b"collection-a".to_vec();
1756        let mut store = IndexStore::init_journaled(memory.open());
1757
1758        assert_eq!(
1759            store.exact_prefix_cardinality(
1760                0,
1761                IndexKeyKind::User,
1762                index_id,
1763                std::slice::from_ref(&collection),
1764            ),
1765            Some(0),
1766        );
1767        store
1768            .reset_journaled_live_projection(7, FoldWatermark::initial())
1769            .expect("empty projection reset should succeed");
1770        assert_eq!(
1771            store.exact_prefix_cardinality(
1772                7,
1773                IndexKeyKind::User,
1774                index_id,
1775                std::slice::from_ref(&collection),
1776            ),
1777            Some(0),
1778        );
1779        drop(store);
1780
1781        let reopened = IndexStore::init_journaled(memory.open());
1782        assert_eq!(
1783            reopened.exact_prefix_cardinality(
1784                0,
1785                IndexKeyKind::User,
1786                index_id,
1787                std::slice::from_ref(&collection),
1788            ),
1789            Some(0),
1790        );
1791    }
1792
1793    #[test]
1794    fn journaled_prefix_delta_binds_and_advances_its_canonical_watermark() {
1795        let mut store = IndexStore::init_journaled(test_memory(99));
1796        assert_eq!(store.exact_prefix_cardinality_delta_watermark(), None);
1797
1798        let current = FoldWatermark::new(JournalSequence::new(7), 3);
1799        let next = FoldWatermark::new(JournalSequence::new(8), 4);
1800        store
1801            .reset_journaled_live_projection(0, current)
1802            .expect("delta reset should bind the current canonical watermark");
1803        assert_eq!(
1804            store.exact_prefix_cardinality_delta_watermark(),
1805            Some(current)
1806        );
1807        store
1808            .preflight_prefix_cardinality_delta_watermark(current)
1809            .expect("matching watermark should preflight");
1810        assert!(
1811            store
1812                .preflight_prefix_cardinality_delta_watermark(next)
1813                .is_err(),
1814            "a stale or future base must fail closed",
1815        );
1816
1817        store.apply_prefix_cardinality_delta_watermark(current, next);
1818        assert_eq!(store.exact_prefix_cardinality_delta_watermark(), Some(next));
1819    }
1820
1821    #[test]
1822    fn recovered_index_fold_maintains_available_prefix_cardinality() {
1823        let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1824        let collection = b"collection-a".to_vec();
1825        let key = indexed_raw_key(&index_id, vec![collection.clone()], 1);
1826        let mut store = IndexStore::init_journaled(test_memory(96));
1827
1828        store
1829            .apply_canonical_entry(key.clone(), Some(IndexEntryValue::presence()))
1830            .expect("recovered index put should fold");
1831        store.mark_prefix_cardinality_data_generation(1);
1832        assert_eq!(
1833            store.exact_prefix_cardinality(
1834                1,
1835                IndexKeyKind::User,
1836                index_id,
1837                std::slice::from_ref(&collection),
1838            ),
1839            Some(1),
1840        );
1841
1842        store
1843            .apply_canonical_entry(key, None)
1844            .expect("recovered index delete should fold");
1845        store.mark_prefix_cardinality_data_generation(2);
1846        assert_eq!(
1847            store.exact_prefix_cardinality(
1848                2,
1849                IndexKeyKind::User,
1850                index_id,
1851                std::slice::from_ref(&collection),
1852            ),
1853            Some(0),
1854        );
1855    }
1856
1857    #[test]
1858    fn positioned_index_overlay_preserves_later_membership_until_exact_retirement() {
1859        let key = raw_key(7);
1860        let mut store = IndexStore::init_journaled(test_memory(97));
1861        store
1862            .apply_canonical_entry(key.clone(), Some(IndexEntryValue::presence()))
1863            .expect("canonical membership should seed");
1864
1865        store
1866            .publish_positioned_journal_entry(key.clone(), None, overlay_position(1))
1867            .expect("positioned tombstone should publish");
1868        store
1869            .publish_positioned_journal_entry(
1870                key.clone(),
1871                Some(IndexEntryValue::presence()),
1872                overlay_position(2),
1873            )
1874            .expect("later membership should supersede the tombstone");
1875        let prepared = store
1876            .prepare_position_retirement([key.clone()], overlay_position(1))
1877            .expect("older retirement should preserve later membership");
1878        assert_eq!(
1879            prepared.entries,
1880            [(key.clone(), PositionedOverlayRetirement::Superseded)]
1881        );
1882        store
1883            .apply_canonical_entry(key.clone(), None)
1884            .expect("tombstone batch should become canonical");
1885        store.apply_prepared_position_retirement(prepared);
1886        assert_eq!(store.get(&key), Some(IndexEntryValue::presence()));
1887
1888        let prepared = store
1889            .prepare_position_retirement([key.clone()], overlay_position(2))
1890            .expect("latest retirement should be exact");
1891        assert_eq!(
1892            prepared.entries,
1893            [(key.clone(), PositionedOverlayRetirement::Exact)]
1894        );
1895        store
1896            .apply_canonical_entry(key.clone(), Some(IndexEntryValue::presence()))
1897            .expect("membership batch should become canonical");
1898        store.apply_prepared_position_retirement(prepared);
1899        assert_eq!(store.get(&key), Some(IndexEntryValue::presence()));
1900
1901        let mut visible = Vec::new();
1902        store
1903            .visit_entries(|visited_key, value| {
1904                visible.push((visited_key.clone(), value.clone()));
1905                Ok::<_, Infallible>(IndexStoreVisit::Continue)
1906            })
1907            .expect("positioned index should remain range-visible");
1908        assert_eq!(visible, vec![(key, IndexEntryValue::presence())]);
1909    }
1910
1911    #[test]
1912    fn positioned_index_overlay_coalesces_repeated_same_batch_target() {
1913        let key = raw_key(8);
1914        let position = overlay_position(3);
1915        let mut store = IndexStore::init_journaled(test_memory(98));
1916
1917        store
1918            .publish_positioned_journal_entry(
1919                key.clone(),
1920                Some(IndexEntryValue::presence()),
1921                position,
1922            )
1923            .expect("first same-batch effect should publish");
1924        store
1925            .publish_positioned_journal_entry(key.clone(), None, position)
1926            .expect("final same-batch effect should coalesce by logical target");
1927        let prepared = store
1928            .prepare_position_retirement([key.clone()], position)
1929            .expect("coalesced target should retire once");
1930        assert_eq!(
1931            prepared.entries,
1932            [(key.clone(), PositionedOverlayRetirement::Exact)]
1933        );
1934        store
1935            .apply_canonical_entry(key.clone(), None)
1936            .expect("coalesced final effect should become canonical");
1937        store.apply_prepared_position_retirement(prepared);
1938        assert!(store.get(&key).is_none());
1939    }
1940}