Skip to main content

icydb_core/db/store/
row.rs

1use crate::{
2    error::{ErrorClass, ErrorOrigin, InternalError},
3    serialize::deserialize,
4    traits::{EntityKind, Storable},
5};
6use canic_cdk::structures::storable::Bound;
7use std::borrow::Cow;
8use thiserror::Error as ThisError;
9
10///
11/// RawRowError
12/// Construction / storage-boundary errors.
13///
14
15#[derive(Debug, ThisError)]
16pub enum RawRowError {
17    #[error("row exceeds max size: {len} bytes (limit {MAX_ROW_BYTES})")]
18    TooLarge { len: u32 },
19}
20
21impl RawRowError {
22    #[must_use]
23    pub const fn class(&self) -> ErrorClass {
24        ErrorClass::Unsupported
25    }
26
27    #[must_use]
28    pub const fn origin(&self) -> ErrorOrigin {
29        ErrorOrigin::Store
30    }
31}
32
33impl From<RawRowError> for InternalError {
34    fn from(err: RawRowError) -> Self {
35        Self::new(err.class(), err.origin(), err.to_string())
36    }
37}
38
39///
40/// RowDecodeError
41/// Logical / format errors during decode.
42///
43
44#[derive(Debug, ThisError)]
45pub enum RowDecodeError {
46    #[error("row failed to deserialize")]
47    Deserialize,
48}
49
50///
51/// RawRow
52///
53
54/// Max serialized bytes for a single row (protocol-level limit).
55pub const MAX_ROW_BYTES: u32 = 4 * 1024 * 1024;
56
57#[derive(Clone, Debug, Eq, PartialEq)]
58pub struct RawRow(Vec<u8>);
59
60impl RawRow {
61    /// Construct a raw row from serialized bytes.
62    #[allow(clippy::cast_possible_truncation)]
63    pub fn try_new(bytes: Vec<u8>) -> Result<Self, RawRowError> {
64        let len = bytes.len() as u32;
65        if len > MAX_ROW_BYTES {
66            return Err(RawRowError::TooLarge { len });
67        }
68        Ok(Self(bytes))
69    }
70
71    #[must_use]
72    pub fn as_bytes(&self) -> &[u8] {
73        &self.0
74    }
75
76    /// Length in bytes (in-memory; bounded by construction).
77    #[must_use]
78    pub const fn len(&self) -> usize {
79        self.0.len()
80    }
81
82    #[must_use]
83    pub const fn is_empty(&self) -> bool {
84        self.0.is_empty()
85    }
86
87    /// Decode into an entity.
88    pub fn try_decode<E: EntityKind>(&self) -> Result<E, RowDecodeError> {
89        deserialize::<E>(&self.0).map_err(|_| RowDecodeError::Deserialize)
90    }
91}
92
93impl Storable for RawRow {
94    fn to_bytes(&self) -> Cow<'_, [u8]> {
95        Cow::Borrowed(&self.0)
96    }
97
98    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
99        // Trusted store boundary: bounded by BOUND
100        Self(bytes.into_owned())
101    }
102
103    fn into_bytes(self) -> Vec<u8> {
104        self.0
105    }
106
107    const BOUND: Bound = Bound::Bounded {
108        max_size: MAX_ROW_BYTES,
109        is_fixed_size: false,
110    };
111}
112
113///
114/// TESTS
115///
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn raw_row_rejects_oversized_payload() {
123        let bytes = vec![0u8; MAX_ROW_BYTES as usize + 1];
124        let err = RawRow::try_new(bytes).unwrap_err();
125        assert!(matches!(err, RawRowError::TooLarge { .. }));
126    }
127}