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