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 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#[derive(Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
25pub struct IndexId(pub IndexName);
26
27impl IndexId {
28 pub fn try_new<E: EntityKind>(index: &IndexModel) -> Result<Self, IndexIdError> {
32 let entity = EntityName::try_from_str(E::ENTITY_NAME)?;
33 let name = IndexName::try_from_parts(&entity, index.fields)?;
34 Ok(Self(name))
35 }
36
37 #[must_use]
41 pub fn new<E: EntityKind>(index: &IndexModel) -> Self {
42 Self::try_new::<E>(index).expect("static IndexModel must define a valid IndexId")
43 }
44
45 #[must_use]
48 pub const fn max_storable() -> Self {
49 Self(IndexName::max_storable())
50 }
51}
52
53#[derive(Debug, ThisError)]
60pub enum IndexIdError {
61 #[error("entity name invalid: {0}")]
62 EntityName(#[from] EntityNameError),
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>(
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 values[len] = fp;
121 len += 1;
122 }
123
124 #[allow(clippy::cast_possible_truncation)]
125 Ok(Some(Self {
126 index_id: IndexId::new::<E>(index),
127 len: len as u8,
128 values,
129 }))
130 }
131
132 #[must_use]
133 pub const fn empty(index_id: IndexId) -> Self {
134 Self {
135 index_id,
136 len: 0,
137 values: [[0u8; 16]; MAX_INDEX_FIELDS],
138 }
139 }
140
141 #[must_use]
142 #[allow(clippy::cast_possible_truncation)]
143 pub fn bounds_for_prefix(
144 index_id: IndexId,
145 index_len: usize,
146 prefix: &[[u8; 16]],
147 ) -> (Self, Self) {
148 let mut start = Self::empty(index_id);
149 let mut end = Self::empty(index_id);
150
151 for (i, fp) in prefix.iter().enumerate() {
152 start.values[i] = *fp;
153 end.values[i] = *fp;
154 }
155
156 start.len = index_len as u8;
157 end.len = start.len;
158
159 for value in end.values.iter_mut().take(index_len).skip(prefix.len()) {
160 *value = [0xFF; 16];
161 }
162
163 (start, end)
164 }
165
166 #[must_use]
167 pub fn to_raw(&self) -> RawIndexKey {
168 let mut buf = [0u8; Self::STORED_SIZE_USIZE];
169
170 let name_bytes = self.index_id.0.to_bytes();
171 buf[..name_bytes.len()].copy_from_slice(&name_bytes);
172
173 let mut offset = IndexName::STORED_SIZE_USIZE;
174 buf[offset] = self.len;
175 offset += 1;
176
177 for value in &self.values {
178 buf[offset..offset + 16].copy_from_slice(value);
179 offset += 16;
180 }
181
182 RawIndexKey(buf)
183 }
184
185 pub fn try_from_raw(raw: &RawIndexKey) -> Result<Self, &'static str> {
186 let bytes = &raw.0;
187 if bytes.len() != Self::STORED_SIZE_USIZE {
188 return Err("corrupted IndexKey: invalid size");
189 }
190
191 let mut offset = 0;
192
193 let index_name =
194 IndexName::from_bytes(&bytes[offset..offset + IndexName::STORED_SIZE_USIZE])
195 .map_err(|_| "corrupted IndexKey: invalid IndexName bytes")?;
196 offset += IndexName::STORED_SIZE_USIZE;
197
198 let len = bytes[offset];
199 offset += 1;
200
201 if len as usize > MAX_INDEX_FIELDS {
202 return Err("corrupted IndexKey: invalid index length");
203 }
204
205 let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
206 for value in &mut values {
207 value.copy_from_slice(&bytes[offset..offset + 16]);
208 offset += 16;
209 }
210
211 let len_usize = len as usize;
212 for value in values.iter().skip(len_usize) {
213 if value.iter().any(|&b| b != 0) {
214 return Err("corrupted IndexKey: non-zero fingerprint padding");
215 }
216 }
217
218 Ok(Self {
219 index_id: IndexId(index_name),
220 len,
221 values,
222 })
223 }
224}
225
226#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
234pub struct RawIndexKey([u8; IndexKey::STORED_SIZE_USIZE]);
235
236impl RawIndexKey {
237 #[must_use]
239 pub const fn as_bytes(&self) -> &[u8; IndexKey::STORED_SIZE_USIZE] {
240 &self.0
241 }
242}
243
244impl Storable for RawIndexKey {
245 fn to_bytes(&self) -> Cow<'_, [u8]> {
246 Cow::Borrowed(&self.0)
247 }
248
249 fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
250 let mut out = [0u8; IndexKey::STORED_SIZE_USIZE];
251 if bytes.len() == out.len() {
252 out.copy_from_slice(bytes.as_ref());
253 }
254 Self(out)
255 }
256
257 fn into_bytes(self) -> Vec<u8> {
258 self.0.to_vec()
259 }
260
261 #[expect(clippy::cast_possible_truncation)]
262 const BOUND: Bound = Bound::Bounded {
263 max_size: IndexKey::STORED_SIZE_BYTES as u32,
264 is_fixed_size: true,
265 };
266}