Skip to main content

wasm_dbms_api/dbms/
custom_value.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::hash::{Hash, Hasher};
4
5use serde::{Deserialize, Serialize};
6
7/// A type-erased representation of a custom data type value.
8///
9/// `CustomValue` holds the binary encoding and cached display string for
10/// a user-defined data type, along with a type tag that identifies the
11/// concrete type. This allows the DBMS engine to compare, order, hash,
12/// and display custom values without knowing their concrete types.
13#[derive(Clone, Debug, Serialize, Deserialize)]
14#[cfg_attr(feature = "candid", derive(candid::CandidType))]
15pub struct CustomValue {
16    /// Type identifier from `CustomDataType::TYPE_TAG`.
17    pub type_tag: String,
18    /// Binary encoding via the concrete type's `Encode` impl.
19    pub encoded: Vec<u8>,
20    /// Cached `Display` output for human-readable representation.
21    pub display: String,
22}
23
24impl CustomValue {
25    /// Creates a new `CustomValue` from a concrete [`CustomDataType`](crate::dbms::types::CustomDataType).
26    ///
27    /// This constructor ensures consistency between the type tag, encoded bytes,
28    /// and display string by deriving all three from the concrete value.
29    pub fn new<T: crate::dbms::types::CustomDataType>(value: &T) -> Self {
30        Self {
31            type_tag: T::TYPE_TAG.to_string(),
32            encoded: crate::memory::Encode::encode(value).into_owned(),
33            display: value.to_string(),
34        }
35    }
36}
37
38impl PartialEq for CustomValue {
39    fn eq(&self, other: &Self) -> bool {
40        self.type_tag == other.type_tag && self.encoded == other.encoded
41    }
42}
43
44impl Eq for CustomValue {}
45
46impl PartialOrd for CustomValue {
47    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
48        Some(self.cmp(other))
49    }
50}
51
52impl Ord for CustomValue {
53    fn cmp(&self, other: &Self) -> Ordering {
54        self.type_tag
55            .cmp(&other.type_tag)
56            .then_with(|| self.encoded.cmp(&other.encoded))
57    }
58}
59
60impl Hash for CustomValue {
61    fn hash<H: Hasher>(&self, state: &mut H) {
62        self.type_tag.hash(state);
63        self.encoded.hash(state);
64    }
65}
66
67impl fmt::Display for CustomValue {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "{}", self.display)
70    }
71}
72
73#[cfg(test)]
74mod test {
75
76    use std::collections::HashSet;
77
78    use super::*;
79
80    fn make_custom_value(type_tag: &str, encoded: &[u8], display: &str) -> CustomValue {
81        CustomValue {
82            type_tag: type_tag.to_string(),
83            encoded: encoded.to_vec(),
84            display: display.to_string(),
85        }
86    }
87
88    #[test]
89    fn test_should_compare_equal_custom_values() {
90        let a = make_custom_value("color", &[0x01, 0x02], "red");
91        let b = make_custom_value("color", &[0x01, 0x02], "red");
92        assert_eq!(a, b);
93    }
94
95    #[test]
96    fn test_should_not_equal_different_encoded() {
97        let a = make_custom_value("color", &[0x01], "red");
98        let b = make_custom_value("color", &[0x02], "blue");
99        assert_ne!(a, b);
100    }
101
102    #[test]
103    fn test_should_not_equal_different_type_tag() {
104        let a = make_custom_value("color", &[0x01], "red");
105        let b = make_custom_value("size", &[0x01], "red");
106        assert_ne!(a, b);
107    }
108
109    #[test]
110    fn test_should_ignore_display_in_equality() {
111        let a = make_custom_value("color", &[0x01, 0x02], "red");
112        let b = make_custom_value("color", &[0x01, 0x02], "rouge");
113        assert_eq!(a, b);
114    }
115
116    #[test]
117    fn test_should_order_by_type_tag_first() {
118        let alpha = make_custom_value("alpha", &[0xFF], "big");
119        let beta = make_custom_value("beta", &[0x00], "small");
120        assert!(alpha < beta);
121    }
122
123    #[test]
124    fn test_should_order_by_encoded_within_same_tag() {
125        let a = make_custom_value("color", &[0x01], "red");
126        let b = make_custom_value("color", &[0x02], "blue");
127        assert!(a < b);
128    }
129
130    #[test]
131    fn test_should_hash_consistently() {
132        let a = make_custom_value("color", &[0x01, 0x02], "red");
133        let b = make_custom_value("color", &[0x01, 0x02], "rouge");
134
135        let mut set = HashSet::new();
136        set.insert(a.clone());
137
138        // b has a different display but same tag+encoded, so it should be found in the set
139        assert!(set.contains(&b));
140
141        // inserting b should not increase the set size
142        set.insert(b);
143        assert_eq!(set.len(), 1);
144    }
145
146    #[test]
147    fn test_should_display_cached_string() {
148        let cv = make_custom_value("color", &[0x01], "red");
149        assert_eq!(format!("{cv}"), "red");
150    }
151
152    #[test]
153    #[allow(clippy::clone_on_copy)]
154    fn test_should_clone() {
155        let original = make_custom_value("color", &[0x01, 0x02, 0x03], "red");
156        let cloned = original.clone();
157        assert_eq!(original, cloned);
158        assert_eq!(original.display, cloned.display);
159    }
160
161    #[test]
162    fn test_should_debug() {
163        let cv = make_custom_value("color", &[0x01], "red");
164        let debug_output = format!("{cv:?}");
165        assert!(debug_output.contains("color"));
166        assert!(debug_output.contains("red"));
167    }
168
169    #[test]
170    fn test_should_implement_custom_data_type() {
171        use std::borrow::Cow;
172        use std::fmt;
173
174        use serde::{Deserialize, Serialize};
175
176        use crate::dbms::types::{CustomDataType, DataType};
177        use crate::dbms::value::Value;
178        use crate::memory::{self, DataSize, MSize, MemoryResult, PageOffset};
179
180        #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
181        pub enum TestStatus {
182            Active,
183            Inactive,
184        }
185
186        // Manual Encode impl since #[derive(Encode)] only supports structs
187        impl memory::Encode for TestStatus {
188            const SIZE: DataSize = DataSize::Fixed(1);
189            const ALIGNMENT: PageOffset = 1;
190
191            fn size(&self) -> MSize {
192                1
193            }
194
195            fn encode(&self) -> Cow<'_, [u8]> {
196                match self {
197                    TestStatus::Active => Cow::Borrowed(&[0]),
198                    TestStatus::Inactive => Cow::Borrowed(&[1]),
199                }
200            }
201
202            fn decode(data: Cow<[u8]>) -> MemoryResult<Self> {
203                match data.first() {
204                    Some(0) => Ok(TestStatus::Active),
205                    Some(1) => Ok(TestStatus::Inactive),
206                    _ => Err(crate::memory::MemoryError::DecodeError(
207                        crate::memory::DecodeError::TooShort,
208                    )),
209                }
210            }
211        }
212
213        impl fmt::Display for TestStatus {
214            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215                write!(f, "{self:?}")
216            }
217        }
218
219        impl Default for TestStatus {
220            fn default() -> Self {
221                Self::Active
222            }
223        }
224
225        impl From<TestStatus> for Value {
226            fn from(val: TestStatus) -> Value {
227                Value::Custom(CustomValue {
228                    type_tag: TestStatus::TYPE_TAG.to_string(),
229                    encoded: crate::memory::Encode::encode(&val).into_owned(),
230                    display: val.to_string(),
231                })
232            }
233        }
234
235        impl DataType for TestStatus {}
236
237        impl CustomDataType for TestStatus {
238            const TYPE_TAG: &'static str = "test_status";
239        }
240
241        // Test trait impl
242        assert_eq!(TestStatus::TYPE_TAG, "test_status");
243
244        // Test Into<Value> conversion
245        let value: Value = TestStatus::Active.into();
246        assert!(matches!(value, Value::Custom(_)));
247
248        let cv = value.as_custom().unwrap();
249        assert_eq!(cv.type_tag, "test_status");
250        assert_eq!(cv.display, "Active");
251
252        // Test round-trip via as_custom_type
253        let decoded: TestStatus = value.as_custom_type::<TestStatus>().unwrap();
254        assert_eq!(decoded, TestStatus::Active);
255    }
256}