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> {
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 #[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 #[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 #[error("index name invalid: {0}")]
63 IndexName(#[from] IndexNameError),
64}
65
66#[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 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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
228pub struct RawIndexKey([u8; IndexKey::STORED_SIZE as usize]);
229
230impl RawIndexKey {
231 #[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}