Skip to main content

nautilus_core/
value.rs

1//! Database value types.
2
3use std::collections::BTreeMap;
4use std::str::FromStr;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8/// PostGIS `geometry` value represented as textual WKT/EWKT or EWKB hex.
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct Geometry(String);
12
13impl Geometry {
14    /// Create a geometry value from its textual representation.
15    pub fn new(value: impl Into<String>) -> Self {
16        Self(value.into())
17    }
18
19    /// Borrow the underlying textual representation.
20    pub fn as_str(&self) -> &str {
21        &self.0
22    }
23
24    /// Consume the wrapper and return the underlying textual representation.
25    pub fn into_inner(self) -> String {
26        self.0
27    }
28}
29
30impl From<String> for Geometry {
31    fn from(value: String) -> Self {
32        Self(value)
33    }
34}
35
36impl From<&str> for Geometry {
37    fn from(value: &str) -> Self {
38        Self(value.to_string())
39    }
40}
41
42impl AsRef<str> for Geometry {
43    fn as_ref(&self) -> &str {
44        self.as_str()
45    }
46}
47
48impl std::fmt::Display for Geometry {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.write_str(self.as_str())
51    }
52}
53
54/// PostGIS `geography` value represented as textual WKT/EWKT or EWKB hex.
55#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
56#[serde(transparent)]
57pub struct Geography(String);
58
59impl Geography {
60    /// Create a geography value from its textual representation.
61    pub fn new(value: impl Into<String>) -> Self {
62        Self(value.into())
63    }
64
65    /// Borrow the underlying textual representation.
66    pub fn as_str(&self) -> &str {
67        &self.0
68    }
69
70    /// Consume the wrapper and return the underlying textual representation.
71    pub fn into_inner(self) -> String {
72        self.0
73    }
74}
75
76impl From<String> for Geography {
77    fn from(value: String) -> Self {
78        Self(value)
79    }
80}
81
82impl From<&str> for Geography {
83    fn from(value: &str) -> Self {
84        Self(value.to_string())
85    }
86}
87
88impl AsRef<str> for Geography {
89    fn as_ref(&self) -> &str {
90        self.as_str()
91    }
92}
93
94impl std::fmt::Display for Geography {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        f.write_str(self.as_str())
97    }
98}
99
100/// Database value type.
101///
102/// Implements custom JSON serialization for cross-language compatibility:
103/// - `Decimal` -> string (avoid precision loss)
104/// - `DateTime` -> RFC3339 string
105/// - `Uuid` -> hyphenated lowercase string
106/// - `Bytes` -> base64 string
107#[derive(Debug, Clone, PartialEq)]
108pub enum Value {
109    /// NULL value.
110    Null,
111    /// Boolean.
112    Bool(bool),
113    /// 32-bit integer.
114    I32(i32),
115    /// 64-bit integer.
116    I64(i64),
117    /// 64-bit float.
118    F64(f64),
119    /// Decimal number with arbitrary precision.
120    Decimal(rust_decimal::Decimal),
121    /// Date and time (without timezone).
122    DateTime(chrono::NaiveDateTime),
123    /// UUID.
124    Uuid(uuid::Uuid),
125    /// JSON value.
126    Json(serde_json::Value),
127    /// PostgreSQL hstore key/value map.
128    Hstore(BTreeMap<String, Option<String>>),
129    /// PostgreSQL PostGIS geometry value.
130    Geometry(String),
131    /// PostgreSQL PostGIS geography value.
132    Geography(String),
133    /// PostgreSQL pgvector dense embedding vector.
134    Vector(Vec<f32>),
135    /// String.
136    String(String),
137    /// Byte array.
138    Bytes(Vec<u8>),
139    /// Array of values (PostgreSQL native arrays).
140    Array(Vec<Value>),
141    /// 2D array of values (PostgreSQL multi-dimensional arrays).
142    Array2D(Vec<Vec<Value>>),
143    /// A database enum value with its PostgreSQL type name.
144    ///
145    /// Carries the variant string (e.g. `"ADMIN"`) together with the
146    /// lowercase PG type name (e.g. `"role"`) so that the PostgreSQL
147    /// dialect can emit the required explicit cast (`$1::role`).
148    /// All other backends treat this identically to `Value::String`.
149    Enum {
150        /// The enum variant string sent to / received from the DB.
151        value: String,
152        /// Lowercase PostgreSQL type name (e.g. `"role"`, `"poststatus"`).
153        type_name: String,
154    },
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[serde(tag = "type", content = "value", rename_all = "snake_case")]
159enum SerdeValue {
160    Null,
161    Bool(bool),
162    I32(i32),
163    I64(i64),
164    F64(f64),
165    Decimal(String),
166    DateTime(String),
167    Uuid(String),
168    Json(serde_json::Value),
169    Hstore(BTreeMap<String, Option<String>>),
170    Geometry(String),
171    Geography(String),
172    Vector(Vec<f32>),
173    String(String),
174    Bytes(String),
175    Array(Vec<Value>),
176    Array2D(Vec<Vec<Value>>),
177    Enum { value: String, type_name: String },
178}
179
180fn format_datetime(value: chrono::NaiveDateTime) -> String {
181    value.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string()
182}
183
184fn parse_datetime_string(raw: &str) -> std::result::Result<chrono::NaiveDateTime, String> {
185    chrono::DateTime::parse_from_rfc3339(raw)
186        .map(|value| value.naive_utc())
187        .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S%.f"))
188        .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%d %H:%M:%S%.f"))
189        .map_err(|_| format!("invalid datetime '{}'", raw))
190}
191
192impl From<&Value> for SerdeValue {
193    fn from(value: &Value) -> Self {
194        match value {
195            Value::Null => SerdeValue::Null,
196            Value::Bool(v) => SerdeValue::Bool(*v),
197            Value::I32(v) => SerdeValue::I32(*v),
198            Value::I64(v) => SerdeValue::I64(*v),
199            Value::F64(v) => SerdeValue::F64(*v),
200            Value::Decimal(v) => SerdeValue::Decimal(v.to_string()),
201            Value::DateTime(v) => SerdeValue::DateTime(format_datetime(*v)),
202            Value::Uuid(v) => SerdeValue::Uuid(v.to_string()),
203            Value::Json(v) => SerdeValue::Json(v.clone()),
204            Value::Hstore(v) => SerdeValue::Hstore(v.clone()),
205            Value::Geometry(v) => SerdeValue::Geometry(v.clone()),
206            Value::Geography(v) => SerdeValue::Geography(v.clone()),
207            Value::Vector(v) => SerdeValue::Vector(v.clone()),
208            Value::String(v) => SerdeValue::String(v.clone()),
209            Value::Bytes(v) => {
210                use base64::Engine;
211                SerdeValue::Bytes(base64::engine::general_purpose::STANDARD.encode(v))
212            }
213            Value::Array(v) => SerdeValue::Array(v.clone()),
214            Value::Array2D(v) => SerdeValue::Array2D(v.clone()),
215            Value::Enum { value, type_name } => SerdeValue::Enum {
216                value: value.clone(),
217                type_name: type_name.clone(),
218            },
219        }
220    }
221}
222
223impl TryFrom<SerdeValue> for Value {
224    type Error = String;
225
226    fn try_from(value: SerdeValue) -> std::result::Result<Self, Self::Error> {
227        match value {
228            SerdeValue::Null => Ok(Value::Null),
229            SerdeValue::Bool(v) => Ok(Value::Bool(v)),
230            SerdeValue::I32(v) => Ok(Value::I32(v)),
231            SerdeValue::I64(v) => Ok(Value::I64(v)),
232            SerdeValue::F64(v) => Ok(Value::F64(v)),
233            SerdeValue::Decimal(raw) => rust_decimal::Decimal::from_str(&raw)
234                .map(Value::Decimal)
235                .map_err(|e| format!("invalid decimal '{}': {}", raw, e)),
236            SerdeValue::DateTime(raw) => parse_datetime_string(&raw).map(Value::DateTime),
237            SerdeValue::Uuid(raw) => uuid::Uuid::parse_str(&raw)
238                .map(Value::Uuid)
239                .map_err(|e| format!("invalid uuid '{}': {}", raw, e)),
240            SerdeValue::Json(v) => Ok(Value::Json(v)),
241            SerdeValue::Hstore(v) => Ok(Value::Hstore(v)),
242            SerdeValue::Geometry(v) => Ok(Value::Geometry(v)),
243            SerdeValue::Geography(v) => Ok(Value::Geography(v)),
244            SerdeValue::Vector(v) => Ok(Value::Vector(v)),
245            SerdeValue::String(v) => Ok(Value::String(v)),
246            SerdeValue::Bytes(raw) => {
247                use base64::Engine;
248                base64::engine::general_purpose::STANDARD
249                    .decode(raw.as_bytes())
250                    .map(Value::Bytes)
251                    .map_err(|e| format!("invalid base64 bytes '{}': {}", raw, e))
252            }
253            SerdeValue::Array(v) => Ok(Value::Array(v)),
254            SerdeValue::Array2D(v) => Ok(Value::Array2D(v)),
255            SerdeValue::Enum { value, type_name } => Ok(Value::Enum { value, type_name }),
256        }
257    }
258}
259
260impl From<bool> for Value {
261    fn from(v: bool) -> Self {
262        Value::Bool(v)
263    }
264}
265
266impl From<i32> for Value {
267    fn from(v: i32) -> Self {
268        Value::I32(v)
269    }
270}
271
272impl From<i64> for Value {
273    fn from(v: i64) -> Self {
274        Value::I64(v)
275    }
276}
277
278impl From<f64> for Value {
279    fn from(v: f64) -> Self {
280        Value::F64(v)
281    }
282}
283
284impl From<f32> for Value {
285    fn from(v: f32) -> Self {
286        Value::F64(v as f64)
287    }
288}
289
290impl From<rust_decimal::Decimal> for Value {
291    fn from(v: rust_decimal::Decimal) -> Self {
292        Value::Decimal(v)
293    }
294}
295
296impl From<chrono::NaiveDateTime> for Value {
297    fn from(v: chrono::NaiveDateTime) -> Self {
298        Value::DateTime(v)
299    }
300}
301
302impl From<uuid::Uuid> for Value {
303    fn from(v: uuid::Uuid) -> Self {
304        Value::Uuid(v)
305    }
306}
307
308impl From<serde_json::Value> for Value {
309    fn from(v: serde_json::Value) -> Self {
310        Value::Json(v)
311    }
312}
313
314impl From<BTreeMap<String, Option<String>>> for Value {
315    fn from(v: BTreeMap<String, Option<String>>) -> Self {
316        Value::Hstore(v)
317    }
318}
319
320impl From<Geometry> for Value {
321    fn from(v: Geometry) -> Self {
322        Value::Geometry(v.into_inner())
323    }
324}
325
326impl From<Geography> for Value {
327    fn from(v: Geography) -> Self {
328        Value::Geography(v.into_inner())
329    }
330}
331
332impl From<Vec<f32>> for Value {
333    fn from(v: Vec<f32>) -> Self {
334        Value::Vector(v)
335    }
336}
337
338impl From<String> for Value {
339    fn from(v: String) -> Self {
340        Value::String(v)
341    }
342}
343
344impl From<&str> for Value {
345    fn from(v: &str) -> Self {
346        Value::String(v.to_string())
347    }
348}
349
350impl From<Vec<u8>> for Value {
351    fn from(v: Vec<u8>) -> Self {
352        Value::Bytes(v)
353    }
354}
355
356// Array conversions — generated for all scalar types that map cleanly to Value
357// via `Into<Value>`. `Vec<u8>` is intentionally excluded: it maps to
358// `Value::Bytes`, not `Value::Array`.
359macro_rules! impl_vec_from {
360    ($($t:ty),* $(,)?) => {
361        $(
362            impl From<Vec<$t>> for Value {
363                fn from(v: Vec<$t>) -> Self {
364                    Value::Array(v.into_iter().map(|x| x.into()).collect())
365                }
366            }
367
368            impl From<Vec<Vec<$t>>> for Value {
369                fn from(v: Vec<Vec<$t>>) -> Self {
370                    Value::Array2D(
371                        v.into_iter()
372                            .map(|row| row.into_iter().map(|x| x.into()).collect())
373                            .collect(),
374                    )
375                }
376            }
377        )*
378    };
379}
380
381impl_vec_from!(
382    i32,
383    i64,
384    f64,
385    bool,
386    String,
387    Geometry,
388    Geography,
389    BTreeMap<String, Option<String>>,
390    rust_decimal::Decimal,
391    uuid::Uuid,
392    chrono::NaiveDateTime,
393    serde_json::Value,
394);
395
396// Option<T> conversions — map None -> Value::Null.
397// `Option<&str>` is kept manual because `&str` requires an explicit `.to_string()`
398// and does not implement `Into<Value>` through the generic `v.into()` path.
399macro_rules! impl_option_from {
400    ($($t:ty),* $(,)?) => {
401        $(
402            impl From<Option<$t>> for Value {
403                fn from(v: Option<$t>) -> Self {
404                    v.map(|x| x.into()).unwrap_or(Value::Null)
405                }
406            }
407        )*
408    };
409}
410
411impl_option_from!(
412    bool,
413    i32,
414    i64,
415    f64,
416    String,
417    Vec<f32>,
418    Geometry,
419    Geography,
420    BTreeMap<String, Option<String>>,
421    rust_decimal::Decimal,
422    uuid::Uuid,
423    chrono::NaiveDateTime,
424);
425
426impl From<Option<&str>> for Value {
427    fn from(v: Option<&str>) -> Self {
428        v.map(|s| Value::String(s.to_string()))
429            .unwrap_or(Value::Null)
430    }
431}
432
433impl Value {
434    /// Convert this value into the plain JSON shape used on transport/wire paths.
435    ///
436    /// Unlike the serde representation of [`Value`] itself, this helper
437    /// intentionally mirrors the historic untagged encoding used by the engine
438    /// and generated raw-query helpers.
439    pub fn to_json_plain(&self) -> serde_json::Value {
440        match self {
441            Value::Null => serde_json::Value::Null,
442            Value::Bool(v) => serde_json::Value::Bool(*v),
443            Value::I32(v) => serde_json::Value::Number((*v).into()),
444            Value::I64(v) => serde_json::Value::Number((*v).into()),
445            Value::F64(v) => serde_json::Number::from_f64(*v)
446                .map(serde_json::Value::Number)
447                .unwrap_or(serde_json::Value::Null),
448            Value::Decimal(v) => serde_json::Value::String(v.to_string()),
449            Value::DateTime(v) => serde_json::Value::String(format_datetime(*v)),
450            Value::Uuid(v) => serde_json::Value::String(v.to_string()),
451            Value::Json(v) => v.clone(),
452            Value::Hstore(v) => serde_json::Value::Object(
453                v.iter()
454                    .map(|(key, value)| {
455                        (
456                            key.clone(),
457                            value
458                                .as_ref()
459                                .map(|item| serde_json::Value::String(item.clone()))
460                                .unwrap_or(serde_json::Value::Null),
461                        )
462                    })
463                    .collect(),
464            ),
465            Value::Geometry(v) | Value::Geography(v) => serde_json::Value::String(v.clone()),
466            Value::Vector(v) => serde_json::Value::Array(
467                v.iter()
468                    .map(|item| {
469                        serde_json::Number::from_f64(*item as f64)
470                            .map(serde_json::Value::Number)
471                            .unwrap_or(serde_json::Value::Null)
472                    })
473                    .collect(),
474            ),
475            Value::String(v) => serde_json::Value::String(v.clone()),
476            Value::Bytes(v) => {
477                use base64::Engine;
478                serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(v))
479            }
480            Value::Array(v) => {
481                serde_json::Value::Array(v.iter().map(Value::to_json_plain).collect())
482            }
483            Value::Array2D(v) => serde_json::Value::Array(
484                v.iter()
485                    .map(|row| {
486                        serde_json::Value::Array(row.iter().map(Value::to_json_plain).collect())
487                    })
488                    .collect(),
489            ),
490            Value::Enum { value, .. } => serde_json::Value::String(value.clone()),
491        }
492    }
493}
494
495impl Serialize for Value {
496    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
497    where
498        S: Serializer,
499    {
500        SerdeValue::from(self).serialize(serializer)
501    }
502}
503
504/// Deserializes a [`Value`] from the tagged serde representation emitted by
505/// [`Serialize`] for [`Value`].
506impl<'de> Deserialize<'de> for Value {
507    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
508    where
509        D: Deserializer<'de>,
510    {
511        let tagged = SerdeValue::deserialize(deserializer)?;
512        Value::try_from(tagged).map_err(serde::de::Error::custom)
513    }
514}
515
516/// Convert a `&serde_json::Value` reference to a [`Value`].
517///
518/// This is the canonical JSON->Value conversion used throughout the crate.
519/// It is `pub(crate)` so that other modules (e.g. `column.rs`) can reuse it
520/// without duplicating the logic.
521///
522/// Numbers are coerced to `I32` before `I64` when they fit, then `F64`.
523/// Arrays of arrays are **not** auto-promoted to `Array2D` here; that
524/// promotion happens in the connector stream decoders where full schema
525/// knowledge is available.
526pub(crate) fn json_to_value_ref(json: &serde_json::Value) -> Value {
527    match json {
528        serde_json::Value::Null => Value::Null,
529        serde_json::Value::Bool(b) => Value::Bool(*b),
530        serde_json::Value::Number(n) => {
531            if let Some(i) = n.as_i64() {
532                if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
533                    Value::I32(i as i32)
534                } else {
535                    Value::I64(i)
536                }
537            } else if let Some(f) = n.as_f64() {
538                Value::F64(f)
539            } else {
540                Value::String(n.to_string())
541            }
542        }
543        serde_json::Value::String(s) => Value::String(s.clone()),
544        serde_json::Value::Array(arr) => Value::Array(arr.iter().map(json_to_value_ref).collect()),
545        serde_json::Value::Object(_) => Value::Json(json.clone()),
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    use core::f64;
552    use std::collections::BTreeMap;
553
554    use super::*;
555
556    #[test]
557    fn test_value_variants() {
558        assert_eq!(Value::Null, Value::Null);
559        assert_eq!(Value::Bool(true), Value::from(true));
560        assert_eq!(Value::I32(42), Value::from(42i32));
561        assert_eq!(Value::I64(42), Value::from(42i64));
562        assert_eq!(Value::F64(2.5), Value::from(2.5f64));
563        assert_eq!(Value::String("hello".to_string()), Value::from("hello"));
564        assert_eq!(Value::Bytes(vec![1, 2, 3]), Value::from(vec![1u8, 2, 3]));
565
566        use rust_decimal::Decimal;
567        let dec = Decimal::new(12345, 2);
568        assert_eq!(Value::Decimal(dec), Value::from(dec));
569
570        use chrono::NaiveDate;
571        let dt = NaiveDate::from_ymd_opt(2024, 1, 1)
572            .unwrap()
573            .and_hms_opt(12, 0, 0)
574            .unwrap();
575        assert_eq!(Value::DateTime(dt), Value::from(dt));
576
577        use uuid::Uuid;
578        let id = Uuid::nil();
579        assert_eq!(Value::Uuid(id), Value::from(id));
580
581        use serde_json::json;
582        let j = json!({"key": "value"});
583        assert_eq!(Value::Json(j.clone()), Value::from(j));
584
585        let hstore = BTreeMap::from([
586            ("display_name".to_string(), Some("Bob".to_string())),
587            ("nickname".to_string(), None),
588        ]);
589        assert_eq!(Value::Hstore(hstore.clone()), Value::from(hstore));
590
591        assert_eq!(
592            Value::Vector(vec![0.1, 0.2]),
593            Value::from(vec![0.1f32, 0.2])
594        );
595    }
596
597    #[test]
598    fn test_value_to_json_plain_primitives() {
599        assert_eq!(Value::Null.to_json_plain(), serde_json::Value::Null);
600        assert_eq!(
601            Value::Bool(true).to_json_plain(),
602            serde_json::Value::Bool(true)
603        );
604        assert_eq!(Value::I32(42).to_json_plain().as_i64(), Some(42));
605        assert_eq!(
606            Value::I64(9007199254740991).to_json_plain().as_i64(),
607            Some(9007199254740991)
608        );
609        assert_eq!(
610            Value::F64(f64::consts::PI).to_json_plain().as_f64(),
611            Some(f64::consts::PI)
612        );
613        assert_eq!(
614            Value::String("hello world".to_string())
615                .to_json_plain()
616                .as_str(),
617            Some("hello world")
618        );
619    }
620
621    #[test]
622    fn test_value_to_json_plain_special_scalars() {
623        use rust_decimal::Decimal;
624        let dec = Decimal::new(12345, 2);
625        use chrono::NaiveDate;
626        let dt = NaiveDate::from_ymd_opt(2026, 2, 18)
627            .unwrap()
628            .and_hms_opt(10, 30, 45)
629            .unwrap();
630        use uuid::Uuid;
631        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
632        assert_eq!(Value::Decimal(dec).to_json_plain().as_str(), Some("123.45"));
633        assert!(Value::DateTime(dt)
634            .to_json_plain()
635            .as_str()
636            .unwrap()
637            .starts_with("2026-02-18T10:30:45"));
638        assert_eq!(
639            Value::Uuid(id).to_json_plain().as_str(),
640            Some("550e8400-e29b-41d4-a716-446655440000")
641        );
642        assert_eq!(
643            Value::Bytes(vec![72, 101, 108, 108, 111])
644                .to_json_plain()
645                .as_str(),
646            Some("SGVsbG8=")
647        );
648    }
649
650    #[test]
651    fn test_value_to_json_plain_json_and_arrays() {
652        use serde_json::json;
653        let object = json!({"name": "Alice", "age": 30});
654        assert_eq!(Value::Json(object.clone()).to_json_plain(), object);
655
656        let value = Value::Array(vec![
657            Value::String("a".to_string()),
658            Value::String("b".to_string()),
659            Value::String("c".to_string()),
660        ]);
661
662        let json = value.to_json_plain();
663        assert_eq!(json[0].as_str(), Some("a"));
664        assert_eq!(json[1].as_str(), Some("b"));
665        assert_eq!(json[2].as_str(), Some("c"));
666    }
667
668    #[test]
669    fn test_value_to_json_plain_hstore() {
670        let value = Value::Hstore(BTreeMap::from([
671            ("display_name".to_string(), Some("Bob".to_string())),
672            ("nickname".to_string(), None),
673        ]));
674
675        assert_eq!(
676            value.to_json_plain(),
677            serde_json::json!({
678                "display_name": "Bob",
679                "nickname": null
680            })
681        );
682    }
683
684    #[test]
685    fn test_value_to_json_plain_vector() {
686        let json = Value::Vector(vec![1.0, 2.5, 3.25]).to_json_plain();
687        assert_eq!(json, serde_json::json!([1.0, 2.5, 3.25]));
688    }
689
690    #[test]
691    fn test_value_plain_json_array2d_roundtrip_stays_untyped_without_schema() {
692        let value = Value::Array2D(vec![
693            vec![Value::I32(1), Value::I32(2)],
694            vec![Value::I32(3), Value::I32(4)],
695        ]);
696
697        let json = value.to_json_plain();
698        assert_eq!(json[0][0].as_i64(), Some(1));
699        assert_eq!(json[0][1].as_i64(), Some(2));
700        assert_eq!(json[1][0].as_i64(), Some(3));
701        assert_eq!(json[1][1].as_i64(), Some(4));
702
703        // Deserialization: without schema context the `Array2D` heuristic is
704        // intentionally absent from `json_to_value_ref`. A nested JSON array
705        // round-trips as `Array(Array(_))`. Promotion to `Array2D` is the
706        // connector stream's responsibility.
707        let expected = Value::Array(vec![
708            Value::Array(vec![Value::I32(1), Value::I32(2)]),
709            Value::Array(vec![Value::I32(3), Value::I32(4)]),
710        ]);
711        assert_eq!(json_to_value_ref(&json), expected);
712    }
713
714    #[test]
715    fn test_tagged_serde_shape_is_explicit() {
716        let value = Value::Decimal(rust_decimal::Decimal::new(12345, 2));
717        let json = serde_json::to_value(&value).unwrap();
718
719        assert_eq!(
720            json,
721            serde_json::json!({
722                "type": "decimal",
723                "value": "123.45"
724            })
725        );
726    }
727
728    #[test]
729    fn test_tagged_serde_round_trip_preserves_typed_variants() {
730        use chrono::NaiveDate;
731        use serde_json::json;
732        use uuid::Uuid;
733
734        let values = vec![
735            Value::Null,
736            Value::Bool(false),
737            Value::I32(-42),
738            Value::I64(9007199254740991), // Large I64 beyond i32 range
739            Value::F64(f64::consts::E),
740            Value::Decimal(rust_decimal::Decimal::new(314, 2)),
741            Value::DateTime(
742                NaiveDate::from_ymd_opt(2026, 2, 18)
743                    .unwrap()
744                    .and_hms_opt(10, 30, 45)
745                    .unwrap(),
746            ),
747            Value::Uuid(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()),
748            Value::Bytes(vec![1, 2, 3, 4]),
749            Value::Json(json!({"ok": true})),
750            Value::Hstore(BTreeMap::from([
751                ("display_name".to_string(), Some("Bob".to_string())),
752                ("nickname".to_string(), None),
753            ])),
754            Value::Vector(vec![1.0, 2.0, 3.5]),
755            Value::String("test".to_string()),
756            Value::Array(vec![Value::I32(1), Value::I32(2)]),
757            Value::Array2D(vec![vec![Value::I32(1), Value::I32(2)]]),
758            Value::Enum {
759                value: "ADMIN".to_string(),
760                type_name: "role".to_string(),
761            },
762        ];
763
764        for value in values {
765            let json = serde_json::to_value(&value).unwrap();
766            let deserialized: Value = serde_json::from_value(json).unwrap();
767            assert_eq!(deserialized, value);
768        }
769    }
770}