Skip to main content

nodedb_types/
value.rs

1//! Dynamic value type for document fields and SQL parameters.
2//!
3//! Covers the value types needed for AI agent workloads: strings, numbers,
4//! booleans, binary blobs (embeddings), arrays, and nested objects.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::datetime::{NdbDateTime, NdbDuration};
11use crate::geometry::Geometry;
12
13/// A dynamic value that can represent any field type in a document
14/// or any parameter in a SQL query.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub enum Value {
17    /// SQL NULL / missing value.
18    Null,
19    /// Boolean.
20    Bool(bool),
21    /// Signed 64-bit integer.
22    Integer(i64),
23    /// 64-bit floating point.
24    Float(f64),
25    /// UTF-8 string.
26    String(String),
27    /// Raw bytes (embeddings, serialized blobs).
28    Bytes(Vec<u8>),
29    /// Ordered array of values.
30    Array(Vec<Value>),
31    /// Nested key-value object.
32    Object(HashMap<String, Value>),
33    /// UUID (any version, stored as 36-char hyphenated string).
34    Uuid(String),
35    /// ULID (26-char Crockford Base32).
36    Ulid(String),
37    /// UTC timestamp with microsecond precision.
38    DateTime(NdbDateTime),
39    /// Duration with microsecond precision (signed).
40    Duration(NdbDuration),
41    /// Arbitrary-precision decimal (financial calculations, exact arithmetic).
42    Decimal(rust_decimal::Decimal),
43    /// GeoJSON-compatible geometry (Point, LineString, Polygon, etc.).
44    Geometry(Geometry),
45    /// Ordered set of unique values (auto-deduplicated, maintains insertion order).
46    Set(Vec<Value>),
47    /// Compiled regex pattern (stored as pattern string).
48    Regex(String),
49    /// A range of values with optional bounds.
50    Range {
51        /// Start bound (None = unbounded).
52        start: Option<Box<Value>>,
53        /// End bound (None = unbounded).
54        end: Option<Box<Value>>,
55        /// Whether the end bound is inclusive (`..=` vs `..`).
56        inclusive: bool,
57    },
58    /// A typed reference to another record: `table:id`.
59    Record {
60        /// The table/collection name.
61        table: String,
62        /// The record's document ID.
63        id: String,
64    },
65}
66
67impl Value {
68    /// Returns true if this value is `Null`.
69    pub fn is_null(&self) -> bool {
70        matches!(self, Value::Null)
71    }
72
73    /// Try to extract as a string reference.
74    pub fn as_str(&self) -> Option<&str> {
75        match self {
76            Value::String(s) => Some(s),
77            _ => None,
78        }
79    }
80
81    /// Try to extract as i64.
82    pub fn as_i64(&self) -> Option<i64> {
83        match self {
84            Value::Integer(i) => Some(*i),
85            _ => None,
86        }
87    }
88
89    /// Try to extract as f64.
90    pub fn as_f64(&self) -> Option<f64> {
91        match self {
92            Value::Float(f) => Some(*f),
93            Value::Integer(i) => Some(*i as f64),
94            _ => None,
95        }
96    }
97
98    /// Try to extract as bool.
99    pub fn as_bool(&self) -> Option<bool> {
100        match self {
101            Value::Bool(b) => Some(*b),
102            _ => None,
103        }
104    }
105
106    /// Try to extract as byte slice (for embeddings).
107    pub fn as_bytes(&self) -> Option<&[u8]> {
108        match self {
109            Value::Bytes(b) => Some(b),
110            _ => None,
111        }
112    }
113
114    /// Try to extract as UUID string.
115    pub fn as_uuid(&self) -> Option<&str> {
116        match self {
117            Value::Uuid(s) => Some(s),
118            _ => None,
119        }
120    }
121
122    /// Try to extract as ULID string.
123    pub fn as_ulid(&self) -> Option<&str> {
124        match self {
125            Value::Ulid(s) => Some(s),
126            _ => None,
127        }
128    }
129
130    /// Try to extract as DateTime.
131    pub fn as_datetime(&self) -> Option<&NdbDateTime> {
132        match self {
133            Value::DateTime(dt) => Some(dt),
134            _ => None,
135        }
136    }
137
138    /// Try to extract as Duration.
139    pub fn as_duration(&self) -> Option<&NdbDuration> {
140        match self {
141            Value::Duration(d) => Some(d),
142            _ => None,
143        }
144    }
145
146    /// Try to extract as Decimal.
147    pub fn as_decimal(&self) -> Option<&rust_decimal::Decimal> {
148        match self {
149            Value::Decimal(d) => Some(d),
150            _ => None,
151        }
152    }
153
154    /// Try to extract as Geometry.
155    pub fn as_geometry(&self) -> Option<&Geometry> {
156        match self {
157            Value::Geometry(g) => Some(g),
158            _ => None,
159        }
160    }
161
162    /// Try to extract as a set (deduplicated array).
163    pub fn as_set(&self) -> Option<&[Value]> {
164        match self {
165            Value::Set(s) => Some(s),
166            _ => None,
167        }
168    }
169
170    /// Try to extract as regex pattern string.
171    pub fn as_regex(&self) -> Option<&str> {
172        match self {
173            Value::Regex(r) => Some(r),
174            _ => None,
175        }
176    }
177
178    /// Try to extract as a record reference (table, id).
179    pub fn as_record(&self) -> Option<(&str, &str)> {
180        match self {
181            Value::Record { table, id } => Some((table, id)),
182            _ => None,
183        }
184    }
185
186    /// Return the type name of this value as a string.
187    pub fn type_name(&self) -> &'static str {
188        match self {
189            Value::Null => "null",
190            Value::Bool(_) => "bool",
191            Value::Integer(_) => "int",
192            Value::Float(_) => "float",
193            Value::String(_) => "string",
194            Value::Bytes(_) => "bytes",
195            Value::Array(_) => "array",
196            Value::Object(_) => "object",
197            Value::Uuid(_) => "uuid",
198            Value::Ulid(_) => "ulid",
199            Value::DateTime(_) => "datetime",
200            Value::Duration(_) => "duration",
201            Value::Decimal(_) => "decimal",
202            Value::Geometry(_) => "geometry",
203            Value::Set(_) => "set",
204            Value::Regex(_) => "regex",
205            Value::Range { .. } => "range",
206            Value::Record { .. } => "record",
207        }
208    }
209}
210
211/// Convenience conversions.
212impl From<&str> for Value {
213    fn from(s: &str) -> Self {
214        Value::String(s.to_owned())
215    }
216}
217
218impl From<String> for Value {
219    fn from(s: String) -> Self {
220        Value::String(s)
221    }
222}
223
224impl From<i64> for Value {
225    fn from(i: i64) -> Self {
226        Value::Integer(i)
227    }
228}
229
230impl From<f64> for Value {
231    fn from(f: f64) -> Self {
232        Value::Float(f)
233    }
234}
235
236impl From<bool> for Value {
237    fn from(b: bool) -> Self {
238        Value::Bool(b)
239    }
240}
241
242impl From<Vec<u8>> for Value {
243    fn from(b: Vec<u8>) -> Self {
244        Value::Bytes(b)
245    }
246}
247
248impl From<NdbDateTime> for Value {
249    fn from(dt: NdbDateTime) -> Self {
250        Value::DateTime(dt)
251    }
252}
253
254impl From<NdbDuration> for Value {
255    fn from(d: NdbDuration) -> Self {
256        Value::Duration(d)
257    }
258}
259
260impl From<rust_decimal::Decimal> for Value {
261    fn from(d: rust_decimal::Decimal) -> Self {
262        Value::Decimal(d)
263    }
264}
265
266impl From<Geometry> for Value {
267    fn from(g: Geometry) -> Self {
268        Value::Geometry(g)
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn value_type_checks() {
278        assert!(Value::Null.is_null());
279        assert!(!Value::Bool(true).is_null());
280
281        assert_eq!(Value::String("hi".into()).as_str(), Some("hi"));
282        assert_eq!(Value::Integer(42).as_i64(), Some(42));
283        assert_eq!(Value::Float(2.78).as_f64(), Some(2.78));
284        assert_eq!(Value::Integer(10).as_f64(), Some(10.0));
285        assert_eq!(Value::Bool(true).as_bool(), Some(true));
286        assert_eq!(Value::Bytes(vec![1, 2]).as_bytes(), Some(&[1, 2][..]));
287    }
288
289    #[test]
290    fn from_conversions() {
291        let s: Value = "hello".into();
292        assert_eq!(s.as_str(), Some("hello"));
293
294        let i: Value = 42i64.into();
295        assert_eq!(i.as_i64(), Some(42));
296
297        let f: Value = 2.78f64.into();
298        assert_eq!(f.as_f64(), Some(2.78));
299    }
300
301    #[test]
302    fn nested_value() {
303        let nested = Value::Object({
304            let mut m = HashMap::new();
305            m.insert(
306                "inner".into(),
307                Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
308            );
309            m
310        });
311        assert!(!nested.is_null());
312    }
313}