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