Skip to main content

icydb_core/db/index/
store.rs

1//! Module: index::store
2//! Responsibility: stable-or-heap index-entry storage behind the index-store boundary.
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    /// Visit all index entries in canonical store order without exposing the
101    /// backing stable-map iterator.
102    pub(in crate::db) fn visit_entries<E>(
103        &self,
104        mut visitor: impl FnMut(&RawIndexStoreKey, &IndexEntryValue) -> Result<IndexStoreVisit, E>,
105    ) -> Result<(), E> {
106        match &self.backend {
107            IndexStoreBackend::Stable(map) => {
108                for entry in map.iter() {
109                    if visitor(entry.key(), &entry.value())?.should_stop() {
110                        return Ok(());
111                    }
112                }
113            }
114            IndexStoreBackend::Heap(map) => {
115                for (key, value) in map {
116                    if visitor(key, value)?.should_stop() {
117                        return Ok(());
118                    }
119                }
120            }
121        }
122
123        Ok(())
124    }
125
126    pub(in crate::db) fn get(&self, key: &RawIndexStoreKey) -> Option<IndexEntryValue> {
127        match &self.backend {
128            IndexStoreBackend::Stable(map) => map.get(key),
129            IndexStoreBackend::Heap(map) => map.get(key).cloned(),
130        }
131    }
132
133    pub fn len(&self) -> u64 {
134        match &self.backend {
135            IndexStoreBackend::Stable(map) => map.len(),
136            IndexStoreBackend::Heap(map) => u64::try_from(map.len()).unwrap_or(u64::MAX),
137        }
138    }
139
140    pub fn is_empty(&self) -> bool {
141        match &self.backend {
142            IndexStoreBackend::Stable(map) => map.is_empty(),
143            IndexStoreBackend::Heap(map) => map.is_empty(),
144        }
145    }
146
147    #[must_use]
148    pub(in crate::db) const fn generation(&self) -> u64 {
149        self.generation
150    }
151
152    /// Return the explicit lifecycle state for this index store.
153    #[must_use]
154    pub(in crate::db) const fn state(&self) -> IndexState {
155        self.state
156    }
157
158    /// Mark this index store as in-progress and therefore ineligible for
159    /// planner visibility until a full authoritative rebuild ends.
160    pub(in crate::db) const fn mark_building(&mut self) {
161        self.state = IndexState::Building;
162    }
163
164    /// Mark this index store as fully built and planner-visible again.
165    pub(in crate::db) const fn mark_ready(&mut self) {
166        self.state = IndexState::Ready;
167    }
168
169    /// Mark this index store as dropping and therefore not planner-visible.
170    pub(in crate::db) const fn mark_dropping(&mut self) {
171        self.state = IndexState::Dropping;
172    }
173
174    pub(crate) fn insert(
175        &mut self,
176        key: RawIndexStoreKey,
177        entry: IndexEntryValue,
178    ) -> Option<IndexEntryValue> {
179        let previous = match &mut self.backend {
180            IndexStoreBackend::Stable(map) => map.insert(key, entry),
181            IndexStoreBackend::Heap(map) => map.insert(key, entry),
182        };
183        self.bump_generation();
184        previous
185    }
186
187    pub(crate) fn remove(&mut self, key: &RawIndexStoreKey) -> Option<IndexEntryValue> {
188        let previous = match &mut self.backend {
189            IndexStoreBackend::Stable(map) => map.remove(key),
190            IndexStoreBackend::Heap(map) => map.remove(key),
191        };
192        self.bump_generation();
193        previous
194    }
195
196    pub fn clear(&mut self) {
197        match &mut self.backend {
198            IndexStoreBackend::Stable(map) => map.clear_new(),
199            IndexStoreBackend::Heap(map) => map.clear(),
200        }
201        self.bump_generation();
202    }
203
204    /// Sum of bytes used by all stored index entries.
205    pub fn memory_bytes(&self) -> u64 {
206        let mut bytes = 0u64;
207        let _: Result<(), std::convert::Infallible> = self.visit_entries(|key, value| {
208            bytes = bytes.saturating_add(key.as_bytes().len() as u64 + value.len() as u64);
209            Ok(IndexStoreVisit::Continue)
210        });
211        bytes
212    }
213
214    const fn bump_generation(&mut self) {
215        self.generation = self.generation.saturating_add(1);
216    }
217}