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::index::{
8        entry::{MAX_INDEX_ENTRY_BYTES, RawIndexEntry},
9        key::RawIndexKey,
10    },
11    traits::Storable,
12};
13
14use canic_cdk::structures::{
15    BTreeMap, DefaultMemoryImpl, memory::VirtualMemory, storable::Bound as StorableBound,
16};
17use canic_utils::hash::Xxh3;
18use std::borrow::Cow;
19
20///
21/// IndexStore
22///
23/// Thin persistence wrapper over one stable BTreeMap.
24///
25/// Invariant: callers provide already-validated `RawIndexKey`/`RawIndexEntry`.
26/// Fingerprints are diagnostic witnesses and are debug-verified only.
27///
28
29pub struct IndexStore {
30    pub(super) map: BTreeMap<RawIndexKey, StoredIndexValue, VirtualMemory<DefaultMemoryImpl>>,
31    generation: u64,
32}
33
34impl IndexStore {
35    #[must_use]
36    pub fn init(memory: VirtualMemory<DefaultMemoryImpl>) -> Self {
37        Self {
38            map: BTreeMap::init(memory),
39            generation: 0,
40        }
41    }
42
43    /// Snapshot all index entry pairs (diagnostics only).
44    pub(crate) fn entries(&self) -> Vec<(RawIndexKey, RawIndexEntry)> {
45        self.map
46            .iter()
47            .map(|entry| (entry.key().clone(), entry.value().entry))
48            .collect()
49    }
50
51    pub(in crate::db) fn get(&self, key: &RawIndexKey) -> Option<RawIndexEntry> {
52        let value = self.map.get(key);
53
54        #[cfg(debug_assertions)]
55        if let Some(ref stored) = value {
56            Self::verify_if_debug(key, stored);
57        }
58
59        value.map(|stored| stored.entry)
60    }
61
62    pub fn len(&self) -> u64 {
63        self.map.len()
64    }
65
66    pub fn is_empty(&self) -> bool {
67        self.map.is_empty()
68    }
69
70    #[must_use]
71    pub(in crate::db) const fn generation(&self) -> u64 {
72        self.generation
73    }
74
75    pub(crate) fn insert(
76        &mut self,
77        key: RawIndexKey,
78        entry: RawIndexEntry,
79    ) -> Option<RawIndexEntry> {
80        let fingerprint = Self::entry_fingerprint(&key, &entry);
81
82        let stored = StoredIndexValue { entry, fingerprint };
83        let previous = self.map.insert(key, stored).map(|prev| prev.entry);
84        self.bump_generation();
85        previous
86    }
87
88    pub(crate) fn remove(&mut self, key: &RawIndexKey) -> Option<RawIndexEntry> {
89        let previous = self.map.remove(key).map(|prev| prev.entry);
90        self.bump_generation();
91        previous
92    }
93
94    pub fn clear(&mut self) {
95        self.map.clear();
96        self.bump_generation();
97    }
98
99    /// Sum of bytes used by all stored index entries.
100    pub fn memory_bytes(&self) -> u64 {
101        self.map
102            .iter()
103            .map(|entry| {
104                entry.key().as_bytes().len() as u64
105                    + entry.value().entry.len() as u64
106                    + u64::from(RawIndexFingerprint::STORED_SIZE)
107            })
108            .sum()
109    }
110
111    const fn bump_generation(&mut self) {
112        self.generation = self.generation.saturating_add(1);
113    }
114
115    fn entry_fingerprint(key: &RawIndexKey, entry: &RawIndexEntry) -> RawIndexFingerprint {
116        const VERSION: u8 = 1;
117
118        let mut hasher = Xxh3::with_seed(0);
119        hasher.update(&[VERSION]);
120        hasher.update(key.as_bytes());
121        hasher.update(entry.as_bytes());
122
123        RawIndexFingerprint(hasher.digest128().to_be_bytes())
124    }
125
126    #[cfg(debug_assertions)]
127    pub(super) fn verify_if_debug(key: &RawIndexKey, stored: &StoredIndexValue) {
128        let expected = Self::entry_fingerprint(key, &stored.entry);
129
130        debug_assert!(
131            stored.fingerprint == expected,
132            "debug invariant violation: index fingerprint mismatch"
133        );
134    }
135}
136
137///
138/// StoredIndexValue
139///
140/// Raw entry plus non-authoritative diagnostic fingerprint.
141/// Encoded as: [RawIndexEntry bytes | 16-byte fingerprint]
142///
143
144#[derive(Clone, Debug)]
145pub(super) struct StoredIndexValue {
146    pub(super) entry: RawIndexEntry,
147    fingerprint: RawIndexFingerprint,
148}
149
150impl StoredIndexValue {
151    const STORED_SIZE: u32 = MAX_INDEX_ENTRY_BYTES + RawIndexFingerprint::STORED_SIZE;
152}
153
154impl Storable for StoredIndexValue {
155    fn to_bytes(&self) -> Cow<'_, [u8]> {
156        Cow::Owned(self.clone().into_bytes())
157    }
158
159    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
160        let bytes = bytes.as_ref();
161
162        let (entry_bytes, fingerprint_bytes) =
163            if bytes.len() < RawIndexFingerprint::STORED_SIZE as usize {
164                (bytes, &[][..])
165            } else {
166                bytes.split_at(bytes.len() - RawIndexFingerprint::STORED_SIZE as usize)
167            };
168
169        let mut out = [0u8; 16];
170        if fingerprint_bytes.len() == out.len() {
171            out.copy_from_slice(fingerprint_bytes);
172        }
173
174        Self {
175            entry: RawIndexEntry::from_bytes(Cow::Borrowed(entry_bytes)),
176            fingerprint: RawIndexFingerprint(out),
177        }
178    }
179
180    fn into_bytes(self) -> Vec<u8> {
181        let mut bytes = self.entry.into_bytes();
182        bytes.extend_from_slice(&self.fingerprint.0);
183        bytes
184    }
185
186    const BOUND: StorableBound = StorableBound::Bounded {
187        max_size: Self::STORED_SIZE,
188        is_fixed_size: false,
189    };
190}
191
192///
193/// RawIndexFingerprint
194///
195
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197pub(crate) struct RawIndexFingerprint([u8; 16]);
198
199impl RawIndexFingerprint {
200    pub(crate) const STORED_SIZE: u32 = 16;
201}
202
203impl Storable for RawIndexFingerprint {
204    fn to_bytes(&self) -> Cow<'_, [u8]> {
205        Cow::Borrowed(&self.0)
206    }
207
208    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
209        let mut out = [0u8; 16];
210        if bytes.len() == out.len() {
211            out.copy_from_slice(bytes.as_ref());
212        }
213        Self(out)
214    }
215
216    fn into_bytes(self) -> Vec<u8> {
217        self.0.to_vec()
218    }
219
220    const BOUND: StorableBound = StorableBound::Bounded {
221        max_size: Self::STORED_SIZE,
222        is_fixed_size: true,
223    };
224}