icydb_core/db/index/
key.rs1use crate::{
2 MAX_INDEX_FIELDS,
3 db::{
4 identity::{EntityName, EntityNameError, IndexName, IndexNameError},
5 index::fingerprint,
6 },
7 error::{ErrorClass, ErrorOrigin, InternalError},
8 model::index::IndexModel,
9 traits::{EntityKind, EntityValue, Storable},
10};
11use canic_cdk::structures::storable::Bound;
12use derive_more::Display;
13use std::borrow::Cow;
14use thiserror::Error as ThisError;
15
16#[derive(Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
25pub struct IndexId(pub IndexName);
26
27impl IndexId {
28 #[must_use]
34 pub fn new<E: EntityKind>(index: &IndexModel) -> Self {
35 let entity = EntityName::try_from_str(E::ENTITY_NAME)
36 .expect("EntityKind::ENTITY_NAME must be a valid EntityName");
37
38 let name = IndexName::try_from_parts(&entity, index.fields)
39 .expect("IndexModel must define a valid IndexName");
40
41 Self(name)
42 }
43
44 #[must_use]
47 pub const fn max_storable() -> Self {
48 Self(IndexName::max_storable())
49 }
50}
51
52#[derive(Debug, ThisError)]
59pub enum IndexIdError {
60 #[error("entity name invalid: {0}")]
61 EntityName(#[from] EntityNameError),
62
63 #[error("index name invalid: {0}")]
64 IndexName(#[from] IndexNameError),
65}
66
67#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
76pub struct IndexKey {
77 index_id: IndexId,
78 len: u8,
79 values: [[u8; 16]; MAX_INDEX_FIELDS],
80}
81
82#[expect(clippy::cast_possible_truncation)]
83impl IndexKey {
84 pub const STORED_SIZE_BYTES: u64 =
86 IndexName::STORED_SIZE_BYTES + 1 + (MAX_INDEX_FIELDS as u64 * 16);
87
88 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
90
91 pub fn new<E: EntityKind + EntityValue>(
94 entity: &E,
95 index: &IndexModel,
96 ) -> Result<Option<Self>, InternalError> {
97 if index.fields.len() > MAX_INDEX_FIELDS {
98 return Err(InternalError::new(
99 ErrorClass::InvariantViolation,
100 ErrorOrigin::Index,
101 format!(
102 "index '{}' has {} fields (max {})",
103 index.name,
104 index.fields.len(),
105 MAX_INDEX_FIELDS
106 ),
107 ));
108 }
109
110 let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
111 let mut len = 0usize;
112
113 for field in index.fields {
114 let Some(value) = entity.get_value(field) else {
115 return Ok(None);
116 };
117 let Some(fp) = fingerprint::to_index_fingerprint(&value)? else {
118 return Ok(None);
119 };
120
121 values[len] = fp;
122 len += 1;
123 }
124
125 #[allow(clippy::cast_possible_truncation)]
126 Ok(Some(Self {
127 index_id: IndexId::new::<E>(index),
128 len: len as u8,
129 values,
130 }))
131 }
132
133 #[must_use]
134 pub const fn empty(index_id: IndexId) -> Self {
135 Self {
136 index_id,
137 len: 0,
138 values: [[0u8; 16]; MAX_INDEX_FIELDS],
139 }
140 }
141
142 #[must_use]
143 #[allow(clippy::cast_possible_truncation)]
144 pub fn bounds_for_prefix(
145 index_id: IndexId,
146 index_len: usize,
147 prefix: &[[u8; 16]],
148 ) -> (Self, Self) {
149 let mut start = Self::empty(index_id);
150 let mut end = Self::empty(index_id);
151
152 for (i, fp) in prefix.iter().enumerate() {
153 start.values[i] = *fp;
154 end.values[i] = *fp;
155 }
156
157 start.len = index_len as u8;
158 end.len = start.len;
159
160 for value in end.values.iter_mut().take(index_len).skip(prefix.len()) {
161 *value = [0xFF; 16];
162 }
163
164 (start, end)
165 }
166
167 #[must_use]
168 pub fn to_raw(&self) -> RawIndexKey {
169 let mut buf = [0u8; Self::STORED_SIZE_USIZE];
170
171 let name_bytes = self.index_id.0.to_bytes();
172 buf[..name_bytes.len()].copy_from_slice(&name_bytes);
173
174 let mut offset = IndexName::STORED_SIZE_USIZE;
175 buf[offset] = self.len;
176 offset += 1;
177
178 for value in &self.values {
179 buf[offset..offset + 16].copy_from_slice(value);
180 offset += 16;
181 }
182
183 RawIndexKey(buf)
184 }
185
186 pub fn try_from_raw(raw: &RawIndexKey) -> Result<Self, &'static str> {
187 let bytes = &raw.0;
188 if bytes.len() != Self::STORED_SIZE_USIZE {
189 return Err("corrupted IndexKey: invalid size");
190 }
191
192 let mut offset = 0;
193
194 let index_name =
195 IndexName::from_bytes(&bytes[offset..offset + IndexName::STORED_SIZE_USIZE])
196 .map_err(|_| "corrupted IndexKey: invalid IndexName bytes")?;
197 offset += IndexName::STORED_SIZE_USIZE;
198
199 let len = bytes[offset];
200 offset += 1;
201
202 if len as usize > MAX_INDEX_FIELDS {
203 return Err("corrupted IndexKey: invalid index length");
204 }
205
206 let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
207 for value in &mut values {
208 value.copy_from_slice(&bytes[offset..offset + 16]);
209 offset += 16;
210 }
211
212 let len_usize = len as usize;
213 for value in values.iter().skip(len_usize) {
214 if value.iter().any(|&b| b != 0) {
215 return Err("corrupted IndexKey: non-zero fingerprint padding");
216 }
217 }
218
219 Ok(Self {
220 index_id: IndexId(index_name),
221 len,
222 values,
223 })
224 }
225}
226
227#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
235pub struct RawIndexKey([u8; IndexKey::STORED_SIZE_USIZE]);
236
237impl RawIndexKey {
238 #[must_use]
240 pub const fn as_bytes(&self) -> &[u8; IndexKey::STORED_SIZE_USIZE] {
241 &self.0
242 }
243}
244
245impl Storable for RawIndexKey {
246 fn to_bytes(&self) -> Cow<'_, [u8]> {
247 Cow::Borrowed(&self.0)
248 }
249
250 fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
251 let mut out = [0u8; IndexKey::STORED_SIZE_USIZE];
252 if bytes.len() == out.len() {
253 out.copy_from_slice(bytes.as_ref());
254 }
255 Self(out)
256 }
257
258 fn into_bytes(self) -> Vec<u8> {
259 self.0.to_vec()
260 }
261
262 #[expect(clippy::cast_possible_truncation)]
263 const BOUND: Bound = Bound::Bounded {
264 max_size: IndexKey::STORED_SIZE_BYTES as u32,
265 is_fixed_size: true,
266 };
267}