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 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
64 #[error("index name invalid: {0}")]
65 IndexName(#[from] IndexNameError),
66}
67
68#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
77pub struct IndexKey {
78 index_id: IndexId,
79 len: u8,
80 values: [[u8; 16]; MAX_INDEX_FIELDS],
81}
82
83#[expect(clippy::cast_possible_truncation)]
84impl IndexKey {
85 pub const STORED_SIZE_BYTES: u64 =
87 IndexName::STORED_SIZE_BYTES + 1 + (MAX_INDEX_FIELDS as u64 * 16);
88
89 pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
91
92 pub fn new<E: EntityKind + EntityValue>(
95 entity: &E,
96 index: &IndexModel,
97 ) -> Result<Option<Self>, InternalError> {
98 if index.fields.len() > MAX_INDEX_FIELDS {
99 return Err(InternalError::new(
100 ErrorClass::InvariantViolation,
101 ErrorOrigin::Index,
102 format!(
103 "index '{}' has {} fields (max {})",
104 index.name,
105 index.fields.len(),
106 MAX_INDEX_FIELDS
107 ),
108 ));
109 }
110
111 let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
112 let mut len = 0usize;
113
114 for field in index.fields {
115 let Some(value) = entity.get_value(field) else {
116 return Ok(None);
117 };
118 let Some(fp) = fingerprint::to_index_fingerprint(&value)? else {
119 return Ok(None);
120 };
121
122 values[len] = fp;
123 len += 1;
124 }
125
126 #[allow(clippy::cast_possible_truncation)]
127 Ok(Some(Self {
128 index_id: IndexId::new::<E>(index),
129 len: len as u8,
130 values,
131 }))
132 }
133
134 #[must_use]
135 pub const fn empty(index_id: IndexId) -> Self {
136 Self {
137 index_id,
138 len: 0,
139 values: [[0u8; 16]; MAX_INDEX_FIELDS],
140 }
141 }
142
143 #[must_use]
144 #[allow(clippy::cast_possible_truncation)]
145 pub fn bounds_for_prefix(
146 index_id: IndexId,
147 index_len: usize,
148 prefix: &[[u8; 16]],
149 ) -> (Self, Self) {
150 let mut start = Self::empty(index_id);
151 let mut end = Self::empty(index_id);
152
153 for (i, fp) in prefix.iter().enumerate() {
154 start.values[i] = *fp;
155 end.values[i] = *fp;
156 }
157
158 start.len = index_len as u8;
159 end.len = start.len;
160
161 for value in end.values.iter_mut().take(index_len).skip(prefix.len()) {
162 *value = [0xFF; 16];
163 }
164
165 (start, end)
166 }
167
168 #[must_use]
169 pub fn to_raw(&self) -> RawIndexKey {
170 let mut buf = [0u8; Self::STORED_SIZE_USIZE];
171
172 let name_bytes = self.index_id.0.to_bytes();
173 buf[..name_bytes.len()].copy_from_slice(&name_bytes);
174
175 let mut offset = IndexName::STORED_SIZE_USIZE;
176 buf[offset] = self.len;
177 offset += 1;
178
179 for value in &self.values {
180 buf[offset..offset + 16].copy_from_slice(value);
181 offset += 16;
182 }
183
184 RawIndexKey(buf)
185 }
186
187 pub fn try_from_raw(raw: &RawIndexKey) -> Result<Self, &'static str> {
188 let bytes = &raw.0;
189 if bytes.len() != Self::STORED_SIZE_USIZE {
190 return Err("corrupted IndexKey: invalid size");
191 }
192
193 let mut offset = 0;
194
195 let index_name =
196 IndexName::from_bytes(&bytes[offset..offset + IndexName::STORED_SIZE_USIZE])
197 .map_err(|_| "corrupted IndexKey: invalid IndexName bytes")?;
198 offset += IndexName::STORED_SIZE_USIZE;
199
200 let len = bytes[offset];
201 offset += 1;
202
203 if len as usize > MAX_INDEX_FIELDS {
204 return Err("corrupted IndexKey: invalid index length");
205 }
206
207 let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
208 for value in &mut values {
209 value.copy_from_slice(&bytes[offset..offset + 16]);
210 offset += 16;
211 }
212
213 let len_usize = len as usize;
214 for value in values.iter().skip(len_usize) {
215 if value.iter().any(|&b| b != 0) {
216 return Err("corrupted IndexKey: non-zero fingerprint padding");
217 }
218 }
219
220 Ok(Self {
221 index_id: IndexId(index_name),
222 len,
223 values,
224 })
225 }
226}
227
228#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
236pub struct RawIndexKey([u8; IndexKey::STORED_SIZE_USIZE]);
237
238impl RawIndexKey {
239 #[must_use]
241 pub const fn as_bytes(&self) -> &[u8; IndexKey::STORED_SIZE_USIZE] {
242 &self.0
243 }
244}
245
246impl Storable for RawIndexKey {
247 fn to_bytes(&self) -> Cow<'_, [u8]> {
248 Cow::Borrowed(&self.0)
249 }
250
251 fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
252 let mut out = [0u8; IndexKey::STORED_SIZE_USIZE];
253 if bytes.len() == out.len() {
254 out.copy_from_slice(bytes.as_ref());
255 }
256 Self(out)
257 }
258
259 fn into_bytes(self) -> Vec<u8> {
260 self.0.to_vec()
261 }
262
263 #[expect(clippy::cast_possible_truncation)]
264 const BOUND: Bound = Bound::Bounded {
265 max_size: IndexKey::STORED_SIZE_BYTES as u32,
266 is_fixed_size: true,
267 };
268}