Skip to main content

icydb_core/db/index/
store.rs

1//! Module: index::store
2//! Responsibility: stable index-entry persistence primitives.
3//! Does not own: range-scan resolution, continuation semantics, or predicate execution.
4//! Boundary: scan/executor layers depend on this storage boundary.
5
6use crate::db::index::{IndexEntryValue, key::RawIndexStoreKey};
7
8use candid::CandidType;
9use ic_memory::stable_structures::{
10    BTreeMap as StableBTreeMap, DefaultMemoryImpl, memory_manager::VirtualMemory,
11};
12use serde::Deserialize;
13use std::collections::BTreeMap as HeapBTreeMap;
14
15//
16// IndexState
17//
18// Explicit lifecycle visibility state for one index store.
19// Visibility matters because planner-visible indexes must already be complete:
20// the index contents are fully built and query-visible for reads.
21//
22#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
23pub enum IndexState {
24    Building,
25    #[default]
26    Ready,
27    Dropping,
28}
29
30impl IndexState {
31    /// Return the stable lowercase text label for this lifecycle state.
32    #[must_use]
33    pub const fn as_str(self) -> &'static str {
34        match self {
35            Self::Building => "building",
36            Self::Ready => "ready",
37            Self::Dropping => "dropping",
38        }
39    }
40}
41
42///
43/// IndexStore
44///
45/// Thin persistence wrapper over one stable or heap BTreeMap.
46///
47/// Invariant: callers provide already-validated `RawIndexStoreKey`/`IndexEntryValue`.
48///
49
50pub struct IndexStore {
51    pub(super) backend: IndexStoreBackend,
52    generation: u64,
53    state: IndexState,
54}
55
56pub(super) enum IndexStoreBackend {
57    Stable(StableBTreeMap<RawIndexStoreKey, IndexEntryValue, VirtualMemory<DefaultMemoryImpl>>),
58    Heap(HeapBTreeMap<RawIndexStoreKey, IndexEntryValue>),
59}
60
61/// Control-flow result for index-store traversal visitors.
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub(in crate::db) enum IndexStoreVisit {
64    Continue,
65    #[allow(
66        dead_code,
67        reason = "index traversal exposes early-stop semantics for bounded future callers; focused tests cover it before live call sites need it"
68    )]
69    Stop,
70}
71
72impl IndexStoreVisit {
73    const fn should_stop(self) -> bool {
74        matches!(self, Self::Stop)
75    }
76}
77
78impl IndexStore {
79    #[must_use]
80    pub fn init(memory: VirtualMemory<DefaultMemoryImpl>) -> Self {
81        Self {
82            backend: IndexStoreBackend::Stable(StableBTreeMap::init(memory)),
83            generation: 0,
84            // Existing stores default to Ready until one explicit build/drop
85            // lifecycle is introduced.
86            state: IndexState::Ready,
87        }
88    }
89
90    /// Initialize a volatile heap-backed index store.
91    #[must_use]
92    pub const fn init_heap() -> Self {
93        Self {
94            backend: IndexStoreBackend::Heap(HeapBTreeMap::new()),
95            generation: 0,
96            state: IndexState::Ready,
97        }
98    }
99
100    /// Return whether this index store is heap-backed and volatile.
101    #[must_use]
102    pub(in crate::db) const fn is_heap_storage(&self) -> bool {
103        matches!(self.backend, IndexStoreBackend::Heap(_))
104    }
105
106    /// Visit all index entries in canonical store order without exposing the
107    /// backing stable-map iterator.
108    pub(in crate::db) fn visit_entries<E>(
109        &self,
110        mut visitor: impl FnMut(&RawIndexStoreKey, &IndexEntryValue) -> Result<IndexStoreVisit, E>,
111    ) -> Result<(), E> {
112        match &self.backend {
113            IndexStoreBackend::Stable(map) => {
114                for entry in map.iter() {
115                    if visitor(entry.key(), &entry.value())?.should_stop() {
116                        return Ok(());
117                    }
118                }
119            }
120            IndexStoreBackend::Heap(map) => {
121                for (key, value) in map {
122                    if visitor(key, value)?.should_stop() {
123                        return Ok(());
124                    }
125                }
126            }
127        }
128
129        Ok(())
130    }
131
132    pub(in crate::db) fn get(&self, key: &RawIndexStoreKey) -> Option<IndexEntryValue> {
133        match &self.backend {
134            IndexStoreBackend::Stable(map) => map.get(key),
135            IndexStoreBackend::Heap(map) => map.get(key).cloned(),
136        }
137    }
138
139    pub fn len(&self) -> u64 {
140        match &self.backend {
141            IndexStoreBackend::Stable(map) => map.len(),
142            IndexStoreBackend::Heap(map) => u64::try_from(map.len()).unwrap_or(u64::MAX),
143        }
144    }
145
146    pub fn is_empty(&self) -> bool {
147        match &self.backend {
148            IndexStoreBackend::Stable(map) => map.is_empty(),
149            IndexStoreBackend::Heap(map) => map.is_empty(),
150        }
151    }
152
153    #[must_use]
154    pub(in crate::db) const fn generation(&self) -> u64 {
155        self.generation
156    }
157
158    /// Return the explicit lifecycle state for this index store.
159    #[must_use]
160    pub(in crate::db) const fn state(&self) -> IndexState {
161        self.state
162    }
163
164    /// Mark this index store as in-progress and therefore ineligible for
165    /// planner visibility until a full authoritative rebuild ends.
166    pub(in crate::db) const fn mark_building(&mut self) {
167        self.state = IndexState::Building;
168    }
169
170    /// Mark this index store as fully built and planner-visible again.
171    pub(in crate::db) const fn mark_ready(&mut self) {
172        self.state = IndexState::Ready;
173    }
174
175    /// Mark this index store as dropping and therefore not planner-visible.
176    pub(in crate::db) const fn mark_dropping(&mut self) {
177        self.state = IndexState::Dropping;
178    }
179
180    pub(crate) fn insert(
181        &mut self,
182        key: RawIndexStoreKey,
183        entry: IndexEntryValue,
184    ) -> Option<IndexEntryValue> {
185        let previous = match &mut self.backend {
186            IndexStoreBackend::Stable(map) => map.insert(key, entry),
187            IndexStoreBackend::Heap(map) => map.insert(key, entry),
188        };
189        self.bump_generation();
190        previous
191    }
192
193    pub(crate) fn remove(&mut self, key: &RawIndexStoreKey) -> Option<IndexEntryValue> {
194        let previous = match &mut self.backend {
195            IndexStoreBackend::Stable(map) => map.remove(key),
196            IndexStoreBackend::Heap(map) => map.remove(key),
197        };
198        self.bump_generation();
199        previous
200    }
201
202    pub fn clear(&mut self) {
203        match &mut self.backend {
204            IndexStoreBackend::Stable(map) => map.clear_new(),
205            IndexStoreBackend::Heap(map) => map.clear(),
206        }
207        self.bump_generation();
208    }
209
210    /// Sum of bytes used by all stored index entries.
211    pub fn memory_bytes(&self) -> u64 {
212        let mut bytes = 0u64;
213        let _: Result<(), std::convert::Infallible> = self.visit_entries(|key, value| {
214            bytes = bytes.saturating_add(key.as_bytes().len() as u64 + value.len() as u64);
215            Ok(IndexStoreVisit::Continue)
216        });
217        bytes
218    }
219
220    const fn bump_generation(&mut self) {
221        self.generation = self.generation.saturating_add(1);
222    }
223}