icydb_core/db/index/
key.rs

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