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