Skip to main content

nautilus_core/
value.rs

1//! Database value types.
2
3use std::str::FromStr;
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6
7/// Database value type.
8///
9/// Implements custom JSON serialization for cross-language compatibility:
10/// - `Decimal` -> string (avoid precision loss)
11/// - `DateTime` -> RFC3339 string
12/// - `Uuid` -> hyphenated lowercase string
13/// - `Bytes` -> base64 string
14#[derive(Debug, Clone, PartialEq)]
15pub enum Value {
16    /// NULL value.
17    Null,
18    /// Boolean.
19    Bool(bool),
20    /// 32-bit integer.
21    I32(i32),
22    /// 64-bit integer.
23    I64(i64),
24    /// 64-bit float.
25    F64(f64),
26    /// Decimal number with arbitrary precision.
27    Decimal(rust_decimal::Decimal),
28    /// Date and time (without timezone).
29    DateTime(chrono::NaiveDateTime),
30    /// UUID.
31    Uuid(uuid::Uuid),
32    /// JSON value.
33    Json(serde_json::Value),
34    /// String.
35    String(String),
36    /// Byte array.
37    Bytes(Vec<u8>),
38    /// Array of values (PostgreSQL native arrays).
39    Array(Vec<Value>),
40    /// 2D array of values (PostgreSQL multi-dimensional arrays).
41    Array2D(Vec<Vec<Value>>),
42    /// A database enum value with its PostgreSQL type name.
43    ///
44    /// Carries the variant string (e.g. `"ADMIN"`) together with the
45    /// lowercase PG type name (e.g. `"role"`) so that the PostgreSQL
46    /// dialect can emit the required explicit cast (`$1::role`).
47    /// All other backends treat this identically to `Value::String`.
48    Enum {
49        /// The enum variant string sent to / received from the DB.
50        value: String,
51        /// Lowercase PostgreSQL type name (e.g. `"role"`, `"poststatus"`).
52        type_name: String,
53    },
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57#[serde(tag = "type", content = "value", rename_all = "snake_case")]
58enum SerdeValue {
59    Null,
60    Bool(bool),
61    I32(i32),
62    I64(i64),
63    F64(f64),
64    Decimal(String),
65    DateTime(String),
66    Uuid(String),
67    Json(serde_json::Value),
68    String(String),
69    Bytes(String),
70    Array(Vec<Value>),
71    Array2D(Vec<Vec<Value>>),
72    Enum { value: String, type_name: String },
73}
74
75fn format_datetime(value: chrono::NaiveDateTime) -> String {
76    value.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string()
77}
78
79fn parse_datetime_string(raw: &str) -> std::result::Result<chrono::NaiveDateTime, String> {
80    chrono::DateTime::parse_from_rfc3339(raw)
81        .map(|value| value.naive_utc())
82        .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S%.f"))
83        .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%d %H:%M:%S%.f"))
84        .map_err(|_| format!("invalid datetime '{}'", raw))
85}
86
87impl From<&Value> for SerdeValue {
88    fn from(value: &Value) -> Self {
89        match value {
90            Value::Null => SerdeValue::Null,
91            Value::Bool(v) => SerdeValue::Bool(*v),
92            Value::I32(v) => SerdeValue::I32(*v),
93            Value::I64(v) => SerdeValue::I64(*v),
94            Value::F64(v) => SerdeValue::F64(*v),
95            Value::Decimal(v) => SerdeValue::Decimal(v.to_string()),
96            Value::DateTime(v) => SerdeValue::DateTime(format_datetime(*v)),
97            Value::Uuid(v) => SerdeValue::Uuid(v.to_string()),
98            Value::Json(v) => SerdeValue::Json(v.clone()),
99            Value::String(v) => SerdeValue::String(v.clone()),
100            Value::Bytes(v) => {
101                use base64::Engine;
102                SerdeValue::Bytes(base64::engine::general_purpose::STANDARD.encode(v))
103            }
104            Value::Array(v) => SerdeValue::Array(v.clone()),
105            Value::Array2D(v) => SerdeValue::Array2D(v.clone()),
106            Value::Enum { value, type_name } => SerdeValue::Enum {
107                value: value.clone(),
108                type_name: type_name.clone(),
109            },
110        }
111    }
112}
113
114impl TryFrom<SerdeValue> for Value {
115    type Error = String;
116
117    fn try_from(value: SerdeValue) -> std::result::Result<Self, Self::Error> {
118        match value {
119            SerdeValue::Null => Ok(Value::Null),
120            SerdeValue::Bool(v) => Ok(Value::Bool(v)),
121            SerdeValue::I32(v) => Ok(Value::I32(v)),
122            SerdeValue::I64(v) => Ok(Value::I64(v)),
123            SerdeValue::F64(v) => Ok(Value::F64(v)),
124            SerdeValue::Decimal(raw) => rust_decimal::Decimal::from_str(&raw)
125                .map(Value::Decimal)
126                .map_err(|e| format!("invalid decimal '{}': {}", raw, e)),
127            SerdeValue::DateTime(raw) => parse_datetime_string(&raw).map(Value::DateTime),
128            SerdeValue::Uuid(raw) => uuid::Uuid::parse_str(&raw)
129                .map(Value::Uuid)
130                .map_err(|e| format!("invalid uuid '{}': {}", raw, e)),
131            SerdeValue::Json(v) => Ok(Value::Json(v)),
132            SerdeValue::String(v) => Ok(Value::String(v)),
133            SerdeValue::Bytes(raw) => {
134                use base64::Engine;
135                base64::engine::general_purpose::STANDARD
136                    .decode(raw.as_bytes())
137                    .map(Value::Bytes)
138                    .map_err(|e| format!("invalid base64 bytes '{}': {}", raw, e))
139            }
140            SerdeValue::Array(v) => Ok(Value::Array(v)),
141            SerdeValue::Array2D(v) => Ok(Value::Array2D(v)),
142            SerdeValue::Enum { value, type_name } => Ok(Value::Enum { value, type_name }),
143        }
144    }
145}
146
147impl From<bool> for Value {
148    fn from(v: bool) -> Self {
149        Value::Bool(v)
150    }
151}
152
153impl From<i32> for Value {
154    fn from(v: i32) -> Self {
155        Value::I32(v)
156    }
157}
158
159impl From<i64> for Value {
160    fn from(v: i64) -> Self {
161        Value::I64(v)
162    }
163}
164
165impl From<f64> for Value {
166    fn from(v: f64) -> Self {
167        Value::F64(v)
168    }
169}
170
171impl From<rust_decimal::Decimal> for Value {
172    fn from(v: rust_decimal::Decimal) -> Self {
173        Value::Decimal(v)
174    }
175}
176
177impl From<chrono::NaiveDateTime> for Value {
178    fn from(v: chrono::NaiveDateTime) -> Self {
179        Value::DateTime(v)
180    }
181}
182
183impl From<uuid::Uuid> for Value {
184    fn from(v: uuid::Uuid) -> Self {
185        Value::Uuid(v)
186    }
187}
188
189impl From<serde_json::Value> for Value {
190    fn from(v: serde_json::Value) -> Self {
191        Value::Json(v)
192    }
193}
194
195impl From<String> for Value {
196    fn from(v: String) -> Self {
197        Value::String(v)
198    }
199}
200
201impl From<&str> for Value {
202    fn from(v: &str) -> Self {
203        Value::String(v.to_string())
204    }
205}
206
207impl From<Vec<u8>> for Value {
208    fn from(v: Vec<u8>) -> Self {
209        Value::Bytes(v)
210    }
211}
212
213// Array conversions — generated for all scalar types that map cleanly to Value
214// via `Into<Value>`. `Vec<u8>` is intentionally excluded: it maps to
215// `Value::Bytes`, not `Value::Array`.
216macro_rules! impl_vec_from {
217    ($($t:ty),* $(,)?) => {
218        $(
219            impl From<Vec<$t>> for Value {
220                fn from(v: Vec<$t>) -> Self {
221                    Value::Array(v.into_iter().map(|x| x.into()).collect())
222                }
223            }
224
225            impl From<Vec<Vec<$t>>> for Value {
226                fn from(v: Vec<Vec<$t>>) -> Self {
227                    Value::Array2D(
228                        v.into_iter()
229                            .map(|row| row.into_iter().map(|x| x.into()).collect())
230                            .collect(),
231                    )
232                }
233            }
234        )*
235    };
236}
237
238impl_vec_from!(
239    i32,
240    i64,
241    f64,
242    bool,
243    String,
244    rust_decimal::Decimal,
245    uuid::Uuid,
246    chrono::NaiveDateTime,
247    serde_json::Value,
248);
249
250// Option<T> conversions — map None -> Value::Null.
251// `Option<&str>` is kept manual because `&str` requires an explicit `.to_string()`
252// and does not implement `Into<Value>` through the generic `v.into()` path.
253macro_rules! impl_option_from {
254    ($($t:ty),* $(,)?) => {
255        $(
256            impl From<Option<$t>> for Value {
257                fn from(v: Option<$t>) -> Self {
258                    v.map(|x| x.into()).unwrap_or(Value::Null)
259                }
260            }
261        )*
262    };
263}
264
265impl_option_from!(
266    bool,
267    i32,
268    i64,
269    f64,
270    String,
271    rust_decimal::Decimal,
272    uuid::Uuid,
273    chrono::NaiveDateTime,
274);
275
276impl From<Option<&str>> for Value {
277    fn from(v: Option<&str>) -> Self {
278        v.map(|s| Value::String(s.to_string()))
279            .unwrap_or(Value::Null)
280    }
281}
282
283impl Value {
284    /// Convert this value into the plain JSON shape used on transport/wire paths.
285    ///
286    /// Unlike the serde representation of [`Value`] itself, this helper
287    /// intentionally mirrors the historic untagged encoding used by the engine
288    /// and generated raw-query helpers.
289    pub fn to_json_plain(&self) -> serde_json::Value {
290        match self {
291            Value::Null => serde_json::Value::Null,
292            Value::Bool(v) => serde_json::Value::Bool(*v),
293            Value::I32(v) => serde_json::Value::Number((*v).into()),
294            Value::I64(v) => serde_json::Value::Number((*v).into()),
295            Value::F64(v) => serde_json::Number::from_f64(*v)
296                .map(serde_json::Value::Number)
297                .unwrap_or(serde_json::Value::Null),
298            Value::Decimal(v) => serde_json::Value::String(v.to_string()),
299            Value::DateTime(v) => serde_json::Value::String(format_datetime(*v)),
300            Value::Uuid(v) => serde_json::Value::String(v.to_string()),
301            Value::Json(v) => v.clone(),
302            Value::String(v) => serde_json::Value::String(v.clone()),
303            Value::Bytes(v) => {
304                use base64::Engine;
305                serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(v))
306            }
307            Value::Array(v) => {
308                serde_json::Value::Array(v.iter().map(Value::to_json_plain).collect())
309            }
310            Value::Array2D(v) => serde_json::Value::Array(
311                v.iter()
312                    .map(|row| {
313                        serde_json::Value::Array(row.iter().map(Value::to_json_plain).collect())
314                    })
315                    .collect(),
316            ),
317            Value::Enum { value, .. } => serde_json::Value::String(value.clone()),
318        }
319    }
320}
321
322impl Serialize for Value {
323    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
324    where
325        S: Serializer,
326    {
327        SerdeValue::from(self).serialize(serializer)
328    }
329}
330
331/// Deserializes a [`Value`] from the tagged serde representation emitted by
332/// [`Serialize`] for [`Value`].
333impl<'de> Deserialize<'de> for Value {
334    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
335    where
336        D: Deserializer<'de>,
337    {
338        let tagged = SerdeValue::deserialize(deserializer)?;
339        Value::try_from(tagged).map_err(serde::de::Error::custom)
340    }
341}
342
343/// Convert a `&serde_json::Value` reference to a [`Value`].
344///
345/// This is the canonical JSON->Value conversion used throughout the crate.
346/// It is `pub(crate)` so that other modules (e.g. `column.rs`) can reuse it
347/// without duplicating the logic.
348///
349/// Numbers are coerced to `I32` before `I64` when they fit, then `F64`.
350/// Arrays of arrays are **not** auto-promoted to `Array2D` here; that
351/// promotion happens in the connector stream decoders where full schema
352/// knowledge is available.
353pub(crate) fn json_to_value_ref(json: &serde_json::Value) -> Value {
354    match json {
355        serde_json::Value::Null => Value::Null,
356        serde_json::Value::Bool(b) => Value::Bool(*b),
357        serde_json::Value::Number(n) => {
358            if let Some(i) = n.as_i64() {
359                if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
360                    Value::I32(i as i32)
361                } else {
362                    Value::I64(i)
363                }
364            } else if let Some(f) = n.as_f64() {
365                Value::F64(f)
366            } else {
367                Value::String(n.to_string())
368            }
369        }
370        serde_json::Value::String(s) => Value::String(s.clone()),
371        serde_json::Value::Array(arr) => Value::Array(arr.iter().map(json_to_value_ref).collect()),
372        serde_json::Value::Object(_) => Value::Json(json.clone()),
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use core::f64;
379
380    use super::*;
381
382    #[test]
383    fn test_value_variants() {
384        assert_eq!(Value::Null, Value::Null);
385        assert_eq!(Value::Bool(true), Value::from(true));
386        assert_eq!(Value::I32(42), Value::from(42i32));
387        assert_eq!(Value::I64(42), Value::from(42i64));
388        assert_eq!(Value::F64(2.5), Value::from(2.5f64));
389        assert_eq!(Value::String("hello".to_string()), Value::from("hello"));
390        assert_eq!(Value::Bytes(vec![1, 2, 3]), Value::from(vec![1u8, 2, 3]));
391
392        use rust_decimal::Decimal;
393        let dec = Decimal::new(12345, 2);
394        assert_eq!(Value::Decimal(dec), Value::from(dec));
395
396        use chrono::NaiveDate;
397        let dt = NaiveDate::from_ymd_opt(2024, 1, 1)
398            .unwrap()
399            .and_hms_opt(12, 0, 0)
400            .unwrap();
401        assert_eq!(Value::DateTime(dt), Value::from(dt));
402
403        use uuid::Uuid;
404        let id = Uuid::nil();
405        assert_eq!(Value::Uuid(id), Value::from(id));
406
407        use serde_json::json;
408        let j = json!({"key": "value"});
409        assert_eq!(Value::Json(j.clone()), Value::from(j));
410    }
411
412    #[test]
413    fn test_value_to_json_plain_primitives() {
414        assert_eq!(Value::Null.to_json_plain(), serde_json::Value::Null);
415        assert_eq!(
416            Value::Bool(true).to_json_plain(),
417            serde_json::Value::Bool(true)
418        );
419        assert_eq!(Value::I32(42).to_json_plain().as_i64(), Some(42));
420        assert_eq!(
421            Value::I64(9007199254740991).to_json_plain().as_i64(),
422            Some(9007199254740991)
423        );
424        assert_eq!(
425            Value::F64(f64::consts::PI).to_json_plain().as_f64(),
426            Some(f64::consts::PI)
427        );
428        assert_eq!(
429            Value::String("hello world".to_string())
430                .to_json_plain()
431                .as_str(),
432            Some("hello world")
433        );
434    }
435
436    #[test]
437    fn test_value_to_json_plain_special_scalars() {
438        use rust_decimal::Decimal;
439        let dec = Decimal::new(12345, 2);
440        use chrono::NaiveDate;
441        let dt = NaiveDate::from_ymd_opt(2026, 2, 18)
442            .unwrap()
443            .and_hms_opt(10, 30, 45)
444            .unwrap();
445        use uuid::Uuid;
446        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
447        assert_eq!(Value::Decimal(dec).to_json_plain().as_str(), Some("123.45"));
448        assert!(Value::DateTime(dt)
449            .to_json_plain()
450            .as_str()
451            .unwrap()
452            .starts_with("2026-02-18T10:30:45"));
453        assert_eq!(
454            Value::Uuid(id).to_json_plain().as_str(),
455            Some("550e8400-e29b-41d4-a716-446655440000")
456        );
457        assert_eq!(
458            Value::Bytes(vec![72, 101, 108, 108, 111])
459                .to_json_plain()
460                .as_str(),
461            Some("SGVsbG8=")
462        );
463    }
464
465    #[test]
466    fn test_value_to_json_plain_json_and_arrays() {
467        use serde_json::json;
468        let object = json!({"name": "Alice", "age": 30});
469        assert_eq!(Value::Json(object.clone()).to_json_plain(), object);
470
471        let value = Value::Array(vec![
472            Value::String("a".to_string()),
473            Value::String("b".to_string()),
474            Value::String("c".to_string()),
475        ]);
476
477        let json = value.to_json_plain();
478        assert_eq!(json[0].as_str(), Some("a"));
479        assert_eq!(json[1].as_str(), Some("b"));
480        assert_eq!(json[2].as_str(), Some("c"));
481    }
482
483    #[test]
484    fn test_value_plain_json_array2d_roundtrip_stays_untyped_without_schema() {
485        let value = Value::Array2D(vec![
486            vec![Value::I32(1), Value::I32(2)],
487            vec![Value::I32(3), Value::I32(4)],
488        ]);
489
490        let json = value.to_json_plain();
491        assert_eq!(json[0][0].as_i64(), Some(1));
492        assert_eq!(json[0][1].as_i64(), Some(2));
493        assert_eq!(json[1][0].as_i64(), Some(3));
494        assert_eq!(json[1][1].as_i64(), Some(4));
495
496        // Deserialization: without schema context the `Array2D` heuristic is
497        // intentionally absent from `json_to_value_ref`. A nested JSON array
498        // round-trips as `Array(Array(_))`. Promotion to `Array2D` is the
499        // connector stream's responsibility.
500        let expected = Value::Array(vec![
501            Value::Array(vec![Value::I32(1), Value::I32(2)]),
502            Value::Array(vec![Value::I32(3), Value::I32(4)]),
503        ]);
504        assert_eq!(json_to_value_ref(&json), expected);
505    }
506
507    #[test]
508    fn test_tagged_serde_shape_is_explicit() {
509        let value = Value::Decimal(rust_decimal::Decimal::new(12345, 2));
510        let json = serde_json::to_value(&value).unwrap();
511
512        assert_eq!(
513            json,
514            serde_json::json!({
515                "type": "decimal",
516                "value": "123.45"
517            })
518        );
519    }
520
521    #[test]
522    fn test_tagged_serde_round_trip_preserves_typed_variants() {
523        use chrono::NaiveDate;
524        use serde_json::json;
525        use uuid::Uuid;
526
527        let values = vec![
528            Value::Null,
529            Value::Bool(false),
530            Value::I32(-42),
531            Value::I64(9007199254740991), // Large I64 beyond i32 range
532            Value::F64(f64::consts::E),
533            Value::Decimal(rust_decimal::Decimal::new(314, 2)),
534            Value::DateTime(
535                NaiveDate::from_ymd_opt(2026, 2, 18)
536                    .unwrap()
537                    .and_hms_opt(10, 30, 45)
538                    .unwrap(),
539            ),
540            Value::Uuid(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()),
541            Value::Bytes(vec![1, 2, 3, 4]),
542            Value::Json(json!({"ok": true})),
543            Value::String("test".to_string()),
544            Value::Array(vec![Value::I32(1), Value::I32(2)]),
545            Value::Array2D(vec![vec![Value::I32(1), Value::I32(2)]]),
546            Value::Enum {
547                value: "ADMIN".to_string(),
548                type_name: "role".to_string(),
549            },
550        ];
551
552        for value in values {
553            let json = serde_json::to_value(&value).unwrap();
554            let deserialized: Value = serde_json::from_value(json).unwrap();
555            assert_eq!(deserialized, value);
556        }
557    }
558}