Skip to main content

icydb_core/db/index/
key.rs

1use crate::{
2    MAX_INDEX_FIELDS,
3    db::{
4        identity::{EntityName, EntityNameError, IndexName, IndexNameError},
5        index::fingerprint,
6    },
7    error::{ErrorClass, ErrorOrigin, InternalError},
8    prelude::{EntityKind, IndexModel},
9    traits::Storable,
10};
11use canic_cdk::structures::storable::Bound;
12use derive_more::Display;
13use std::borrow::Cow;
14use thiserror::Error as ThisError;
15
16///
17/// IndexId
18///
19/// Logical identifier for an index.
20/// Combines entity identity and indexed field set into a stable, ordered name.
21/// Used as the prefix component of all index keys.
22///
23
24#[derive(Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
25pub struct IndexId(pub IndexName);
26
27impl IndexId {
28    /// Build an index id from static entity metadata.
29    pub fn try_new<E: EntityKind>(index: &IndexModel) -> Result<Self, IndexIdError> {
30        let entity = EntityName::try_from_static(E::ENTITY_NAME)?;
31        let name = IndexName::try_from_parts(&entity, index.fields)?;
32        Ok(Self(name))
33    }
34
35    /// Build an index id from validated static metadata.
36    ///
37    /// Caller must uphold identity invariants; intended for generated code.
38    #[must_use]
39    pub(crate) fn new_unchecked<E: EntityKind>(index: &IndexModel) -> Self {
40        let entity = EntityName::from_static_unchecked(E::ENTITY_NAME);
41        Self(IndexName::from_parts_unchecked(&entity, index.fields))
42    }
43
44    /// Maximum sentinel value for stable-memory bounds.
45    /// Used for upper-bound scans and fuzz validation.
46    #[must_use]
47    pub const fn max_storable() -> Self {
48        Self(IndexName::max_storable())
49    }
50}
51
52///
53/// IndexIdError
54/// Errors returned when constructing an [`IndexId`].
55/// This surfaces identity validation failures.
56///
57
58#[derive(Debug, ThisError)]
59pub enum IndexIdError {
60    #[error("entity name invalid: {0}")]
61    EntityName(#[from] EntityNameError),
62    #[error("index name invalid: {0}")]
63    IndexName(#[from] IndexNameError),
64}
65
66///
67/// IndexKey
68///
69/// Fully-qualified index lookup key.
70/// Fixed-size, manually encoded structure designed for stable-memory ordering.
71/// Ordering of this type must exactly match byte-level ordering.
72///
73
74#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
75pub struct IndexKey {
76    index_id: IndexId,
77    len: u8,
78    values: [[u8; 16]; MAX_INDEX_FIELDS],
79}
80
81impl IndexKey {
82    #[allow(clippy::cast_possible_truncation)]
83    pub const STORED_SIZE: u32 = IndexName::STORED_SIZE + 1 + (MAX_INDEX_FIELDS as u32 * 16);
84
85    /// Build an index key; returns `Ok(None)` if any indexed field is missing or non-indexable.
86    /// `Value::None` and `Value::Unsupported` are treated as non-indexable.
87    pub fn new<E: EntityKind>(
88        entity: &E,
89        index: &IndexModel,
90    ) -> Result<Option<Self>, InternalError> {
91        if index.fields.len() > MAX_INDEX_FIELDS {
92            return Err(InternalError::new(
93                ErrorClass::InvariantViolation,
94                ErrorOrigin::Index,
95                format!(
96                    "index '{}' has {} fields (max {})",
97                    index.name,
98                    index.fields.len(),
99                    MAX_INDEX_FIELDS
100                ),
101            ));
102        }
103
104        let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
105        let mut len = 0usize;
106
107        for field in index.fields {
108            let Some(value) = entity.get_value(field) else {
109                return Ok(None);
110            };
111            let Some(fp) = fingerprint::to_index_fingerprint(&value)? else {
112                return Ok(None);
113            };
114            values[len] = fp;
115            len += 1;
116        }
117
118        #[allow(clippy::cast_possible_truncation)]
119        Ok(Some(Self {
120            index_id: IndexId::new_unchecked::<E>(index),
121            len: len as u8,
122            values,
123        }))
124    }
125
126    #[must_use]
127    pub const fn empty(index_id: IndexId) -> Self {
128        Self {
129            index_id,
130            len: 0,
131            values: [[0u8; 16]; MAX_INDEX_FIELDS],
132        }
133    }
134
135    #[must_use]
136    #[allow(clippy::cast_possible_truncation)]
137    pub fn bounds_for_prefix(
138        index_id: IndexId,
139        index_len: usize,
140        prefix: &[[u8; 16]],
141    ) -> (Self, Self) {
142        let mut start = Self::empty(index_id);
143        let mut end = Self::empty(index_id);
144
145        for (i, fp) in prefix.iter().enumerate() {
146            start.values[i] = *fp;
147            end.values[i] = *fp;
148        }
149
150        start.len = index_len as u8;
151        end.len = start.len;
152
153        for value in end.values.iter_mut().take(index_len).skip(prefix.len()) {
154            *value = [0xFF; 16];
155        }
156
157        (start, end)
158    }
159
160    #[must_use]
161    pub fn to_raw(&self) -> RawIndexKey {
162        let mut buf = [0u8; Self::STORED_SIZE as usize];
163
164        let name_bytes = self.index_id.0.to_bytes();
165        buf[..name_bytes.len()].copy_from_slice(&name_bytes);
166
167        let mut offset = IndexName::STORED_SIZE as usize;
168        buf[offset] = self.len;
169        offset += 1;
170
171        for value in &self.values {
172            buf[offset..offset + 16].copy_from_slice(value);
173            offset += 16;
174        }
175
176        RawIndexKey(buf)
177    }
178
179    pub fn try_from_raw(raw: &RawIndexKey) -> Result<Self, &'static str> {
180        let bytes = &raw.0;
181        if bytes.len() != Self::STORED_SIZE as usize {
182            return Err("corrupted IndexKey: invalid size");
183        }
184
185        let mut offset = 0;
186
187        let index_name =
188            IndexName::from_bytes(&bytes[offset..offset + IndexName::STORED_SIZE as usize])
189                .map_err(|_| "corrupted IndexKey: invalid IndexName bytes")?;
190        offset += IndexName::STORED_SIZE as usize;
191
192        let len = bytes[offset];
193        offset += 1;
194
195        if len as usize > MAX_INDEX_FIELDS {
196            return Err("corrupted IndexKey: invalid index length");
197        }
198
199        let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
200        for value in &mut values {
201            value.copy_from_slice(&bytes[offset..offset + 16]);
202            offset += 16;
203        }
204
205        let len_usize = len as usize;
206        for value in values.iter().skip(len_usize) {
207            if value.iter().any(|&b| b != 0) {
208                return Err("corrupted IndexKey: non-zero fingerprint padding");
209            }
210        }
211
212        Ok(Self {
213            index_id: IndexId(index_name),
214            len,
215            values,
216        })
217    }
218}
219
220///
221/// RawIndexKey
222///
223/// Fixed-size, stable-memory representation of IndexKey.
224/// This is the form stored in BTreeMap keys.
225///
226
227#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
228pub struct RawIndexKey([u8; IndexKey::STORED_SIZE as usize]);
229
230impl RawIndexKey {
231    /// Borrow the raw byte representation.
232    #[must_use]
233    pub const fn as_bytes(&self) -> &[u8; IndexKey::STORED_SIZE as usize] {
234        &self.0
235    }
236}
237
238impl Storable for RawIndexKey {
239    fn to_bytes(&self) -> Cow<'_, [u8]> {
240        Cow::Borrowed(&self.0)
241    }
242
243    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
244        let mut out = [0u8; IndexKey::STORED_SIZE as usize];
245        if bytes.len() == out.len() {
246            out.copy_from_slice(bytes.as_ref());
247        }
248        Self(out)
249    }
250
251    fn into_bytes(self) -> Vec<u8> {
252        self.0.to_vec()
253    }
254
255    const BOUND: Bound = Bound::Bounded {
256        max_size: IndexKey::STORED_SIZE,
257        is_fixed_size: true,
258    };
259}