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