Skip to main content

icydb_core/db/store/
row.rs

1use crate::{
2    error::{ErrorClass, ErrorOrigin, InternalError},
3    serialize::{SerializeError, 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: {source}")]
47    Deserialize {
48        #[source]
49        source: SerializeError,
50    },
51}
52
53///
54/// RawRow
55///
56
57/// Max serialized bytes for a single row (protocol-level limit).
58pub const MAX_ROW_BYTES: u32 = 4 * 1024 * 1024;
59
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct RawRow(Vec<u8>);
62
63impl RawRow {
64    /// Construct a raw row from serialized bytes.
65    #[allow(clippy::cast_possible_truncation)]
66    pub fn try_new(bytes: Vec<u8>) -> Result<Self, RawRowError> {
67        let len = bytes.len() as u32;
68        if len > MAX_ROW_BYTES {
69            return Err(RawRowError::TooLarge { len });
70        }
71        Ok(Self(bytes))
72    }
73
74    #[must_use]
75    pub fn as_bytes(&self) -> &[u8] {
76        &self.0
77    }
78
79    /// Length in bytes (in-memory; bounded by construction).
80    #[must_use]
81    pub const fn len(&self) -> usize {
82        self.0.len()
83    }
84
85    #[must_use]
86    pub const fn is_empty(&self) -> bool {
87        self.0.is_empty()
88    }
89
90    /// Decode into an entity.
91    pub fn try_decode<E: EntityKind>(&self) -> Result<E, RowDecodeError> {
92        deserialize::<E>(&self.0).map_err(|source| RowDecodeError::Deserialize { source })
93    }
94}
95
96impl Storable for RawRow {
97    fn to_bytes(&self) -> Cow<'_, [u8]> {
98        Cow::Borrowed(&self.0)
99    }
100
101    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
102        // Trusted store boundary: bounded by BOUND
103        Self(bytes.into_owned())
104    }
105
106    fn into_bytes(self) -> Vec<u8> {
107        self.0
108    }
109
110    const BOUND: Bound = Bound::Bounded {
111        max_size: MAX_ROW_BYTES,
112        is_fixed_size: false,
113    };
114}
115
116///
117/// TESTS
118///
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn raw_row_rejects_oversized_payload() {
126        let bytes = vec![0u8; MAX_ROW_BYTES as usize + 1];
127        let err = RawRow::try_new(bytes).unwrap_err();
128        assert!(matches!(err, RawRowError::TooLarge { .. }));
129    }
130}