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.clone();
979 let upper = bounds.1.clone();
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.clone(), upper.clone())),
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.clone(), upper.clone())).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 ];
1365 assert_eq!(
1366 store.exact_child_prefixes_for_parent_set(7, IndexKeyKind::User, index_id, parents, 4,),
1367 Some(vec![
1368 vec![collection.clone(), draft],
1369 vec![collection.clone(), review],
1370 vec![other_collection.clone(), published],
1371 ]),
1372 "batched child-prefix enumeration should skip missing sparse parents and return deterministic real children",
1373 );
1374 assert_eq!(
1375 store.exact_child_prefixes_for_parent_set(
1376 7,
1377 IndexKeyKind::User,
1378 index_id,
1379 [
1380 std::slice::from_ref(&missing_a),
1381 std::slice::from_ref(&missing_b)
1382 ],
1383 4,
1384 ),
1385 Some(Vec::new()),
1386 "missing-only sparse parent sets should be proven empty when cardinality is synchronized",
1387 );
1388 assert_eq!(
1389 store.exact_child_prefixes_for_parent_set(
1390 7,
1391 IndexKeyKind::User,
1392 index_id,
1393 [
1394 std::slice::from_ref(&collection),
1395 std::slice::from_ref(&other_collection)
1396 ],
1397 2,
1398 ),
1399 None,
1400 "over-cap sparse parent-set expansion should fail closed",
1401 );
1402 assert_eq!(
1403 store.exact_child_prefixes_for_parent_set(
1404 8,
1405 IndexKeyKind::User,
1406 index_id,
1407 [std::slice::from_ref(&collection)],
1408 4,
1409 ),
1410 None,
1411 "generation drift should keep batched child-prefix expansion fail-closed",
1412 );
1413 }
1414
1415 #[test]
1416 fn index_prefix_cardinality_ignores_system_index_mutations() {
1417 let user_index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1418 let system_index_id = IndexId::new(EntityTag::new(0xCA7D), 2);
1419 let collection = b"collection-a".to_vec();
1420 let draft = b"Draft".to_vec();
1421 let system_component = b"reverse-edge".to_vec();
1422 let mut store = IndexStore::init_heap();
1423
1424 store.insert(
1425 indexed_raw_key(&user_index_id, vec![collection.clone(), draft.clone()], 1),
1426 IndexEntryValue::presence(),
1427 );
1428 store.mark_prefix_cardinality_data_generation(7);
1429
1430 assert_eq!(
1431 store.exact_prefix_cardinality(
1432 7,
1433 IndexKeyKind::User,
1434 user_index_id,
1435 &[collection.clone(), draft.clone()],
1436 ),
1437 Some(1),
1438 );
1439
1440 let system_key = indexed_raw_key_with_kind(
1441 &system_index_id,
1442 IndexKeyKind::System,
1443 vec![system_component],
1444 1,
1445 );
1446 store.insert(system_key.clone(), IndexEntryValue::presence());
1447 assert_eq!(
1448 store.exact_prefix_cardinality(
1449 7,
1450 IndexKeyKind::User,
1451 user_index_id,
1452 &[collection.clone(), draft.clone()],
1453 ),
1454 Some(1),
1455 "system index writes must not invalidate synchronized user-prefix cardinality",
1456 );
1457
1458 store.remove(&system_key);
1459 assert_eq!(
1460 store.exact_prefix_cardinality(
1461 7,
1462 IndexKeyKind::User,
1463 user_index_id,
1464 &[collection.clone(), draft.clone()],
1465 ),
1466 Some(1),
1467 "system index removals must not invalidate synchronized user-prefix cardinality",
1468 );
1469
1470 let malformed_system_key = indexed_raw_key_with_kind(
1471 &system_index_id,
1472 IndexKeyKind::System,
1473 vec![b"malformed-reverse-edge".to_vec()],
1474 2,
1475 );
1476 store.insert(malformed_system_key.clone(), malformed_index_entry_value());
1477 assert_eq!(
1478 store.exact_prefix_cardinality(
1479 7,
1480 IndexKeyKind::User,
1481 user_index_id,
1482 &[collection.clone(), draft.clone()],
1483 ),
1484 Some(1),
1485 "malformed system index payloads must not invalidate user-prefix cardinality",
1486 );
1487
1488 store.remove(&malformed_system_key);
1489 assert_eq!(
1490 store.exact_prefix_cardinality(
1491 7,
1492 IndexKeyKind::User,
1493 user_index_id,
1494 &[collection.clone(), draft],
1495 ),
1496 Some(1),
1497 "malformed system index removals must not invalidate user-prefix cardinality",
1498 );
1499
1500 let review = b"Review".to_vec();
1501 store.insert(
1502 indexed_raw_key(&user_index_id, vec![collection.clone(), review.clone()], 2),
1503 IndexEntryValue::presence(),
1504 );
1505 assert_eq!(
1506 store.exact_prefix_cardinality(
1507 7,
1508 IndexKeyKind::User,
1509 user_index_id,
1510 &[collection, review]
1511 ),
1512 None,
1513 "user-prefix count changes must still require a fresh row-generation stamp",
1514 );
1515 }
1516
1517 #[test]
1518 fn index_prefix_cardinality_ignores_missing_user_index_mutations() {
1519 let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1520 let collection = b"collection-a".to_vec();
1521 let draft = b"Draft".to_vec();
1522 let mut store = IndexStore::init_heap();
1523
1524 store.insert(
1525 indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 1),
1526 IndexEntryValue::presence(),
1527 );
1528 store.mark_prefix_cardinality_data_generation(7);
1529
1530 let stale_key = indexed_raw_key(&index_id, vec![collection.clone(), draft.clone()], 2);
1531 store.insert(stale_key.clone(), missing_index_entry_value());
1532 assert_eq!(
1533 store.exact_prefix_cardinality(
1534 7,
1535 IndexKeyKind::User,
1536 index_id,
1537 &[collection.clone(), draft.clone()],
1538 ),
1539 Some(1),
1540 "missing user index entries must not affect synchronized prefix cardinality",
1541 );
1542
1543 store.remove(&stale_key);
1544 assert_eq!(
1545 store.exact_prefix_cardinality(7, IndexKeyKind::User, index_id, &[collection, draft],),
1546 Some(1),
1547 "missing user index removals must not affect synchronized prefix cardinality",
1548 );
1549 }
1550
1551 #[test]
1552 fn journaled_mixed_index_range_traversal_streams_without_snapshot() {
1553 let mut store = IndexStore::init_journaled(test_memory(93));
1554 for value in [1_u8, 3, 5] {
1555 store.insert(raw_key(value), IndexEntryValue::presence());
1556 }
1557 store
1558 .fold_journaled_materialized_view()
1559 .expect("canonical index seed should fold");
1560
1561 store.insert(raw_key(0), IndexEntryValue::presence());
1562 store.insert(raw_key(4), IndexEntryValue::presence());
1563 store.insert(raw_key(5), IndexEntryValue::presence());
1564 store.remove(&raw_key(1));
1565
1566 let lower = Bound::Included(raw_key(0));
1567 let upper = Bound::Included(raw_key(5));
1568
1569 reset_journaled_snapshot_call_count_for_tests();
1570 let mut asc = Vec::new();
1571 store
1572 .visit_journaled_entries_in_range((&lower, &upper), Direction::Asc, |key, _value| {
1573 asc.push(key.as_bytes()[0]);
1574 Ok::<_, Infallible>(asc.len() == 2)
1575 })
1576 .expect("asc journaled index range traversal should succeed");
1577 assert_eq!(asc, vec![0, 3]);
1578 assert_eq!(
1579 journaled_snapshot_call_count_for_tests(),
1580 0,
1581 "mixed journaled index range traversal should preserve early stop without materializing a snapshot",
1582 );
1583
1584 reset_journaled_snapshot_call_count_for_tests();
1585 let mut desc = Vec::new();
1586 store
1587 .visit_journaled_entries_in_range((&lower, &upper), Direction::Desc, |key, _value| {
1588 desc.push(key.as_bytes()[0]);
1589 Ok::<_, Infallible>(desc.len() == 2)
1590 })
1591 .expect("desc journaled index range traversal should succeed");
1592 assert_eq!(desc, vec![5, 4]);
1593 assert_eq!(
1594 journaled_snapshot_call_count_for_tests(),
1595 0,
1596 "mixed reverse journaled index range traversal should preserve early stop without materializing a snapshot",
1597 );
1598 }
1599
1600 #[test]
1601 fn journaled_index_store_reopens_without_materializing_prefix_cardinality() {
1602 let memory = test_memory(94);
1603 let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1604 let collection = b"collection-a".to_vec();
1605 let mut store = IndexStore::init_journaled(memory.clone());
1606 let key = indexed_raw_key(&index_id, vec![collection.clone()], 1);
1607 store.insert(key.clone(), IndexEntryValue::presence());
1608 store
1609 .fold_journaled_materialized_view()
1610 .expect("canonical index seed should fold");
1611 drop(store);
1612
1613 let mut reopened = IndexStore::init_journaled(memory);
1614
1615 assert_eq!(reopened.get(&key), Some(IndexEntryValue::presence()));
1616 assert_eq!(
1617 reopened.exact_prefix_cardinality(
1618 0,
1619 IndexKeyKind::User,
1620 index_id,
1621 std::slice::from_ref(&collection),
1622 ),
1623 None,
1624 "startup must leave optional prefix cardinality unavailable without scanning stable entries",
1625 );
1626 assert_eq!(
1627 reopened
1628 .exact_first_component_distinct_cardinality(0, index_id, 2)
1629 .expect("unmaterialized metadata should remain structurally valid"),
1630 None,
1631 "startup must not treat an absent materialized leading-component map as exact evidence",
1632 );
1633 assert_eq!(
1634 reopened.exact_prefix_cardinality_delta(
1635 IndexKeyKind::User,
1636 index_id,
1637 std::slice::from_ref(&collection),
1638 ),
1639 Some(0),
1640 );
1641 let second = indexed_raw_key(&index_id, vec![collection.clone()], 2);
1642 reopened.insert(second.clone(), IndexEntryValue::presence());
1643 assert_eq!(
1644 reopened.exact_prefix_cardinality_delta(
1645 IndexKeyKind::User,
1646 index_id,
1647 std::slice::from_ref(&collection),
1648 ),
1649 Some(1),
1650 );
1651 reopened
1652 .fold_recovered_journal_entry(second, Some(IndexEntryValue::presence()))
1653 .expect("matching canonical index entry should fold");
1654 assert_eq!(
1655 reopened.exact_prefix_cardinality_delta(
1656 IndexKeyKind::User,
1657 index_id,
1658 std::slice::from_ref(&collection),
1659 ),
1660 Some(0),
1661 "canonical fold must consume only its exact overlay prefix contribution",
1662 );
1663 }
1664
1665 #[test]
1666 fn empty_journaled_index_store_retains_exact_prefix_cardinality_without_scanning() {
1667 let memory = test_memory(95);
1668 let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1669 let collection = b"collection-a".to_vec();
1670 let mut store = IndexStore::init_journaled(memory.clone());
1671
1672 assert_eq!(
1673 store.exact_prefix_cardinality(
1674 0,
1675 IndexKeyKind::User,
1676 index_id,
1677 std::slice::from_ref(&collection),
1678 ),
1679 Some(0),
1680 );
1681 store
1682 .reset_journaled_live_projection(7, FoldWatermark::initial())
1683 .expect("empty projection reset should succeed");
1684 assert_eq!(
1685 store.exact_prefix_cardinality(
1686 7,
1687 IndexKeyKind::User,
1688 index_id,
1689 std::slice::from_ref(&collection),
1690 ),
1691 Some(0),
1692 );
1693 drop(store);
1694
1695 let reopened = IndexStore::init_journaled(memory);
1696 assert_eq!(
1697 reopened.exact_prefix_cardinality(
1698 0,
1699 IndexKeyKind::User,
1700 index_id,
1701 std::slice::from_ref(&collection),
1702 ),
1703 Some(0),
1704 );
1705 }
1706
1707 #[test]
1708 fn journaled_prefix_delta_binds_and_advances_its_canonical_watermark() {
1709 let mut store = IndexStore::init_journaled(test_memory(99));
1710 assert_eq!(store.exact_prefix_cardinality_delta_watermark(), None);
1711
1712 let current = FoldWatermark::new(JournalSequence::new(7), 3);
1713 let next = FoldWatermark::new(JournalSequence::new(8), 4);
1714 store
1715 .reset_journaled_live_projection(0, current)
1716 .expect("delta reset should bind the current canonical watermark");
1717 assert_eq!(
1718 store.exact_prefix_cardinality_delta_watermark(),
1719 Some(current)
1720 );
1721 store
1722 .preflight_prefix_cardinality_delta_watermark(current)
1723 .expect("matching watermark should preflight");
1724 assert!(
1725 store
1726 .preflight_prefix_cardinality_delta_watermark(next)
1727 .is_err(),
1728 "a stale or future base must fail closed",
1729 );
1730
1731 store.apply_prefix_cardinality_delta_watermark(current, next);
1732 assert_eq!(store.exact_prefix_cardinality_delta_watermark(), Some(next));
1733 }
1734
1735 #[test]
1736 fn recovered_index_fold_maintains_available_prefix_cardinality() {
1737 let index_id = IndexId::new(EntityTag::new(0xCA7D), 1);
1738 let collection = b"collection-a".to_vec();
1739 let key = indexed_raw_key(&index_id, vec![collection.clone()], 1);
1740 let mut store = IndexStore::init_journaled(test_memory(96));
1741
1742 store
1743 .fold_recovered_journal_entry(key.clone(), Some(IndexEntryValue::presence()))
1744 .expect("recovered index put should fold");
1745 store.mark_prefix_cardinality_data_generation(1);
1746 assert_eq!(
1747 store.exact_prefix_cardinality(
1748 1,
1749 IndexKeyKind::User,
1750 index_id,
1751 std::slice::from_ref(&collection),
1752 ),
1753 Some(1),
1754 );
1755
1756 store
1757 .fold_recovered_journal_entry(key, None)
1758 .expect("recovered index delete should fold");
1759 store.mark_prefix_cardinality_data_generation(2);
1760 assert_eq!(
1761 store.exact_prefix_cardinality(
1762 2,
1763 IndexKeyKind::User,
1764 index_id,
1765 std::slice::from_ref(&collection),
1766 ),
1767 Some(0),
1768 );
1769 }
1770
1771 #[test]
1772 fn positioned_index_overlay_preserves_later_membership_until_exact_retirement() {
1773 let key = raw_key(7);
1774 let mut store = IndexStore::init_journaled(test_memory(97));
1775 store
1776 .fold_recovered_journal_entry(key.clone(), Some(IndexEntryValue::presence()))
1777 .expect("canonical membership should seed");
1778
1779 store
1780 .publish_positioned_journal_entry(key.clone(), None, overlay_position(1))
1781 .expect("positioned tombstone should publish");
1782 store
1783 .publish_positioned_journal_entry(
1784 key.clone(),
1785 Some(IndexEntryValue::presence()),
1786 overlay_position(2),
1787 )
1788 .expect("later membership should supersede the tombstone");
1789 store
1790 .fold_recovered_journal_entry(key.clone(), None)
1791 .expect("tombstone batch should become canonical");
1792 assert_eq!(
1793 store
1794 .retire_positioned_journal_effect(&key, overlay_position(1))
1795 .expect("older retirement should preserve later membership"),
1796 PositionedOverlayRetirement::Superseded,
1797 );
1798 assert_eq!(store.get(&key), Some(IndexEntryValue::presence()));
1799
1800 store
1801 .fold_recovered_journal_entry(key.clone(), Some(IndexEntryValue::presence()))
1802 .expect("membership batch should become canonical");
1803 assert_eq!(
1804 store
1805 .retire_positioned_journal_effect(&key, overlay_position(2))
1806 .expect("latest retirement should be exact"),
1807 PositionedOverlayRetirement::Exact,
1808 );
1809 assert_eq!(store.get(&key), Some(IndexEntryValue::presence()));
1810
1811 let mut visible = Vec::new();
1812 store
1813 .visit_entries(|visited_key, value| {
1814 visible.push((visited_key.clone(), value.clone()));
1815 Ok::<_, Infallible>(IndexStoreVisit::Continue)
1816 })
1817 .expect("positioned index should remain range-visible");
1818 assert_eq!(visible, vec![(key, IndexEntryValue::presence())]);
1819 }
1820
1821 #[test]
1822 fn positioned_index_overlay_coalesces_repeated_same_batch_target() {
1823 let key = raw_key(8);
1824 let position = overlay_position(3);
1825 let mut store = IndexStore::init_journaled(test_memory(98));
1826
1827 store
1828 .publish_positioned_journal_entry(
1829 key.clone(),
1830 Some(IndexEntryValue::presence()),
1831 position,
1832 )
1833 .expect("first same-batch effect should publish");
1834 store
1835 .publish_positioned_journal_entry(key.clone(), None, position)
1836 .expect("final same-batch effect should coalesce by logical target");
1837 store
1838 .fold_recovered_journal_entry(key.clone(), None)
1839 .expect("coalesced final effect should become canonical");
1840 assert_eq!(
1841 store
1842 .retire_positioned_journal_effect(&key, position)
1843 .expect("coalesced target should retire once"),
1844 PositionedOverlayRetirement::Exact,
1845 );
1846 assert!(store.get(&key).is_none());
1847 }
1848}