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