Skip to main content

wasm_dbms_api/dbms/
value.rs

1use std::str::FromStr;
2use std::sync::OnceLock;
3
4use serde::{Deserialize, Serialize};
5
6use super::types;
7
8/// A generic wrapper enum to hold any DBMS value.
9#[cfg_attr(feature = "candid", derive(candid::CandidType))]
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11pub enum Value {
12    Blob(types::Blob),
13    Boolean(types::Boolean),
14    Date(types::Date),
15    DateTime(types::DateTime),
16    Decimal(types::Decimal),
17    Int8(types::Int8),
18    Int16(types::Int16),
19    Int32(types::Int32),
20    Int64(types::Int64),
21    Json(types::Json),
22    Null,
23    Text(types::Text),
24    Uint8(types::Uint8),
25    Uint16(types::Uint16),
26    Uint32(types::Uint32),
27    Uint64(types::Uint64),
28    Uuid(types::Uuid),
29    Custom(crate::dbms::custom_value::CustomValue),
30}
31
32impl FromStr for Value {
33    type Err = ();
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        Ok(Self::Text(s.into()))
37    }
38}
39
40// macro rules for implementing From trait for Value enum variants
41macro_rules! impl_conv_for_value {
42    ($variant:ident, $ty:ty, $name:ident, $test_name:ident) => {
43        impl From<$ty> for Value {
44            fn from(value: $ty) -> Self {
45                Value::$variant(value)
46            }
47        }
48
49        impl Value {
50            /// Attempts to extract a reference to the inner value if it matches the variant.
51            pub fn $name(&self) -> Option<&$ty> {
52                if let Value::$variant(v) = self {
53                    Some(v)
54                } else {
55                    None
56                }
57            }
58        }
59
60        #[cfg(test)]
61        mod $test_name {
62            use super::*;
63
64            #[test]
65            fn test_value_conversion() {
66                let value_instance: $ty = Default::default();
67                let value: Value = value_instance.clone().into();
68                assert_eq!(value.$name(), Some(&value_instance));
69            }
70        }
71    };
72}
73
74macro_rules! value_from_primitive {
75    ($variant:ident, $primitive:ty, $test_name:ident) => {
76        value_from_primitive!($variant, $primitive, $test_name, Default::default());
77    };
78
79    ($variant:ident, $primitive:ty, $test_name:ident, $default_value:expr) => {
80        impl From<$primitive> for Value {
81            fn from(value: $primitive) -> Self {
82                Value::$variant($crate::prelude::$variant(value.into()))
83            }
84        }
85
86        #[cfg(test)]
87        mod $test_name {
88            use super::*;
89
90            #[test]
91            fn test_value_from_primitive() {
92                let primitive_value: $primitive = $default_value;
93                if let Value::$variant(inner_value) = Value::from(primitive_value.clone()) {
94                    assert_eq!(inner_value.0, primitive_value);
95                } else {
96                    panic!("Value variant does not match");
97                }
98            }
99        }
100    };
101}
102
103// implement conversions for all Value variants
104impl_conv_for_value!(Blob, types::Blob, as_blob, tests_blob);
105impl_conv_for_value!(Boolean, types::Boolean, as_boolean, tests_boolean);
106impl_conv_for_value!(Date, types::Date, as_date, tests_date);
107impl_conv_for_value!(DateTime, types::DateTime, as_datetime, tests_datetime);
108impl_conv_for_value!(Decimal, types::Decimal, as_decimal, tests_decimal);
109impl_conv_for_value!(Int8, types::Int8, as_int8, tests_int8);
110impl_conv_for_value!(Int16, types::Int16, as_int16, tests_int16);
111impl_conv_for_value!(Int32, types::Int32, as_int32, tests_int32);
112impl_conv_for_value!(Int64, types::Int64, as_int64, tests_int64);
113impl_conv_for_value!(Json, types::Json, as_json, tests_json);
114impl_conv_for_value!(Text, types::Text, as_text, tests_text);
115impl_conv_for_value!(Uint8, types::Uint8, as_uint8, tests_uint8);
116impl_conv_for_value!(Uint16, types::Uint16, as_uint16, tests_uint16);
117impl_conv_for_value!(Uint32, types::Uint32, as_uint32, tests_uint32);
118impl_conv_for_value!(Uint64, types::Uint64, as_uint64, tests_uint64);
119impl_conv_for_value!(Uuid, types::Uuid, as_uuid, tests_uuid);
120
121// from inner values of types
122value_from_primitive!(Blob, &[u8], tests_blob_primitive_slice);
123value_from_primitive!(Blob, Vec<u8>, tests_blob_primitive);
124value_from_primitive!(Boolean, bool, tests_boolean_primitive);
125value_from_primitive!(Decimal, rust_decimal::Decimal, tests_decimal_primitive);
126value_from_primitive!(Int8, i8, tests_int8_primitive);
127value_from_primitive!(Int16, i16, tests_int16_primitive);
128value_from_primitive!(Int32, i32, tests_int32_primitive);
129value_from_primitive!(Int64, i64, tests_int64_primitive);
130value_from_primitive!(Uint8, u8, tests_uint8_primitive);
131value_from_primitive!(Uint16, u16, tests_uint16_primitive);
132value_from_primitive!(Uint32, u32, tests_uint32_primitive);
133value_from_primitive!(Uint64, u64, tests_uint64_primitive);
134value_from_primitive!(Text, String, tests_text_primitive_string);
135value_from_primitive!(Text, &str, tests_text_primitive_str);
136value_from_primitive!(Uuid, uuid::Uuid, tests_uuid_primitive);
137
138impl Value {
139    /// Checks if the value is [`Value::Null`].
140    pub fn is_null(&self) -> bool {
141        matches!(self, Value::Null)
142    }
143
144    /// Returns the type name of the value as a string.
145    pub fn type_name(&self) -> &'static str {
146        match self {
147            Value::Blob(_) => "Blob",
148            Value::Boolean(_) => "Boolean",
149            Value::Date(_) => "Date",
150            Value::DateTime(_) => "DateTime",
151            Value::Decimal(_) => "Decimal",
152            Value::Int8(_) => "Int8",
153            Value::Int16(_) => "Int16",
154            Value::Int32(_) => "Int32",
155            Value::Int64(_) => "Int64",
156            Value::Json(_) => "Json",
157            Value::Null => "Null",
158            Value::Text(_) => "Text",
159            Value::Uint8(_) => "Uint8",
160            Value::Uint16(_) => "Uint16",
161            Value::Uint32(_) => "Uint32",
162            Value::Uint64(_) => "Uint64",
163            Value::Uuid(_) => "Uuid",
164            Value::Custom(cv) => {
165                // Cache custom type names to avoid repeated allocations.
166                // The number of unique type tags is bounded at compile time,
167                // so the map grows to a fixed size. A maximum of 64 entries
168                // is enforced as a safety guard against unbounded memory usage
169                // on the IC, where heap is a scarce resource.
170                const MAX_CACHE_ENTRIES: usize = 64;
171                static CACHE: OnceLock<
172                    std::sync::Mutex<std::collections::HashMap<String, &'static str>>,
173                > = OnceLock::new();
174                let cache =
175                    CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
176                let mut map = cache.lock().unwrap_or_else(|e| e.into_inner());
177                if map.len() >= MAX_CACHE_ENTRIES && !map.contains_key(&cv.type_tag) {
178                    return "Custom(?)";
179                }
180                map.entry(cv.type_tag.clone()).or_insert_with(|| {
181                    let s = format!("Custom({})", cv.type_tag);
182                    s.leak()
183                })
184            }
185        }
186    }
187
188    /// Returns reference to the inner [`CustomValue`] if this is a `Custom` variant.
189    pub fn as_custom(&self) -> Option<&crate::dbms::custom_value::CustomValue> {
190        match self {
191            Value::Custom(v) => Some(v),
192            _ => None,
193        }
194    }
195
196    /// Attempts to decode a `Custom` variant into a concrete [`CustomDataType`](crate::dbms::types::CustomDataType).
197    ///
198    /// Returns `None` if this is not a `Custom` variant, the type tag doesn't
199    /// match, or decoding fails.
200    pub fn as_custom_type<T: crate::dbms::types::CustomDataType>(&self) -> Option<T> {
201        self.as_custom()
202            .filter(|cv| cv.type_tag == T::TYPE_TAG)
203            .and_then(|cv| T::decode(std::borrow::Cow::Borrowed(&cv.encoded)).ok())
204    }
205}
206
207#[cfg(test)]
208mod tests {
209
210    use uuid::Uuid;
211
212    use super::*;
213
214    #[test]
215    fn test_null() {
216        let int_value: Value = types::Int32(42).into();
217        assert!(!int_value.is_null());
218
219        let null_value = Value::Null;
220        assert!(null_value.is_null());
221    }
222
223    #[test]
224    fn test_value_conversion_blob() {
225        let blob = types::Blob(vec![1, 2, 3]);
226        let value: Value = blob.clone().into();
227        assert_eq!(value.as_blob(), Some(&blob));
228    }
229
230    #[test]
231    fn test_value_conversion_boolean() {
232        let boolean = types::Boolean(true);
233        let value: Value = boolean.into();
234        assert_eq!(value.as_boolean(), Some(&boolean));
235    }
236
237    #[test]
238    fn test_value_conversion_date() {
239        let date = types::Date {
240            year: 2023,
241            month: 3,
242            day: 15,
243        }; // Example date
244        let value: Value = date.into();
245        assert_eq!(value.as_date(), Some(&date));
246    }
247
248    #[test]
249    fn test_value_conversion_datetime() {
250        let datetime = types::DateTime {
251            year: 2023,
252            month: 3,
253            day: 15,
254            hour: 12,
255            minute: 30,
256            second: 45,
257            microsecond: 123456,
258            timezone_offset_minutes: 0,
259        }; // Example datetime
260        let value: Value = datetime.into();
261        assert_eq!(value.as_datetime(), Some(&datetime));
262    }
263
264    #[test]
265    fn test_value_conversion_decimal() {
266        let decimal = types::Decimal(rust_decimal::Decimal::new(12345, 2)); // 123.45
267        let value: Value = decimal.into();
268        assert_eq!(value.as_decimal(), Some(&decimal));
269    }
270
271    #[test]
272    fn test_value_conversion_int32() {
273        let int32 = types::Int32(1234567890);
274        let value: Value = int32.into();
275        assert_eq!(value.as_int32(), Some(&int32));
276    }
277
278    #[test]
279    fn test_value_conversion_int64() {
280        let int64 = types::Int64(1234567890);
281        let value: Value = int64.into();
282        assert_eq!(value.as_int64(), Some(&int64));
283    }
284
285    #[test]
286    fn test_value_conversion_text() {
287        let text = types::Text("Hello, World!".to_string());
288        let value: Value = text.clone().into();
289        assert_eq!(value.as_text(), Some(&text));
290    }
291
292    #[test]
293    fn test_value_conversion_uint32() {
294        let uint32 = types::Uint32(123456);
295        let value: Value = uint32.into();
296        assert_eq!(value.as_uint32(), Some(&uint32));
297    }
298
299    #[test]
300    fn test_value_conversion_uint64() {
301        let uint64 = types::Uint64(12345678901234);
302        let value: Value = uint64.into();
303        assert_eq!(value.as_uint64(), Some(&uint64));
304    }
305
306    #[test]
307    fn test_value_conversion_uuid() {
308        let uuid = types::Uuid(
309            Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").expect("failed to parse uuid"),
310        );
311        let value: Value = uuid.clone().into();
312        assert_eq!(value.as_uuid(), Some(&uuid));
313    }
314
315    #[test]
316    fn test_value_type_name() {
317        let int_value: Value = types::Int32(42).into();
318        assert_eq!(int_value.type_name(), "Int32");
319
320        let text_value: Value = types::Text("Hello".to_string()).into();
321        assert_eq!(text_value.type_name(), "Text");
322
323        let null_value = Value::Null;
324        assert_eq!(null_value.type_name(), "Null");
325    }
326
327    #[test]
328    fn test_value_from_str() {
329        let str_value = "Hello, DBMS!";
330
331        let value = Value::from_str(str_value).unwrap();
332        assert_eq!(value.as_text().unwrap().0, str_value);
333    }
334
335    #[test]
336    fn test_should_create_custom_value() {
337        let cv = crate::dbms::custom_value::CustomValue {
338            type_tag: "role".to_string(),
339            encoded: vec![0x01],
340            display: "Admin".to_string(),
341        };
342        let value = Value::Custom(cv.clone());
343        assert_eq!(value.as_custom(), Some(&cv));
344    }
345
346    #[test]
347    fn test_should_return_none_for_non_custom() {
348        let value = Value::Null;
349        assert_eq!(value.as_custom(), None);
350    }
351
352    #[test]
353    fn test_should_compare_custom_values() {
354        let a = Value::Custom(crate::dbms::custom_value::CustomValue {
355            type_tag: "role".to_string(),
356            encoded: vec![0x01],
357            display: "Admin".to_string(),
358        });
359        let b = Value::Custom(crate::dbms::custom_value::CustomValue {
360            type_tag: "role".to_string(),
361            encoded: vec![0x01],
362            display: "Admin".to_string(),
363        });
364        assert_eq!(a, b);
365    }
366
367    #[test]
368    fn test_should_order_custom_after_builtin() {
369        let builtin = Value::Uuid(types::Uuid::default());
370        let custom = Value::Custom(crate::dbms::custom_value::CustomValue {
371            type_tag: "role".to_string(),
372            encoded: vec![0x01],
373            display: "Admin".to_string(),
374        });
375        assert!(builtin < custom);
376    }
377
378    #[test]
379    fn test_should_get_custom_type_name() {
380        let cv = Value::Custom(crate::dbms::custom_value::CustomValue {
381            type_tag: "role".to_string(),
382            encoded: vec![0x01],
383            display: "Admin".to_string(),
384        });
385        assert_eq!(cv.type_name(), "Custom(role)");
386    }
387}