siderite_backends/mongodb/
value.rs1use ::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
11pub 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
43fn 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
80pub 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}