Skip to main content

icydb_core/db/index/store/
mod.rs

1mod fingerprint_debug;
2mod lookup;
3
4use crate::{
5    db::index::{
6        entry::{MAX_INDEX_ENTRY_BYTES, RawIndexEntry},
7        key::RawIndexKey,
8    },
9    traits::Storable,
10};
11use canic_cdk::structures::{BTreeMap, DefaultMemoryImpl, memory::VirtualMemory, storable::Bound};
12use canic_utils::hash::Xxh3;
13use std::borrow::Cow;
14
15/*
16Index Fingerprints — Design Contract (0.7)
17
18Fingerprints are *non-authoritative diagnostic witnesses* stored alongside
19index entries. They exist solely to detect divergence during development.
20
21Authoritative correctness comes from:
22- Stored index entries
23- Decoded row data
24- Commit/recovery replay
25
26Key properties:
27- Fingerprints are written and removed in lockstep with index entries.
28- Release builds do not read or validate fingerprints.
29- Debug builds verify fingerprints opportunistically and panic on mismatch.
30- These panics are intentional debug-time invariant sentinels only.
31- Divergence is detectable, not repaired.
32- Rebuild is the migration boundary for fingerprint format changes.
33
34This file intentionally does *not* attempt healing, validation in release,
35or correctness enforcement via fingerprints.
36*/
37
38///
39/// RawIndexFingerprint
40/// Raw, fixed-size fingerprint bytes stored alongside index entries.
41///
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub(crate) struct RawIndexFingerprint([u8; 16]);
45
46impl RawIndexFingerprint {
47    pub(crate) const STORED_SIZE: u32 = 16;
48}
49
50impl Storable for RawIndexFingerprint {
51    fn to_bytes(&self) -> Cow<'_, [u8]> {
52        Cow::Borrowed(&self.0)
53    }
54
55    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
56        let mut out = [0u8; 16];
57        if bytes.len() == out.len() {
58            out.copy_from_slice(bytes.as_ref());
59        }
60        Self(out)
61    }
62
63    fn into_bytes(self) -> Vec<u8> {
64        self.0.to_vec()
65    }
66
67    const BOUND: Bound = Bound::Bounded {
68        max_size: Self::STORED_SIZE,
69        is_fixed_size: true,
70    };
71}
72
73///
74/// InlineIndexValue
75/// Raw entry plus a non-authoritative debug fingerprint in one stored value.
76/// Encoded as: `[RawIndexEntry bytes | 16-byte fingerprint]`.
77///
78
79#[derive(Clone, Debug)]
80struct InlineIndexValue {
81    entry: RawIndexEntry,
82    fingerprint: RawIndexFingerprint,
83}
84
85impl InlineIndexValue {
86    const STORED_SIZE: u32 = MAX_INDEX_ENTRY_BYTES + RawIndexFingerprint::STORED_SIZE;
87}
88
89impl Storable for InlineIndexValue {
90    fn to_bytes(&self) -> Cow<'_, [u8]> {
91        Cow::Owned(self.clone().into_bytes())
92    }
93
94    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
95        let bytes = bytes.as_ref();
96        let (entry_bytes, fingerprint_bytes) =
97            if bytes.len() < RawIndexFingerprint::STORED_SIZE as usize {
98                (bytes, &[][..])
99            } else {
100                bytes.split_at(bytes.len() - RawIndexFingerprint::STORED_SIZE as usize)
101            };
102
103        let mut out = [0u8; 16];
104        if fingerprint_bytes.len() == out.len() {
105            out.copy_from_slice(fingerprint_bytes);
106        }
107
108        Self {
109            entry: RawIndexEntry::from_bytes(Cow::Borrowed(entry_bytes)),
110            fingerprint: RawIndexFingerprint(out),
111        }
112    }
113
114    fn into_bytes(self) -> Vec<u8> {
115        let mut bytes = self.entry.into_bytes();
116        bytes.extend_from_slice(&self.fingerprint.0);
117        bytes
118    }
119
120    const BOUND: Bound = Bound::Bounded {
121        max_size: Self::STORED_SIZE,
122        is_fixed_size: false,
123    };
124}
125
126///
127/// IndexStore
128///
129
130pub struct IndexStore {
131    entry: VirtualMemory<DefaultMemoryImpl>,
132}
133
134impl IndexStore {
135    #[must_use]
136    pub const fn init(entry: VirtualMemory<DefaultMemoryImpl>) -> Self {
137        Self { entry }
138    }
139
140    /// Snapshot all index entry pairs (diagnostics only).
141    pub(crate) fn entries(&self) -> Vec<(RawIndexKey, RawIndexEntry)> {
142        self.entry_map()
143            .iter()
144            .map(|entry| (entry.key().clone(), entry.value().entry))
145            .collect()
146    }
147
148    pub fn len(&self) -> u64 {
149        self.entry_map().len()
150    }
151
152    pub fn is_empty(&self) -> bool {
153        self.entry_map().is_empty()
154    }
155
156    pub(crate) fn get(&self, key: &RawIndexKey) -> Option<RawIndexEntry> {
157        let value = self.entry_map().get(key);
158
159        // Debug-only verification: fingerprints are non-authoritative and
160        // checked only to surface divergence during development.
161        #[cfg(debug_assertions)]
162        if let Some(ref inline) = value
163            && let Err(err) = Self::verify_entry_fingerprint(None, key, inline)
164        {
165            panic!(
166                "invariant violation (debug-only): index fingerprint verification failed: {err}"
167            );
168        }
169
170        value.map(|inline| inline.entry)
171    }
172
173    pub(crate) fn insert(&self, key: RawIndexKey, value: RawIndexEntry) -> Option<RawIndexEntry> {
174        let fingerprint = Self::entry_fingerprint(&key, &value);
175        let inline = InlineIndexValue {
176            entry: value,
177            fingerprint,
178        };
179        self.entry_map().insert(key, inline).map(|prev| prev.entry)
180    }
181
182    pub(crate) fn remove(&self, key: &RawIndexKey) -> Option<RawIndexEntry> {
183        self.entry_map().remove(key).map(|prev| prev.entry)
184    }
185
186    pub fn clear(&mut self) {
187        self.entry_map().clear();
188    }
189
190    pub fn memory_bytes(&self) -> u64 {
191        self.entry_map()
192            .iter()
193            .map(|entry| {
194                let value: InlineIndexValue = entry.value();
195                entry.key().as_bytes().len() as u64
196                    + value.entry.len() as u64
197                    + u64::from(RawIndexFingerprint::STORED_SIZE)
198            })
199            .sum::<u64>()
200    }
201
202    fn entry_map(
203        &self,
204    ) -> BTreeMap<RawIndexKey, InlineIndexValue, VirtualMemory<DefaultMemoryImpl>> {
205        BTreeMap::init(self.entry.clone())
206    }
207
208    fn entry_fingerprint(key: &RawIndexKey, entry: &RawIndexEntry) -> RawIndexFingerprint {
209        const VERSION: u8 = 1;
210
211        let mut hasher = Xxh3::with_seed(0);
212        hasher.update(&[VERSION]);
213        hasher.update(key.as_bytes());
214        hasher.update(entry.as_bytes());
215
216        RawIndexFingerprint(hasher.digest128().to_be_bytes())
217    }
218}