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    /// A PostgreSQL native composite type value.
156    ///
157    /// Carries the lowercase PG type name (e.g. `"championstats"`) together
158    /// with the field values in their declared order. The PostgreSQL dialect
159    /// emits the required explicit cast (`$1::championstats`) and the connector
160    /// encodes the fields as a record literal (`("0","0",…)`). Backends that
161    /// store composites as JSON never receive this variant.
162    Composite {
163        /// Lowercase PostgreSQL type name (e.g. `"championstats"`).
164        type_name: String,
165        /// Field values in the composite type's declared order.
166        fields: Vec<Value>,
167    },
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[serde(tag = "type", content = "value", rename_all = "snake_case")]
172enum SerdeValue {
173    Null,
174    Bool(bool),
175    I32(i32),
176    I64(i64),
177    F64(f64),
178    Decimal(String),
179    DateTime(String),
180    Uuid(String),
181    Json(serde_json::Value),
182    Hstore(BTreeMap<String, Option<String>>),
183    Geometry(String),
184    Geography(String),
185    Vector(Vec<f32>),
186    String(String),
187    Bytes(String),
188    Array(Vec<Value>),
189    Array2D(Vec<Vec<Value>>),
190    Enum {
191        value: String,
192        type_name: String,
193    },
194    Composite {
195        type_name: String,
196        fields: Vec<Value>,
197    },
198}
199
200fn format_datetime(value: chrono::NaiveDateTime) -> String {
201    value.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string()
202}
203
204fn parse_datetime_string(raw: &str) -> std::result::Result<chrono::NaiveDateTime, String> {
205    chrono::DateTime::parse_from_rfc3339(raw)
206        .map(|value| value.naive_utc())
207        .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S%.f"))
208        .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%d %H:%M:%S%.f"))
209        .map_err(|_| format!("invalid datetime '{}'", raw))
210}
211
212impl From<&Value> for SerdeValue {
213    fn from(value: &Value) -> Self {
214        match value {
215            Value::Null => SerdeValue::Null,
216            Value::Bool(v) => SerdeValue::Bool(*v),
217            Value::I32(v) => SerdeValue::I32(*v),
218            Value::I64(v) => SerdeValue::I64(*v),
219            Value::F64(v) => SerdeValue::F64(*v),
220            Value::Decimal(v) => SerdeValue::Decimal(v.to_string()),
221            Value::DateTime(v) => SerdeValue::DateTime(format_datetime(*v)),
222            Value::Uuid(v) => SerdeValue::Uuid(v.to_string()),
223            Value::Json(v) => SerdeValue::Json(v.clone()),
224            Value::Hstore(v) => SerdeValue::Hstore(v.clone()),
225            Value::Geometry(v) => SerdeValue::Geometry(v.clone()),
226            Value::Geography(v) => SerdeValue::Geography(v.clone()),
227            Value::Vector(v) => SerdeValue::Vector(v.clone()),
228            Value::String(v) => SerdeValue::String(v.clone()),
229            Value::Bytes(v) => {
230                use base64::Engine;
231                SerdeValue::Bytes(base64::engine::general_purpose::STANDARD.encode(v))
232            }
233            Value::Array(v) => SerdeValue::Array(v.clone()),
234            Value::Array2D(v) => SerdeValue::Array2D(v.clone()),
235            Value::Enum { value, type_name } => SerdeValue::Enum {
236                value: value.clone(),
237                type_name: type_name.clone(),
238            },
239            Value::Composite { type_name, fields } => SerdeValue::Composite {
240                type_name: type_name.clone(),
241                fields: fields.clone(),
242            },
243        }
244    }
245}
246
247impl TryFrom<SerdeValue> for Value {
248    type Error = String;
249
250    fn try_from(value: SerdeValue) -> std::result::Result<Self, Self::Error> {
251        match value {
252            SerdeValue::Null => Ok(Value::Null),
253            SerdeValue::Bool(v) => Ok(Value::Bool(v)),
254            SerdeValue::I32(v) => Ok(Value::I32(v)),
255            SerdeValue::I64(v) => Ok(Value::I64(v)),
256            SerdeValue::F64(v) => Ok(Value::F64(v)),
257            SerdeValue::Decimal(raw) => rust_decimal::Decimal::from_str(&raw)
258                .map(Value::Decimal)
259                .map_err(|e| format!("invalid decimal '{}': {}", raw, e)),
260            SerdeValue::DateTime(raw) => parse_datetime_string(&raw).map(Value::DateTime),
261            SerdeValue::Uuid(raw) => uuid::Uuid::parse_str(&raw)
262                .map(Value::Uuid)
263                .map_err(|e| format!("invalid uuid '{}': {}", raw, e)),
264            SerdeValue::Json(v) => Ok(Value::Json(v)),
265            SerdeValue::Hstore(v) => Ok(Value::Hstore(v)),
266            SerdeValue::Geometry(v) => Ok(Value::Geometry(v)),
267            SerdeValue::Geography(v) => Ok(Value::Geography(v)),
268            SerdeValue::Vector(v) => Ok(Value::Vector(v)),
269            SerdeValue::String(v) => Ok(Value::String(v)),
270            SerdeValue::Bytes(raw) => {
271                use base64::Engine;
272                base64::engine::general_purpose::STANDARD
273                    .decode(raw.as_bytes())
274                    .map(Value::Bytes)
275                    .map_err(|e| format!("invalid base64 bytes '{}': {}", raw, e))
276            }
277            SerdeValue::Array(v) => Ok(Value::Array(v)),
278            SerdeValue::Array2D(v) => Ok(Value::Array2D(v)),
279            SerdeValue::Enum { value, type_name } => Ok(Value::Enum { value, type_name }),
280            SerdeValue::Composite { type_name, fields } => {
281                Ok(Value::Composite { type_name, fields })
282            }
283        }
284    }
285}
286
287impl From<bool> for Value {
288    fn from(v: bool) -> Self {
289        Value::Bool(v)
290    }
291}
292
293impl From<i32> for Value {
294    fn from(v: i32) -> Self {
295        Value::I32(v)
296    }
297}
298
299impl From<i64> for Value {
300    fn from(v: i64) -> Self {
301        Value::I64(v)
302    }
303}
304
305impl From<f64> for Value {
306    fn from(v: f64) -> Self {
307        Value::F64(v)
308    }
309}
310
311impl From<f32> for Value {
312    fn from(v: f32) -> Self {
313        Value::F64(v as f64)
314    }
315}
316
317impl From<rust_decimal::Decimal> for Value {
318    fn from(v: rust_decimal::Decimal) -> Self {
319        Value::Decimal(v)
320    }
321}
322
323impl From<chrono::NaiveDateTime> for Value {
324    fn from(v: chrono::NaiveDateTime) -> Self {
325        Value::DateTime(v)
326    }
327}
328
329impl From<uuid::Uuid> for Value {
330    fn from(v: uuid::Uuid) -> Self {
331        Value::Uuid(v)
332    }
333}
334
335impl From<serde_json::Value> for Value {
336    fn from(v: serde_json::Value) -> Self {
337        Value::Json(v)
338    }
339}
340
341impl From<BTreeMap<String, Option<String>>> for Value {
342    fn from(v: BTreeMap<String, Option<String>>) -> Self {
343        Value::Hstore(v)
344    }
345}
346
347impl From<Geometry> for Value {
348    fn from(v: Geometry) -> Self {
349        Value::Geometry(v.into_inner())
350    }
351}
352
353impl From<Geography> for Value {
354    fn from(v: Geography) -> Self {
355        Value::Geography(v.into_inner())
356    }
357}
358
359impl From<Vec<f32>> for Value {
360    fn from(v: Vec<f32>) -> Self {
361        Value::Vector(v)
362    }
363}
364
365impl From<String> for Value {
366    fn from(v: String) -> Self {
367        Value::String(v)
368    }
369}
370
371impl From<&str> for Value {
372    fn from(v: &str) -> Self {
373        Value::String(v.to_string())
374    }
375}
376
377impl From<Vec<u8>> for Value {
378    fn from(v: Vec<u8>) -> Self {
379        Value::Bytes(v)
380    }
381}
382
383// Array conversions — generated for all scalar types that map cleanly to Value
384// via `Into<Value>`. `Vec<u8>` is intentionally excluded: it maps to
385// `Value::Bytes`, not `Value::Array`.
386macro_rules! impl_vec_from {
387    ($($t:ty),* $(,)?) => {
388        $(
389            impl From<Vec<$t>> for Value {
390                fn from(v: Vec<$t>) -> Self {
391                    Value::Array(v.into_iter().map(|x| x.into()).collect())
392                }
393            }
394
395            impl From<Vec<Vec<$t>>> for Value {
396                fn from(v: Vec<Vec<$t>>) -> Self {
397                    Value::Array2D(
398                        v.into_iter()
399                            .map(|row| row.into_iter().map(|x| x.into()).collect())
400                            .collect(),
401                    )
402                }
403            }
404        )*
405    };
406}
407
408impl_vec_from!(
409    i32,
410    i64,
411    f64,
412    bool,
413    String,
414    Geometry,
415    Geography,
416    BTreeMap<String, Option<String>>,
417    rust_decimal::Decimal,
418    uuid::Uuid,
419    chrono::NaiveDateTime,
420    serde_json::Value,
421);
422
423// Option<T> conversions — map None -> Value::Null.
424// `Option<&str>` is kept manual because `&str` requires an explicit `.to_string()`
425// and does not implement `Into<Value>` through the generic `v.into()` path.
426macro_rules! impl_option_from {
427    ($($t:ty),* $(,)?) => {
428        $(
429            impl From<Option<$t>> for Value {
430                fn from(v: Option<$t>) -> Self {
431                    v.map(|x| x.into()).unwrap_or(Value::Null)
432                }
433            }
434        )*
435    };
436}
437
438impl_option_from!(
439    bool,
440    i32,
441    i64,
442    f64,
443    String,
444    Vec<f32>,
445    Geometry,
446    Geography,
447    BTreeMap<String, Option<String>>,
448    rust_decimal::Decimal,
449    uuid::Uuid,
450    chrono::NaiveDateTime,
451);
452
453impl From<Option<&str>> for Value {
454    fn from(v: Option<&str>) -> Self {
455        v.map(|s| Value::String(s.to_string()))
456            .unwrap_or(Value::Null)
457    }
458}
459
460impl Value {
461    /// Convert this value into the plain JSON shape used on transport/wire paths.
462    ///
463    /// Unlike the serde representation of [`Value`] itself, this helper
464    /// intentionally mirrors the historic untagged encoding used by the engine
465    /// and generated raw-query helpers.
466    pub fn to_json_plain(&self) -> serde_json::Value {
467        match self {
468            Value::Null => serde_json::Value::Null,
469            Value::Bool(v) => serde_json::Value::Bool(*v),
470            Value::I32(v) => serde_json::Value::Number((*v).into()),
471            Value::I64(v) => serde_json::Value::Number((*v).into()),
472            Value::F64(v) => serde_json::Number::from_f64(*v)
473                .map(serde_json::Value::Number)
474                .unwrap_or(serde_json::Value::Null),
475            Value::Decimal(v) => serde_json::Value::String(v.to_string()),
476            Value::DateTime(v) => serde_json::Value::String(format_datetime(*v)),
477            Value::Uuid(v) => serde_json::Value::String(v.to_string()),
478            Value::Json(v) => v.clone(),
479            Value::Hstore(v) => serde_json::Value::Object(
480                v.iter()
481                    .map(|(key, value)| {
482                        (
483                            key.clone(),
484                            value
485                                .as_ref()
486                                .map(|item| serde_json::Value::String(item.clone()))
487                                .unwrap_or(serde_json::Value::Null),
488                        )
489                    })
490                    .collect(),
491            ),
492            Value::Geometry(v) | Value::Geography(v) => serde_json::Value::String(v.clone()),
493            Value::Vector(v) => serde_json::Value::Array(
494                v.iter()
495                    .map(|item| {
496                        serde_json::Number::from_f64(*item as f64)
497                            .map(serde_json::Value::Number)
498                            .unwrap_or(serde_json::Value::Null)
499                    })
500                    .collect(),
501            ),
502            Value::String(v) => serde_json::Value::String(v.clone()),
503            Value::Bytes(v) => {
504                use base64::Engine;
505                serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(v))
506            }
507            Value::Array(v) => {
508                serde_json::Value::Array(v.iter().map(Value::to_json_plain).collect())
509            }
510            Value::Array2D(v) => serde_json::Value::Array(
511                v.iter()
512                    .map(|row| {
513                        serde_json::Value::Array(row.iter().map(Value::to_json_plain).collect())
514                    })
515                    .collect(),
516            ),
517            Value::Enum { value, .. } => serde_json::Value::String(value.clone()),
518            Value::Composite { fields, .. } => {
519                serde_json::Value::Array(fields.iter().map(Value::to_json_plain).collect())
520            }
521        }
522    }
523}
524
525impl Serialize for Value {
526    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
527    where
528        S: Serializer,
529    {
530        SerdeValue::from(self).serialize(serializer)
531    }
532}
533
534/// Deserializes a [`Value`] from the tagged serde representation emitted by
535/// [`Serialize`] for [`Value`].
536impl<'de> Deserialize<'de> for Value {
537    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
538    where
539        D: Deserializer<'de>,
540    {
541        let tagged = SerdeValue::deserialize(deserializer)?;
542        Value::try_from(tagged).map_err(serde::de::Error::custom)
543    }
544}
545
546/// Convert a `&serde_json::Value` reference to a [`Value`].
547///
548/// This is the canonical JSON->Value conversion used throughout the crate.
549/// It is `pub(crate)` so that other modules (e.g. `column.rs`) can reuse it
550/// without duplicating the logic.
551///
552/// Numbers are coerced to `I32` before `I64` when they fit, then `F64`.
553/// Arrays of arrays are **not** auto-promoted to `Array2D` here; that
554/// promotion happens in the connector stream decoders where full schema
555/// knowledge is available.
556pub(crate) fn json_to_value_ref(json: &serde_json::Value) -> Value {
557    match json {
558        serde_json::Value::Null => Value::Null,
559        serde_json::Value::Bool(b) => Value::Bool(*b),
560        serde_json::Value::Number(n) => {
561            if let Some(i) = n.as_i64() {
562                if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
563                    Value::I32(i as i32)
564                } else {
565                    Value::I64(i)
566                }
567            } else if let Some(f) = n.as_f64() {
568                Value::F64(f)
569            } else {
570                Value::String(n.to_string())
571            }
572        }
573        serde_json::Value::String(s) => Value::String(s.clone()),
574        serde_json::Value::Array(arr) => Value::Array(arr.iter().map(json_to_value_ref).collect()),
575        serde_json::Value::Object(_) => Value::Json(json.clone()),
576    }
577}
578
579#[cfg(test)]
580mod tests {
581    use core::f64;
582    use std::collections::BTreeMap;
583
584    use super::*;
585
586    #[test]
587    fn test_value_variants() {
588        assert_eq!(Value::Null, Value::Null);
589        assert_eq!(Value::Bool(true), Value::from(true));
590        assert_eq!(Value::I32(42), Value::from(42i32));
591        assert_eq!(Value::I64(42), Value::from(42i64));
592        assert_eq!(Value::F64(2.5), Value::from(2.5f64));
593        assert_eq!(Value::String("hello".to_string()), Value::from("hello"));
594        assert_eq!(Value::Bytes(vec![1, 2, 3]), Value::from(vec![1u8, 2, 3]));
595
596        use rust_decimal::Decimal;
597        let dec = Decimal::new(12345, 2);
598        assert_eq!(Value::Decimal(dec), Value::from(dec));
599
600        use chrono::NaiveDate;
601        let dt = NaiveDate::from_ymd_opt(2024, 1, 1)
602            .unwrap()
603            .and_hms_opt(12, 0, 0)
604            .unwrap();
605        assert_eq!(Value::DateTime(dt), Value::from(dt));
606
607        use uuid::Uuid;
608        let id = Uuid::nil();
609        assert_eq!(Value::Uuid(id), Value::from(id));
610
611        use serde_json::json;
612        let j = json!({"key": "value"});
613        assert_eq!(Value::Json(j.clone()), Value::from(j));
614
615        let hstore = BTreeMap::from([
616            ("display_name".to_string(), Some("Bob".to_string())),
617            ("nickname".to_string(), None),
618        ]);
619        assert_eq!(Value::Hstore(hstore.clone()), Value::from(hstore));
620
621        assert_eq!(
622            Value::Vector(vec![0.1, 0.2]),
623            Value::from(vec![0.1f32, 0.2])
624        );
625    }
626
627    #[test]
628    fn test_value_to_json_plain_primitives() {
629        assert_eq!(Value::Null.to_json_plain(), serde_json::Value::Null);
630        assert_eq!(
631            Value::Bool(true).to_json_plain(),
632            serde_json::Value::Bool(true)
633        );
634        assert_eq!(Value::I32(42).to_json_plain().as_i64(), Some(42));
635        assert_eq!(
636            Value::I64(9007199254740991).to_json_plain().as_i64(),
637            Some(9007199254740991)
638        );
639        assert_eq!(
640            Value::F64(f64::consts::PI).to_json_plain().as_f64(),
641            Some(f64::consts::PI)
642        );
643        assert_eq!(
644            Value::String("hello world".to_string())
645                .to_json_plain()
646                .as_str(),
647            Some("hello world")
648        );
649    }
650
651    #[test]
652    fn test_value_to_json_plain_special_scalars() {
653        use rust_decimal::Decimal;
654        let dec = Decimal::new(12345, 2);
655        use chrono::NaiveDate;
656        let dt = NaiveDate::from_ymd_opt(2026, 2, 18)
657            .unwrap()
658            .and_hms_opt(10, 30, 45)
659            .unwrap();
660        use uuid::Uuid;
661        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
662        assert_eq!(Value::Decimal(dec).to_json_plain().as_str(), Some("123.45"));
663        assert!(Value::DateTime(dt)
664            .to_json_plain()
665            .as_str()
666            .unwrap()
667            .starts_with("2026-02-18T10:30:45"));
668        assert_eq!(
669            Value::Uuid(id).to_json_plain().as_str(),
670            Some("550e8400-e29b-41d4-a716-446655440000")
671        );
672        assert_eq!(
673            Value::Bytes(vec![72, 101, 108, 108, 111])
674                .to_json_plain()
675                .as_str(),
676            Some("SGVsbG8=")
677        );
678    }
679
680    #[test]
681    fn test_value_to_json_plain_json_and_arrays() {
682        use serde_json::json;
683        let object = json!({"name": "Alice", "age": 30});
684        assert_eq!(Value::Json(object.clone()).to_json_plain(), object);
685
686        let value = Value::Array(vec![
687            Value::String("a".to_string()),
688            Value::String("b".to_string()),
689            Value::String("c".to_string()),
690        ]);
691
692        let json = value.to_json_plain();
693        assert_eq!(json[0].as_str(), Some("a"));
694        assert_eq!(json[1].as_str(), Some("b"));
695        assert_eq!(json[2].as_str(), Some("c"));
696    }
697
698    #[test]
699    fn test_value_to_json_plain_hstore() {
700        let value = Value::Hstore(BTreeMap::from([
701            ("display_name".to_string(), Some("Bob".to_string())),
702            ("nickname".to_string(), None),
703        ]));
704
705        assert_eq!(
706            value.to_json_plain(),
707            serde_json::json!({
708                "display_name": "Bob",
709                "nickname": null
710            })
711        );
712    }
713
714    #[test]
715    fn test_value_to_json_plain_vector() {
716        let json = Value::Vector(vec![1.0, 2.5, 3.25]).to_json_plain();
717        assert_eq!(json, serde_json::json!([1.0, 2.5, 3.25]));
718    }
719
720    #[test]
721    fn test_value_plain_json_array2d_roundtrip_stays_untyped_without_schema() {
722        let value = Value::Array2D(vec![
723            vec![Value::I32(1), Value::I32(2)],
724            vec![Value::I32(3), Value::I32(4)],
725        ]);
726
727        let json = value.to_json_plain();
728        assert_eq!(json[0][0].as_i64(), Some(1));
729        assert_eq!(json[0][1].as_i64(), Some(2));
730        assert_eq!(json[1][0].as_i64(), Some(3));
731        assert_eq!(json[1][1].as_i64(), Some(4));
732
733        // Deserialization: without schema context the `Array2D` heuristic is
734        // intentionally absent from `json_to_value_ref`. A nested JSON array
735        // round-trips as `Array(Array(_))`. Promotion to `Array2D` is the
736        // connector stream's responsibility.
737        let expected = Value::Array(vec![
738            Value::Array(vec![Value::I32(1), Value::I32(2)]),
739            Value::Array(vec![Value::I32(3), Value::I32(4)]),
740        ]);
741        assert_eq!(json_to_value_ref(&json), expected);
742    }
743
744    #[test]
745    fn test_tagged_serde_shape_is_explicit() {
746        let value = Value::Decimal(rust_decimal::Decimal::new(12345, 2));
747        let json = serde_json::to_value(&value).unwrap();
748
749        assert_eq!(
750            json,
751            serde_json::json!({
752                "type": "decimal",
753                "value": "123.45"
754            })
755        );
756    }
757
758    #[test]
759    fn test_tagged_serde_round_trip_preserves_typed_variants() {
760        use chrono::NaiveDate;
761        use serde_json::json;
762        use uuid::Uuid;
763
764        let values = vec![
765            Value::Null,
766            Value::Bool(false),
767            Value::I32(-42),
768            Value::I64(9007199254740991), // Large I64 beyond i32 range
769            Value::F64(f64::consts::E),
770            Value::Decimal(rust_decimal::Decimal::new(314, 2)),
771            Value::DateTime(
772                NaiveDate::from_ymd_opt(2026, 2, 18)
773                    .unwrap()
774                    .and_hms_opt(10, 30, 45)
775                    .unwrap(),
776            ),
777            Value::Uuid(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()),
778            Value::Bytes(vec![1, 2, 3, 4]),
779            Value::Json(json!({"ok": true})),
780            Value::Hstore(BTreeMap::from([
781                ("display_name".to_string(), Some("Bob".to_string())),
782                ("nickname".to_string(), None),
783            ])),
784            Value::Vector(vec![1.0, 2.0, 3.5]),
785            Value::String("test".to_string()),
786            Value::Array(vec![Value::I32(1), Value::I32(2)]),
787            Value::Array2D(vec![vec![Value::I32(1), Value::I32(2)]]),
788            Value::Enum {
789                value: "ADMIN".to_string(),
790                type_name: "role".to_string(),
791            },
792        ];
793
794        for value in values {
795            let json = serde_json::to_value(&value).unwrap();
796            let deserialized: Value = serde_json::from_value(json).unwrap();
797            assert_eq!(deserialized, value);
798        }
799    }
800}