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