Skip to main content

icydb_core/value/
storage_key.rs

1//! Module: value::storage_key
2//! Responsibility: fixed-width scalar key encoding for persistent ordering.
3//! Does not own: typed primary-key semantics (`Id<E>`) or query coercion rules.
4//! Boundary: shared by value normalization and data/index persistence layers.
5//!
6//! `StorageKey` is a storage-normalized scalar and MUST NOT be used as a
7//! public primary-key abstraction.
8
9#![expect(clippy::cast_possible_truncation)]
10
11use crate::{
12    error::InternalError,
13    traits::Repr,
14    types::{Account, Principal, Subaccount, Timestamp, Ulid},
15};
16use candid::CandidType;
17use serde::Deserialize;
18use std::cmp::Ordering;
19use thiserror::Error as ThisError;
20
21//
22// StorageKeyEncodeError
23// Errors returned when encoding a storage key for persistence.
24//
25
26#[derive(Debug, ThisError)]
27pub enum StorageKeyEncodeError {
28    #[error("account owner principal exceeds max length: {len} bytes (limit {max})")]
29    AccountOwnerTooLarge { len: usize, max: usize },
30
31    #[error("value kind '{kind}' is not storage-key encodable")]
32    UnsupportedValueKind { kind: &'static str },
33
34    #[error("principal exceeds max length: {len} bytes (limit {max})")]
35    PrincipalTooLarge { len: usize, max: usize },
36}
37
38impl From<StorageKeyEncodeError> for InternalError {
39    fn from(err: StorageKeyEncodeError) -> Self {
40        Self::serialize_unsupported(err.to_string())
41    }
42}
43
44//
45// StorageKeyDecodeError
46// Errors returned when decoding a persisted storage key payload.
47//
48
49#[derive(Debug, ThisError)]
50pub enum StorageKeyDecodeError {
51    #[error("corrupted StorageKey: invalid size")]
52    InvalidSize,
53
54    #[error("corrupted StorageKey: invalid tag")]
55    InvalidTag,
56
57    #[error("corrupted StorageKey: invalid principal length")]
58    InvalidPrincipalLength,
59
60    #[error("corrupted StorageKey: non-zero {field} padding")]
61    NonZeroPadding { field: &'static str },
62
63    #[error("corrupted StorageKey: invalid account payload ({reason})")]
64    InvalidAccountPayload { reason: &'static str },
65}
66
67//
68// StorageKey
69//
70// Storage-normalized scalar key used by persistence and indexing.
71//
72// This type defines the *only* on-disk representation for scalar keys.
73// It is deliberately separated from typed primary-key values (`Id<E>`).
74//
75
76#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq)]
77pub enum StorageKey {
78    Account(Account),
79    Int(i64),
80    Principal(Principal),
81    Subaccount(Subaccount),
82    Timestamp(Timestamp),
83    Uint(u64),
84    Ulid(Ulid),
85    Unit,
86}
87
88impl StorageKey {
89    // ── Variant tags (DO NOT reorder) ────────────────────────────────
90    pub(crate) const TAG_ACCOUNT: u8 = 0;
91    pub(crate) const TAG_INT: u8 = 1;
92    pub(crate) const TAG_PRINCIPAL: u8 = 2;
93    pub(crate) const TAG_SUBACCOUNT: u8 = 3;
94    pub(crate) const TAG_TIMESTAMP: u8 = 4;
95    pub(crate) const TAG_UINT: u8 = 5;
96    pub(crate) const TAG_ULID: u8 = 6;
97    pub(crate) const TAG_UNIT: u8 = 7;
98
99    /// Fixed serialized size in bytes (protocol invariant).
100    /// DO NOT CHANGE without migration.
101    pub const STORED_SIZE_BYTES: u64 = 64;
102    pub const STORED_SIZE_USIZE: usize = Self::STORED_SIZE_BYTES as usize;
103
104    const TAG_SIZE: usize = 1;
105    pub(crate) const TAG_OFFSET: usize = 0;
106
107    pub(crate) const PAYLOAD_OFFSET: usize = Self::TAG_SIZE;
108    const PAYLOAD_SIZE: usize = Self::STORED_SIZE_USIZE - Self::TAG_SIZE;
109
110    pub(crate) const INT_SIZE: usize = 8;
111    pub(crate) const UINT_SIZE: usize = 8;
112    pub(crate) const TIMESTAMP_SIZE: usize = 8;
113    pub(crate) const ULID_SIZE: usize = 16;
114    pub(crate) const SUBACCOUNT_SIZE: usize = 32;
115    const ACCOUNT_MAX_SIZE: usize = 62;
116
117    const fn tag(&self) -> u8 {
118        match self {
119            Self::Account(_) => Self::TAG_ACCOUNT,
120            Self::Int(_) => Self::TAG_INT,
121            Self::Principal(_) => Self::TAG_PRINCIPAL,
122            Self::Subaccount(_) => Self::TAG_SUBACCOUNT,
123            Self::Timestamp(_) => Self::TAG_TIMESTAMP,
124            Self::Uint(_) => Self::TAG_UINT,
125            Self::Ulid(_) => Self::TAG_ULID,
126            Self::Unit => Self::TAG_UNIT,
127        }
128    }
129
130    /// Sentinel key representing the maximum storable value.
131    #[must_use]
132    pub fn max_storable() -> Self {
133        Self::Account(Account::max_storable())
134    }
135
136    /// Global minimum key for scan bounds.
137    pub const MIN: Self = Self::Account(Account::from_parts(Principal::from_slice(&[]), None));
138
139    #[must_use]
140    pub const fn lower_bound() -> Self {
141        Self::MIN
142    }
143
144    #[must_use]
145    pub const fn upper_bound() -> Self {
146        Self::Unit
147    }
148
149    const fn variant_rank(&self) -> u8 {
150        self.tag()
151    }
152
153    const fn from_account_encode_error(
154        err: crate::types::AccountEncodeError,
155    ) -> StorageKeyEncodeError {
156        match err {
157            crate::types::AccountEncodeError::OwnerEncode(inner) => {
158                Self::from_principal_encode_error(inner)
159            }
160            crate::types::AccountEncodeError::OwnerTooLarge { len, max } => {
161                StorageKeyEncodeError::AccountOwnerTooLarge { len, max }
162            }
163        }
164    }
165
166    const fn from_principal_encode_error(
167        err: crate::types::PrincipalEncodeError,
168    ) -> StorageKeyEncodeError {
169        match err {
170            crate::types::PrincipalEncodeError::TooLarge { len, max } => {
171                StorageKeyEncodeError::PrincipalTooLarge { len, max }
172            }
173        }
174    }
175
176    /// Encode this key into its fixed-size on-disk representation.
177    pub fn to_bytes(self) -> Result<[u8; Self::STORED_SIZE_USIZE], StorageKeyEncodeError> {
178        // Phase 1: write variant tag and select fixed payload window.
179        let mut buf = [0u8; Self::STORED_SIZE_USIZE];
180        buf[Self::TAG_OFFSET] = self.tag();
181        let payload = &mut buf[Self::PAYLOAD_OFFSET..=Self::PAYLOAD_SIZE];
182
183        // Phase 2: encode variant payload into the normalized fixed-width frame.
184        match self {
185            Self::Account(v) => {
186                let bytes = v
187                    .to_stored_bytes()
188                    .map_err(Self::from_account_encode_error)?;
189                payload[..Self::ACCOUNT_MAX_SIZE].copy_from_slice(&bytes);
190            }
191            Self::Int(v) => {
192                let biased = v.cast_unsigned() ^ (1u64 << 63);
193                payload[..Self::INT_SIZE].copy_from_slice(&biased.to_be_bytes());
194            }
195            Self::Uint(v) => payload[..Self::UINT_SIZE].copy_from_slice(&v.to_be_bytes()),
196            Self::Timestamp(v) => {
197                payload[..Self::TIMESTAMP_SIZE].copy_from_slice(&v.repr().to_be_bytes());
198            }
199            Self::Principal(v) => {
200                let bytes = v
201                    .stored_bytes()
202                    .map_err(Self::from_principal_encode_error)?;
203                let len = bytes.len();
204                payload[0] =
205                    u8::try_from(len).map_err(|_| StorageKeyEncodeError::PrincipalTooLarge {
206                        len,
207                        max: Principal::MAX_LENGTH_IN_BYTES as usize,
208                    })?;
209                payload[1..=len].copy_from_slice(bytes);
210            }
211            Self::Subaccount(v) => payload[..Self::SUBACCOUNT_SIZE].copy_from_slice(&v.to_array()),
212            Self::Ulid(v) => payload[..Self::ULID_SIZE].copy_from_slice(&v.to_bytes()),
213            Self::Unit => {}
214        }
215
216        Ok(buf)
217    }
218
219    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, StorageKeyDecodeError> {
220        let bytes: &[u8; Self::STORED_SIZE_USIZE] = bytes
221            .try_into()
222            .map_err(|_| StorageKeyDecodeError::InvalidSize)?;
223
224        Self::try_from_stored_bytes(bytes)
225    }
226
227    /// Decode one storage key from one already size-validated stored frame.
228    pub(crate) fn try_from_stored_bytes(
229        bytes: &[u8; Self::STORED_SIZE_USIZE],
230    ) -> Result<Self, StorageKeyDecodeError> {
231        let tag = bytes[Self::TAG_OFFSET];
232        let payload = &bytes[Self::PAYLOAD_OFFSET..=Self::PAYLOAD_SIZE];
233
234        let ensure_zero_padding = |used: usize, ctx: &'static str| {
235            if payload[used..].iter().all(|&b| b == 0) {
236                Ok(())
237            } else {
238                Err(StorageKeyDecodeError::NonZeroPadding { field: ctx })
239            }
240        };
241
242        // Phase 2: decode tagged payload and enforce zero-padding invariants.
243        match tag {
244            Self::TAG_ACCOUNT => {
245                let end = Account::STORED_SIZE as usize;
246                ensure_zero_padding(end, "account")?;
247                Ok(Self::Account(
248                    Account::try_from_bytes(&payload[..end]).map_err(|reason| {
249                        StorageKeyDecodeError::InvalidAccountPayload { reason }
250                    })?,
251                ))
252            }
253            Self::TAG_INT => {
254                let mut buf = [0u8; Self::INT_SIZE];
255                buf.copy_from_slice(&payload[..Self::INT_SIZE]);
256                ensure_zero_padding(Self::INT_SIZE, "int")?;
257                Ok(Self::Int(
258                    (u64::from_be_bytes(buf) ^ (1u64 << 63)).cast_signed(),
259                ))
260            }
261            Self::TAG_PRINCIPAL => {
262                let len = payload[0] as usize;
263                if len > Principal::MAX_LENGTH_IN_BYTES as usize {
264                    return Err(StorageKeyDecodeError::InvalidPrincipalLength);
265                }
266                ensure_zero_padding(1 + len, "principal")?;
267                Ok(Self::Principal(Principal::from_slice(&payload[1..=len])))
268            }
269            Self::TAG_SUBACCOUNT => {
270                ensure_zero_padding(Self::SUBACCOUNT_SIZE, "subaccount")?;
271                let mut buf = [0u8; Self::SUBACCOUNT_SIZE];
272                buf.copy_from_slice(&payload[..Self::SUBACCOUNT_SIZE]);
273                Ok(Self::Subaccount(Subaccount::from_array(buf)))
274            }
275            Self::TAG_TIMESTAMP => {
276                ensure_zero_padding(Self::TIMESTAMP_SIZE, "timestamp")?;
277                let mut buf = [0u8; Self::TIMESTAMP_SIZE];
278                buf.copy_from_slice(&payload[..Self::TIMESTAMP_SIZE]);
279                Ok(Self::Timestamp(Timestamp::from_repr(i64::from_be_bytes(
280                    buf,
281                ))))
282            }
283            Self::TAG_UINT => {
284                ensure_zero_padding(Self::UINT_SIZE, "uint")?;
285                let mut buf = [0u8; Self::UINT_SIZE];
286                buf.copy_from_slice(&payload[..Self::UINT_SIZE]);
287                Ok(Self::Uint(u64::from_be_bytes(buf)))
288            }
289            Self::TAG_ULID => {
290                ensure_zero_padding(Self::ULID_SIZE, "ulid")?;
291                let mut buf = [0u8; Self::ULID_SIZE];
292                buf.copy_from_slice(&payload[..Self::ULID_SIZE]);
293                Ok(Self::Ulid(Ulid::from_bytes(buf)))
294            }
295            Self::TAG_UNIT => {
296                ensure_zero_padding(0, "unit")?;
297                Ok(Self::Unit)
298            }
299            _ => Err(StorageKeyDecodeError::InvalidTag),
300        }
301    }
302}
303
304impl Ord for StorageKey {
305    fn cmp(&self, other: &Self) -> Ordering {
306        match (self, other) {
307            (Self::Account(a), Self::Account(b)) => a.cmp(b),
308            (Self::Int(a), Self::Int(b)) => a.cmp(b),
309            (Self::Principal(a), Self::Principal(b)) => a.cmp(b),
310            (Self::Uint(a), Self::Uint(b)) => a.cmp(b),
311            (Self::Ulid(a), Self::Ulid(b)) => a.cmp(b),
312            (Self::Subaccount(a), Self::Subaccount(b)) => a.cmp(b),
313            (Self::Timestamp(a), Self::Timestamp(b)) => a.cmp(b),
314            _ => self.variant_rank().cmp(&other.variant_rank()),
315        }
316    }
317}
318
319impl PartialOrd for StorageKey {
320    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
321        Some(self.cmp(other))
322    }
323}
324
325impl TryFrom<&[u8]> for StorageKey {
326    type Error = StorageKeyDecodeError;
327    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
328        Self::try_from_bytes(bytes)
329    }
330}
331
332//
333// TESTS
334//
335
336#[cfg(test)]
337mod tests {
338    use super::{StorageKey, StorageKeyDecodeError, StorageKeyEncodeError};
339    use crate::{
340        types::{
341            Account, Date, Decimal, Duration, Float32, Float64, Int, Int128, Nat, Nat128,
342            Principal, Subaccount, Timestamp, Ulid,
343        },
344        value::{Value, ValueEnum, storage_key_from_runtime_value},
345    };
346
347    macro_rules! sample_value_for_scalar {
348        (Account) => {
349            Value::Account(Account::dummy(7))
350        };
351        (Blob) => {
352            Value::Blob(vec![1u8, 2u8, 3u8])
353        };
354        (Bool) => {
355            Value::Bool(true)
356        };
357        (Date) => {
358            Value::Date(Date::new(2024, 1, 2))
359        };
360        (Decimal) => {
361            Value::Decimal(Decimal::new(123, 2))
362        };
363        (Duration) => {
364            Value::Duration(Duration::from_secs(1))
365        };
366        (Enum) => {
367            Value::Enum(ValueEnum::loose("example"))
368        };
369        (Float32) => {
370            Value::Float32(Float32::try_new(1.25).expect("Float32 sample should be finite"))
371        };
372        (Float64) => {
373            Value::Float64(Float64::try_new(2.5).expect("Float64 sample should be finite"))
374        };
375        (Int) => {
376            Value::Int(-7)
377        };
378        (Int128) => {
379            Value::Int128(Int128::from(123i128))
380        };
381        (IntBig) => {
382            Value::IntBig(Int::from(99i32))
383        };
384        (Principal) => {
385            Value::Principal(Principal::from_slice(&[1u8, 2u8, 3u8]))
386        };
387        (Subaccount) => {
388            Value::Subaccount(Subaccount::new([1u8; 32]))
389        };
390        (Text) => {
391            Value::Text("example".to_string())
392        };
393        (Timestamp) => {
394            Value::Timestamp(Timestamp::from_secs(1))
395        };
396        (Uint) => {
397            Value::Uint(7)
398        };
399        (Uint128) => {
400            Value::Uint128(Nat128::from(9u128))
401        };
402        (UintBig) => {
403            Value::UintBig(Nat::from(11u64))
404        };
405        (Ulid) => {
406            Value::Ulid(Ulid::from_u128(42))
407        };
408        (Unit) => {
409            Value::Unit
410        };
411    }
412
413    fn registry_storage_encodable_cases() -> Vec<(Value, bool)> {
414        macro_rules! collect_cases {
415            ( @entries $( ($scalar:ident, $family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
416                vec![ $( (sample_value_for_scalar!($scalar), $is_storage_key_encodable) ),* ]
417            };
418            ( @args $($ignore:tt)*; @entries $( ($scalar:ident, $family:expr, $value_pat:pat, is_numeric_value = $is_numeric:expr, supports_numeric_coercion = $supports_numeric_coercion:expr, supports_arithmetic = $supports_arithmetic:expr, supports_equality = $supports_equality:expr, supports_ordering = $supports_ordering:expr, is_keyable = $is_keyable:expr, is_storage_key_encodable = $is_storage_key_encodable:expr) ),* $(,)? ) => {
419                vec![ $( (sample_value_for_scalar!($scalar), $is_storage_key_encodable) ),* ]
420            };
421        }
422
423        scalar_registry!(collect_cases)
424    }
425
426    #[test]
427    fn storage_key_try_from_value_matches_registry_flag() {
428        for (value, expected_encodable) in registry_storage_encodable_cases() {
429            assert_eq!(
430                storage_key_from_runtime_value(&value).is_ok(),
431                expected_encodable,
432                "value: {value:?}"
433            );
434        }
435    }
436
437    #[test]
438    fn storage_key_known_encodability_contracts() {
439        assert!(storage_key_from_runtime_value(&Value::Unit).is_ok());
440        assert!(storage_key_from_runtime_value(&Value::Decimal(Decimal::new(1, 0))).is_err());
441        assert!(storage_key_from_runtime_value(&Value::Text("x".to_string())).is_err());
442        assert!(storage_key_from_runtime_value(&Value::Account(Account::dummy(1))).is_ok());
443    }
444
445    #[test]
446    fn storage_key_unsupported_values_report_kind() {
447        let decimal_err = storage_key_from_runtime_value(&Value::Decimal(Decimal::new(1, 0)))
448            .expect_err("Decimal is not storage-key encodable");
449        assert!(matches!(
450            decimal_err,
451            StorageKeyEncodeError::UnsupportedValueKind { kind } if kind == "Decimal"
452        ));
453
454        let text_err = storage_key_from_runtime_value(&Value::Text("x".to_string()))
455            .expect_err("Text is not storage-key encodable");
456        assert!(matches!(
457            text_err,
458            StorageKeyEncodeError::UnsupportedValueKind { kind } if kind == "Text"
459        ));
460    }
461
462    #[test]
463    fn storage_keys_sort_deterministically_across_mixed_variants() {
464        let mut keys = vec![
465            storage_key_from_runtime_value(&Value::Unit).expect("Unit is encodable"),
466            storage_key_from_runtime_value(&Value::Ulid(Ulid::from_u128(2)))
467                .expect("Ulid is encodable"),
468            storage_key_from_runtime_value(&Value::Uint(2)).expect("Uint is encodable"),
469            storage_key_from_runtime_value(&Value::Timestamp(Timestamp::from_secs(2)))
470                .expect("Timestamp is encodable"),
471            storage_key_from_runtime_value(&Value::Subaccount(Subaccount::new([3u8; 32])))
472                .expect("Subaccount is encodable"),
473            storage_key_from_runtime_value(&Value::Principal(Principal::from_slice(&[9u8])))
474                .expect("Principal is encodable"),
475            storage_key_from_runtime_value(&Value::Int(-1)).expect("Int is encodable"),
476            storage_key_from_runtime_value(&Value::Account(Account::dummy(3)))
477                .expect("Account is encodable"),
478        ];
479
480        keys.sort();
481
482        let expected = vec![
483            StorageKey::Account(Account::dummy(3)),
484            StorageKey::Int(-1),
485            StorageKey::Principal(Principal::from_slice(&[9u8])),
486            StorageKey::Subaccount(Subaccount::new([3u8; 32])),
487            StorageKey::Timestamp(Timestamp::from_secs(2)),
488            StorageKey::Uint(2),
489            StorageKey::Ulid(Ulid::from_u128(2)),
490            StorageKey::Unit,
491        ];
492
493        assert_eq!(keys, expected);
494    }
495
496    #[test]
497    fn storage_key_decode_rejects_invalid_size_as_structured_error() {
498        let err =
499            StorageKey::try_from_bytes(&[]).expect_err("decode should reject invalid key size");
500        assert!(matches!(err, StorageKeyDecodeError::InvalidSize));
501    }
502
503    #[test]
504    fn storage_key_decode_rejects_invalid_tag_as_structured_error() {
505        let mut bytes = [0u8; StorageKey::STORED_SIZE_USIZE];
506        bytes[StorageKey::TAG_OFFSET] = 0xFF;
507
508        let err = StorageKey::try_from_bytes(&bytes).expect_err("decode should reject invalid tag");
509        assert!(matches!(err, StorageKeyDecodeError::InvalidTag));
510    }
511
512    #[test]
513    fn storage_key_decode_rejects_non_zero_padding_with_segment_context() {
514        let mut bytes = [0u8; StorageKey::STORED_SIZE_USIZE];
515        bytes[StorageKey::TAG_OFFSET] = StorageKey::TAG_UNIT;
516        bytes[StorageKey::PAYLOAD_OFFSET] = 1;
517
518        let err = StorageKey::try_from_bytes(&bytes)
519            .expect_err("decode should reject non-zero padding for unit payload");
520        assert!(matches!(
521            err,
522            StorageKeyDecodeError::NonZeroPadding { field } if field == "unit"
523        ));
524    }
525}