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::{
7    db::{
8        data::StorageKey,
9        index::{entry::RawIndexEntry, key::RawIndexKey},
10    },
11    error::InternalError,
12};
13
14use candid::CandidType;
15use canic_cdk::structures::{BTreeMap, DefaultMemoryImpl, memory::VirtualMemory};
16use serde::Deserialize;
17
18//
19// IndexState
20//
21// Explicit lifecycle visibility state for one index store.
22// Visibility matters because planner-visible indexes must already be complete:
23// the index contents are fully built and query-visible for reads.
24//
25#[derive(CandidType, Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
26pub enum IndexState {
27    Building,
28    #[default]
29    Ready,
30    Dropping,
31}
32
33impl IndexState {
34    /// Return the stable lowercase text label for this lifecycle state.
35    #[must_use]
36    pub const fn as_str(self) -> &'static str {
37        match self {
38            Self::Building => "building",
39            Self::Ready => "ready",
40            Self::Dropping => "dropping",
41        }
42    }
43}
44
45///
46/// IndexStore
47///
48/// Thin persistence wrapper over one stable BTreeMap.
49///
50/// Invariant: callers provide already-validated `RawIndexKey`/`RawIndexEntry`.
51///
52
53pub struct IndexStore {
54    pub(super) map: BTreeMap<RawIndexKey, RawIndexEntry, VirtualMemory<DefaultMemoryImpl>>,
55    generation: u64,
56    state: IndexState,
57}
58
59impl IndexStore {
60    #[must_use]
61    pub fn init(memory: VirtualMemory<DefaultMemoryImpl>) -> Self {
62        Self {
63            map: BTreeMap::init(memory),
64            generation: 0,
65            // Existing stores default to Ready until one explicit build/drop
66            // lifecycle is introduced.
67            state: IndexState::Ready,
68        }
69    }
70
71    /// Snapshot all index entry pairs (diagnostics only).
72    #[expect(clippy::redundant_closure_for_method_calls)]
73    pub(crate) fn entries(&self) -> Vec<(RawIndexKey, RawIndexEntry)> {
74        self.map.iter().map(|entry| entry.into_pair()).collect()
75    }
76
77    pub(in crate::db) fn get(&self, key: &RawIndexKey) -> Option<RawIndexEntry> {
78        self.map.get(key)
79    }
80
81    pub fn len(&self) -> u64 {
82        self.map.len()
83    }
84
85    pub fn is_empty(&self) -> bool {
86        self.map.is_empty()
87    }
88
89    #[must_use]
90    pub(in crate::db) const fn generation(&self) -> u64 {
91        self.generation
92    }
93
94    /// Return the explicit lifecycle state for this index store.
95    #[must_use]
96    pub(in crate::db) const fn state(&self) -> IndexState {
97        self.state
98    }
99
100    /// Mark this index store as in-progress and therefore ineligible for
101    /// planner visibility until a full authoritative rebuild ends.
102    pub(in crate::db) const fn mark_building(&mut self) {
103        self.state = IndexState::Building;
104    }
105
106    /// Mark this index store as fully built and planner-visible again.
107    pub(in crate::db) const fn mark_ready(&mut self) {
108        self.state = IndexState::Ready;
109    }
110
111    /// Mark this index store as dropping and therefore not planner-visible.
112    pub(in crate::db) const fn mark_dropping(&mut self) {
113        self.state = IndexState::Dropping;
114    }
115
116    pub(crate) fn insert(
117        &mut self,
118        key: RawIndexKey,
119        entry: RawIndexEntry,
120    ) -> Option<RawIndexEntry> {
121        let previous = self.map.insert(key, entry);
122        self.bump_generation();
123        previous
124    }
125
126    pub(crate) fn remove(&mut self, key: &RawIndexKey) -> Option<RawIndexEntry> {
127        let previous = self.map.remove(key);
128        self.bump_generation();
129        previous
130    }
131
132    pub fn clear(&mut self) {
133        self.map.clear();
134        self.bump_generation();
135    }
136
137    /// Mark one storage key as missing anywhere it still appears inside this
138    /// secondary index store, while preserving the surrounding entry itself.
139    pub(in crate::db) fn mark_memberships_missing_for_storage_key(
140        &mut self,
141        storage_key: StorageKey,
142    ) -> Result<usize, InternalError> {
143        let mut entries = Vec::new();
144
145        for entry in self.map.iter() {
146            entries.push(entry.into_pair());
147        }
148
149        let mut marked = 0usize;
150
151        for (raw_key, mut raw_entry) in entries {
152            if raw_entry
153                .mark_key_missing(storage_key)
154                .map_err(InternalError::index_entry_decode_failed)?
155            {
156                self.map.insert(raw_key, raw_entry);
157                marked = marked.saturating_add(1);
158            }
159        }
160
161        if marked > 0 {
162            self.bump_generation();
163        }
164
165        Ok(marked)
166    }
167
168    /// Sum of bytes used by all stored index entries.
169    pub fn memory_bytes(&self) -> u64 {
170        self.map
171            .iter()
172            .map(|entry| entry.key().as_bytes().len() as u64 + entry.value().len() as u64)
173            .sum()
174    }
175
176    const fn bump_generation(&mut self) {
177        self.generation = self.generation.saturating_add(1);
178    }
179}