Skip to main content

siderite_backends/mongodb/
value.rs

1//! Canonical mapping between [`Value`] and BSON.
2
3use ::mongodb::bson::spec::BinarySubtype;
4use ::mongodb::bson::{Binary, Bson, DateTime, Decimal128, Document};
5use rust_decimal::Decimal;
6use siderite_orm::types::{DATE_FORMAT, TIME_FORMAT};
7use siderite_orm::{QueryError, Value};
8use std::str::FromStr;
9use uuid::Uuid;
10
11/// Convert a [`Value`] to BSON (see the module docs of
12/// [`mongodb`](crate::mongodb) for the table).
13///
14/// # Errors
15/// [`QueryError::InvalidPlan`] for a decimal that Decimal128 cannot hold or a
16/// JSON number outside the 64-bit range.
17pub fn to_bson(value: &Value) -> Result<Bson, QueryError> {
18    Ok(match value {
19        Value::Null => Bson::Null,
20        Value::Bool(b) => Bson::Boolean(*b),
21        Value::Int(i) => Bson::Int64(*i),
22        Value::Float(f) => Bson::Double(*f),
23        Value::Decimal(d) => Bson::Decimal128(
24            Decimal128::from_str(&d.to_string())
25                .map_err(|e| QueryError::InvalidPlan(format!("decimal {d}: {e}")))?,
26        ),
27        Value::Text(s) => Bson::String(s.clone()),
28        Value::Bytes(b) => Bson::Binary(Binary {
29            subtype: BinarySubtype::Generic,
30            bytes: b.clone(),
31        }),
32        Value::Uuid(u) => Bson::Binary(Binary {
33            subtype: BinarySubtype::Uuid,
34            bytes: u.as_bytes().to_vec(),
35        }),
36        Value::Date(d) => Bson::String(d.format(DATE_FORMAT).to_string()),
37        Value::Time(t) => Bson::String(t.format(TIME_FORMAT).to_string()),
38        Value::Timestamp(t) => Bson::DateTime(DateTime::from_millis(t.timestamp_millis())),
39        Value::Json(j) => json_to_bson(j)?,
40    })
41}
42
43/// JSON objects and arrays are embedded; JSON scalars are stored as their
44/// JSON text (`5`, `"x"`, `null`), so they decode back through
45/// `DbType::from_value` for `serde_json::Value`.
46fn json_to_bson(json: &serde_json::Value) -> Result<Bson, QueryError> {
47    Ok(match json {
48        serde_json::Value::Object(_) | serde_json::Value::Array(_) => embed_json(json)?,
49        scalar => Bson::String(scalar.to_string()),
50    })
51}
52
53fn embed_json(json: &serde_json::Value) -> Result<Bson, QueryError> {
54    Ok(match json {
55        serde_json::Value::Null => Bson::Null,
56        serde_json::Value::Bool(b) => Bson::Boolean(*b),
57        serde_json::Value::Number(n) => match (n.as_i64(), n.as_f64()) {
58            (Some(i), _) => Bson::Int64(i),
59            (None, Some(f)) if !n.is_u64() => Bson::Double(f),
60            _ => {
61                return Err(QueryError::InvalidPlan(format!(
62                    "JSON number {n} does not fit a BSON int64 or double"
63                )));
64            }
65        },
66        serde_json::Value::String(s) => Bson::String(s.clone()),
67        serde_json::Value::Array(items) => {
68            Bson::Array(items.iter().map(embed_json).collect::<Result<_, _>>()?)
69        }
70        serde_json::Value::Object(map) => {
71            let mut doc = Document::new();
72            for (k, v) in map {
73                doc.insert(k.clone(), embed_json(v)?);
74            }
75            Bson::Document(doc)
76        }
77    })
78}
79
80/// Convert BSON read from the database to a [`Value`].
81///
82/// Documents and arrays become [`Value::Json`]; strings stay [`Value::Text`]
83/// (dates and times decode from their canonical text through
84/// `DbType::from_value`).
85///
86/// # Errors
87/// [`QueryError::Decode`] for BSON types with no [`Value`] counterpart.
88pub fn from_bson(bson: Bson, column: &str) -> Result<Value, QueryError> {
89    let fail = |reason: String| QueryError::Decode {
90        column: column.to_owned(),
91        reason,
92    };
93    Ok(match bson {
94        Bson::Null | Bson::Undefined => Value::Null,
95        Bson::Boolean(b) => Value::Bool(b),
96        Bson::Int32(i) => Value::Int(i64::from(i)),
97        Bson::Int64(i) => Value::Int(i),
98        Bson::Double(f) => Value::Float(f),
99        Bson::Decimal128(d) => {
100            let text = d.to_string();
101            let parsed = Decimal::from_scientific(&text).or_else(|_| Decimal::from_str(&text));
102            Value::Decimal(parsed.map_err(|e| fail(format!("decimal {text}: {e}")))?)
103        }
104        Bson::String(s) => Value::Text(s),
105        Bson::Binary(Binary {
106            subtype: BinarySubtype::Uuid,
107            bytes,
108        }) if bytes.len() == 16 => {
109            Value::Uuid(Uuid::from_slice(&bytes).map_err(|e| fail(format!("invalid uuid: {e}")))?)
110        }
111        Bson::Binary(b) => Value::Bytes(b.bytes),
112        Bson::DateTime(dt) => Value::Timestamp(
113            chrono::DateTime::from_timestamp_millis(dt.timestamp_millis())
114                .ok_or_else(|| fail("timestamp out of range".into()))?,
115        ),
116        Bson::ObjectId(oid) => Value::Text(oid.to_hex()),
117        doc @ (Bson::Document(_) | Bson::Array(_)) => Value::Json(bson_to_json(doc)),
118        other => {
119            return Err(fail(format!(
120                "unsupported BSON type {:?}",
121                other.element_type()
122            )));
123        }
124    })
125}
126
127fn bson_to_json(bson: Bson) -> serde_json::Value {
128    use serde_json::Value as J;
129    match bson {
130        Bson::Null | Bson::Undefined => J::Null,
131        Bson::Boolean(b) => J::Bool(b),
132        Bson::Int32(i) => J::from(i),
133        Bson::Int64(i) => J::from(i),
134        Bson::Double(f) => serde_json::Number::from_f64(f).map_or(J::Null, J::Number),
135        Bson::String(s) => J::String(s),
136        Bson::Array(items) => J::Array(items.into_iter().map(bson_to_json).collect()),
137        Bson::Document(doc) => {
138            J::Object(doc.into_iter().map(|(k, v)| (k, bson_to_json(v))).collect())
139        }
140        Bson::DateTime(dt) => J::String(dt.try_to_rfc3339_string().unwrap_or_default()),
141        Bson::Decimal128(d) => J::String(d.to_string()),
142        Bson::ObjectId(oid) => J::String(oid.to_hex()),
143        other => other.into_relaxed_extjson(),
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    #![allow(clippy::unwrap_used)]
150    use super::*;
151    use chrono::{NaiveDate, NaiveTime, TimeZone, Utc};
152
153    fn round(value: Value) -> Value {
154        from_bson(to_bson(&value).unwrap(), "c").unwrap()
155    }
156
157    #[test]
158    fn scalars_round_trip() {
159        assert_eq!(round(Value::Int(7)), Value::Int(7));
160        assert_eq!(round(Value::Float(1.5)), Value::Float(1.5));
161        assert_eq!(round(Value::Bool(true)), Value::Bool(true));
162        assert_eq!(round(Value::Null), Value::Null);
163        assert_eq!(round(Value::Text("$x".into())), Value::Text("$x".into()));
164        assert_eq!(round(Value::Bytes(vec![1, 2])), Value::Bytes(vec![1, 2]));
165        let u = Uuid::from_u128(42);
166        assert_eq!(round(Value::Uuid(u)), Value::Uuid(u));
167    }
168
169    #[test]
170    fn decimal_is_decimal128() {
171        let d = Decimal::new(1250, 2);
172        assert!(matches!(
173            to_bson(&Value::Decimal(d)),
174            Ok(Bson::Decimal128(_))
175        ));
176        assert_eq!(round(Value::Decimal(d)), Value::Decimal(d));
177        let small = Decimal::from_str("-0.000001").unwrap();
178        assert_eq!(round(Value::Decimal(small)), Value::Decimal(small));
179    }
180
181    #[test]
182    fn uuid_is_binary_subtype_4() {
183        let Ok(Bson::Binary(b)) = to_bson(&Value::Uuid(Uuid::nil())) else {
184            panic!("not binary");
185        };
186        assert_eq!(b.subtype, BinarySubtype::Uuid);
187        assert_eq!(b.bytes.len(), 16);
188    }
189
190    #[test]
191    fn timestamps_truncate_to_milliseconds() {
192        let t = Utc.timestamp_micros(1_700_000_000_123_456).unwrap();
193        let expected = Utc.timestamp_millis_opt(1_700_000_000_123).unwrap();
194        assert_eq!(round(Value::Timestamp(t)), Value::Timestamp(expected));
195    }
196
197    #[test]
198    fn dates_and_times_are_canonical_text() {
199        let d = NaiveDate::from_ymd_opt(2026, 9, 29).unwrap();
200        assert_eq!(
201            to_bson(&Value::Date(d)).unwrap(),
202            Bson::String("2026-09-29".into())
203        );
204        let t = NaiveTime::from_hms_micro_opt(1, 2, 3, 4).unwrap();
205        assert_eq!(
206            to_bson(&Value::Time(t)).unwrap(),
207            Bson::String("01:02:03.000004".into())
208        );
209    }
210
211    #[test]
212    fn json_documents_are_embedded_and_scalars_are_text() {
213        let j = serde_json::json!({"a": [1, 2.5, null], "$k": {"b": true}});
214        assert!(matches!(
215            to_bson(&Value::Json(j.clone())).unwrap(),
216            Bson::Document(_)
217        ));
218        assert_eq!(round(Value::Json(j.clone())), Value::Json(j));
219        assert_eq!(
220            to_bson(&Value::Json(serde_json::json!("x"))).unwrap(),
221            Bson::String("\"x\"".into())
222        );
223        assert!(to_bson(&Value::Json(serde_json::json!([u64::MAX]))).is_err());
224    }
225}