Skip to main content

icydb_core/value/
storage_key.rs

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