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