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