Skip to main content

icydb_core/db/store/
data_key.rs

1#![expect(clippy::cast_possible_truncation)]
2use crate::{
3    db::{
4        identity::{EntityName, IdentityDecodeError},
5        store::storage_key::{StorageKey, StorageKeyEncodeError},
6    },
7    error::{ErrorClass, ErrorOrigin, InternalError},
8    traits::{EntityKind, FieldValue, Storable},
9};
10use canic_cdk::structures::storable::Bound;
11use std::{
12    borrow::Cow,
13    fmt::{self, Display},
14};
15use thiserror::Error as ThisError;
16
17///
18/// DataKeyEncodeError
19/// (serialize boundary)
20///
21
22#[derive(Debug, ThisError)]
23pub enum DataKeyEncodeError {
24    #[error("data key encoding failed for {key}: {source}")]
25    KeyEncoding {
26        key: DataKey,
27        source: StorageKeyEncodeError,
28    },
29}
30
31impl From<DataKeyEncodeError> for InternalError {
32    fn from(err: DataKeyEncodeError) -> Self {
33        Self::new(
34            ErrorClass::Unsupported,
35            ErrorOrigin::Serialize,
36            err.to_string(),
37        )
38    }
39}
40
41///
42/// KeyDecodeError
43/// (decode / corruption boundary)
44///
45
46#[derive(Debug, ThisError)]
47pub enum KeyDecodeError {
48    #[error("invalid primary key encoding")]
49    InvalidEncoding,
50}
51
52impl From<&'static str> for KeyDecodeError {
53    fn from(_: &'static str) -> Self {
54        Self::InvalidEncoding
55    }
56}
57
58///
59/// DataKeyDecodeError
60/// (decode / corruption boundary)
61///
62
63#[derive(Debug, ThisError)]
64pub enum DataKeyDecodeError {
65    #[error("invalid entity name")]
66    Entity(#[from] IdentityDecodeError),
67
68    #[error("invalid primary key")]
69    Key(#[from] KeyDecodeError),
70}
71
72///
73/// DataKey
74///
75
76#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
77pub struct DataKey {
78    entity: EntityName,
79    key: StorageKey,
80}
81
82impl DataKey {
83    /// Fixed on-disk size in bytes (stable, protocol-level)
84    pub const STORED_SIZE_BYTES: u64 =
85        EntityName::STORED_SIZE_BYTES + StorageKey::STORED_SIZE_BYTES;
86
87    /// Fixed in-memory size (for buffers and arrays only)
88    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
89
90    // ------------------------------------------------------------------
91    // Constructors
92    // ------------------------------------------------------------------
93
94    /// Construct using compile-time entity metadata.
95    ///
96    /// This requires that the entity key is persistable.
97    pub fn try_new<E>(key: E::Key) -> Result<Self, InternalError>
98    where
99        E: EntityKind,
100    {
101        let value = key.to_value();
102        let key = StorageKey::try_from_value(&value)?;
103
104        Ok(Self {
105            entity: Self::entity_for::<E>(),
106            key,
107        })
108    }
109
110    /// Decode a raw entity key from this data key.
111    ///
112    /// This is a fallible boundary that validates entity identity and
113    /// key compatibility against the target entity type.
114    pub fn try_key<E>(&self) -> Result<E::Key, InternalError>
115    where
116        E: EntityKind,
117    {
118        let expected = Self::entity_for::<E>();
119        if self.entity != expected {
120            return Err(InternalError::new(
121                ErrorClass::Corruption,
122                ErrorOrigin::Store,
123                format!(
124                    "data key entity mismatch: expected {}, found {}",
125                    expected, self.entity
126                ),
127            ));
128        }
129
130        let value = self.key.as_value();
131        <E::Key as FieldValue>::from_value(&value).ok_or_else(|| {
132            InternalError::new(
133                ErrorClass::Corruption,
134                ErrorOrigin::Store,
135                format!("data key primary key decode failed: {value:?}"),
136            )
137        })
138    }
139
140    /// Construct a DataKey from a raw StorageKey using entity metadata.
141    #[must_use]
142    pub fn from_key<E: EntityKind>(key: StorageKey) -> Self {
143        Self {
144            entity: Self::entity_for::<E>(),
145            key,
146        }
147    }
148
149    #[must_use]
150    pub fn lower_bound<E>() -> Self
151    where
152        E: EntityKind,
153    {
154        Self {
155            entity: Self::entity_for::<E>(),
156            key: StorageKey::MIN,
157        }
158    }
159
160    #[must_use]
161    pub fn upper_bound<E>() -> Self
162    where
163        E: EntityKind,
164    {
165        Self {
166            entity: Self::entity_for::<E>(),
167            key: StorageKey::upper_bound(),
168        }
169    }
170
171    #[inline]
172    fn entity_for<E: EntityKind>() -> EntityName {
173        // INVARIANT:
174        // `E::ENTITY_NAME` is compile-time schema/codegen metadata. Runtime
175        // user input cannot influence this value.
176        // A failure here is an internal model/codegen contract break.
177        EntityName::try_from_str(E::ENTITY_NAME)
178            .expect("invariant violation: invalid E::ENTITY_NAME (schema/codegen contract broken)")
179    }
180
181    // ------------------------------------------------------------------
182    // Accessors
183    // ------------------------------------------------------------------
184
185    #[must_use]
186    pub const fn entity_name(&self) -> &EntityName {
187        &self.entity
188    }
189
190    #[must_use]
191    pub(crate) const fn storage_key(&self) -> StorageKey {
192        self.key
193    }
194
195    /// Compute on-disk entry size from value length.
196    #[must_use]
197    pub const fn entry_size_bytes(value_len: u64) -> u64 {
198        Self::STORED_SIZE_BYTES + value_len
199    }
200
201    #[must_use]
202    pub fn max_storable() -> Self {
203        Self {
204            entity: EntityName::max_storable(),
205            key: StorageKey::max_storable(),
206        }
207    }
208
209    // ------------------------------------------------------------------
210    // Encoding / decoding
211    // ------------------------------------------------------------------
212
213    /// Encode into fixed-size on-disk representation.
214    pub fn to_raw(&self) -> Result<RawDataKey, InternalError> {
215        self.to_raw_storage_key_error().map_err(|err| {
216            DataKeyEncodeError::KeyEncoding {
217                key: self.clone(),
218                source: err,
219            }
220            .into()
221        })
222    }
223
224    /// Encode into fixed-size on-disk representation, returning storage-key encode errors directly.
225    pub(crate) fn to_raw_storage_key_error(&self) -> Result<RawDataKey, StorageKeyEncodeError> {
226        let mut buf = [0u8; Self::STORED_SIZE_USIZE];
227
228        let entity_bytes = self.entity.to_bytes();
229        buf[..EntityName::STORED_SIZE_USIZE].copy_from_slice(&entity_bytes);
230
231        let key_bytes = self.key.to_bytes()?;
232
233        let key_offset = EntityName::STORED_SIZE_USIZE;
234        buf[key_offset..key_offset + StorageKey::STORED_SIZE_USIZE].copy_from_slice(&key_bytes);
235
236        Ok(RawDataKey(buf))
237    }
238
239    /// Encode a raw data key from validated entity + storage-key parts.
240    pub(crate) fn raw_from_parts(
241        entity: EntityName,
242        key: StorageKey,
243    ) -> Result<RawDataKey, StorageKeyEncodeError> {
244        Self { entity, key }.to_raw_storage_key_error()
245    }
246
247    pub fn try_from_raw(raw: &RawDataKey) -> Result<Self, DataKeyDecodeError> {
248        let bytes = &raw.0;
249
250        let entity = EntityName::from_bytes(&bytes[..EntityName::STORED_SIZE_USIZE])?;
251
252        let key = StorageKey::try_from_bytes(&bytes[EntityName::STORED_SIZE_USIZE..])
253            .map_err(KeyDecodeError::from)?;
254
255        Ok(Self { entity, key })
256    }
257}
258
259impl Display for DataKey {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        write!(f, "#{} ({})", self.entity, self.key)
262    }
263}
264
265///
266/// RawDataKey
267///
268
269#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
270pub struct RawDataKey([u8; DataKey::STORED_SIZE_USIZE]);
271
272impl RawDataKey {
273    #[must_use]
274    pub const fn as_bytes(&self) -> &[u8; DataKey::STORED_SIZE_USIZE] {
275        &self.0
276    }
277}
278
279impl Storable for RawDataKey {
280    fn to_bytes(&self) -> Cow<'_, [u8]> {
281        Cow::Borrowed(&self.0)
282    }
283
284    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
285        let mut out = [0u8; DataKey::STORED_SIZE_USIZE];
286        if bytes.len() == out.len() {
287            out.copy_from_slice(bytes.as_ref());
288        }
289        Self(out)
290    }
291
292    fn into_bytes(self) -> Vec<u8> {
293        self.0.to_vec()
294    }
295
296    const BOUND: Bound = Bound::Bounded {
297        max_size: DataKey::STORED_SIZE_BYTES as u32,
298        is_fixed_size: true,
299    };
300}
301
302///
303/// TESTS
304///
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use std::borrow::Cow;
310
311    #[test]
312    fn data_key_is_exactly_fixed_size() {
313        let data_key = DataKey::max_storable();
314        let size = data_key.to_raw().unwrap().as_bytes().len();
315        assert_eq!(size, DataKey::STORED_SIZE_USIZE);
316    }
317
318    #[test]
319    fn data_key_ordering_matches_bytes() {
320        let keys = vec![
321            DataKey {
322                entity: EntityName::try_from_str("a").unwrap(),
323                key: StorageKey::Int(0),
324            },
325            DataKey {
326                entity: EntityName::try_from_str("aa").unwrap(),
327                key: StorageKey::Int(0),
328            },
329            DataKey {
330                entity: EntityName::try_from_str("b").unwrap(),
331                key: StorageKey::Int(0),
332            },
333            DataKey {
334                entity: EntityName::try_from_str("a").unwrap(),
335                key: StorageKey::Uint(1),
336            },
337        ];
338
339        let mut by_ord = keys.clone();
340        by_ord.sort();
341
342        let mut by_bytes = keys;
343        by_bytes.sort_by(|a, b| {
344            a.to_raw()
345                .unwrap()
346                .as_bytes()
347                .cmp(b.to_raw().unwrap().as_bytes())
348        });
349
350        assert_eq!(by_ord, by_bytes);
351    }
352
353    #[test]
354    fn data_key_rejects_corrupt_entity() {
355        let mut raw = DataKey::max_storable().to_raw().unwrap();
356        raw.0[0] = 0;
357        assert!(DataKey::try_from_raw(&raw).is_err());
358    }
359
360    #[test]
361    fn data_key_rejects_corrupt_key() {
362        let mut raw = DataKey::max_storable().to_raw().unwrap();
363        let off = EntityName::STORED_SIZE_USIZE;
364        raw.0[off] = 0xFF;
365        assert!(DataKey::try_from_raw(&raw).is_err());
366    }
367
368    #[test]
369    #[allow(clippy::cast_possible_truncation)]
370    fn data_key_fuzz_roundtrip_is_canonical() {
371        let mut seed = 0xDEAD_BEEF_u64;
372
373        for _ in 0..1_000 {
374            let mut bytes = [0u8; DataKey::STORED_SIZE_USIZE];
375            for b in &mut bytes {
376                seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
377                *b = (seed >> 24) as u8;
378            }
379
380            let raw = RawDataKey(bytes);
381            if let Ok(decoded) = DataKey::try_from_raw(&raw) {
382                let re = decoded.to_raw().unwrap();
383                assert_eq!(raw.as_bytes(), re.as_bytes());
384            }
385        }
386    }
387
388    #[test]
389    fn raw_data_key_storable_roundtrip() {
390        let key = DataKey::max_storable().to_raw().unwrap();
391        let bytes = key.to_bytes();
392        let decoded = RawDataKey::from_bytes(Cow::Borrowed(&bytes));
393        assert_eq!(key, decoded);
394    }
395}