fix(mongodb): preserve long ids in filters and updates

This commit is contained in:
zipg authored and GitHub committed 2026-07-14 11:09:11 +08:00
1 parent 2666ed1cd2
commit 2a5e7ac1c7
9 files changed
+325 -31

No files matched your search

+88 -3
View File
@@ -991,6 +991,12 @@ pub async fn delete_document(client: &Client, database: &str, collection: &str,
}
fn document_id_filters(id: &str) -> Vec<Document> {
if let Some(string_id) = decode_string_document_id(id) {
return vec![doc! { "_id": Bson::String(string_id) }];
}
if let Some(filter) = extended_json_document_id_filter(id) {
return vec![filter];
}
let string_filter = doc! { "_id": Bson::String(id.to_string()) };
match ObjectId::parse_str(id) {
Ok(oid) => vec![doc! { "_id": Bson::ObjectId(oid) }, string_filter],
@@ -998,6 +1004,23 @@ fn document_id_filters(id: &str) -> Vec<Document> {
}
}
fn decode_string_document_id(id: &str) -> Option<String> {
id.strip_prefix("__dbx_mongo_string_id__").and_then(|payload| serde_json::from_str::<String>(payload).ok())
}
fn extended_json_document_id_filter(id: &str) -> Option<Document> {
let trimmed = id.trim();
if !trimmed.starts_with('{') {
return None;
}
let value: serde_json::Value = serde_json::from_str(trimmed).ok()?;
let bson = json_value_to_bson(&value);
if matches!(bson, Bson::Document(_)) {
return None;
}
Some(doc! { "_id": bson })
}
pub async fn delete_documents(
client: &Client,
database: &str,
@@ -1034,7 +1057,7 @@ fn bson_to_json(bson: &Bson) -> serde_json::Value {
Bson::Document(doc) => {
let mut map = serde_json::Map::new();
for (k, v) in doc {
map.insert(k.clone(), bson_to_json(v));
map.insert(k.clone(), bson_document_field_to_json(k, v));
}
serde_json::Value::Object(map)
}
@@ -1042,6 +1065,15 @@ fn bson_to_json(bson: &Bson) -> serde_json::Value {
}
}
fn bson_document_field_to_json(key: &str, bson: &Bson) -> serde_json::Value {
if key == "_id" {
if let Bson::Int64(value) = bson {
return serde_json::json!({ "$numberLong": value.to_string() });
}
}
bson_to_json(bson)
}
/// Convert a `serde_json::Value` (JSON object) to a BSON `Document`,
/// handling MongoDB extended JSON conventions such as `{"$oid":"..."}`.
pub fn json_object_to_document(value: &serde_json::Value) -> Result<Document, String> {
@@ -1142,6 +1174,9 @@ fn json_value_to_bson(value: &serde_json::Value) -> Bson {
return Bson::ObjectId(oid);
}
}
if let Some(value) = parse_extended_json_int64(obj) {
return Bson::Int64(value);
}
if let Some(date) = parse_extended_json_date(obj) {
return Bson::DateTime(date);
}
@@ -1207,6 +1242,14 @@ fn parse_extended_json_date(obj: &serde_json::Map<String, serde_json::Value>) ->
}
}
fn parse_extended_json_int64(obj: &serde_json::Map<String, serde_json::Value>) -> Option<i64> {
match obj.get("$numberLong")? {
serde_json::Value::String(value) => value.parse().ok(),
serde_json::Value::Number(value) => value.as_i64(),
_ => None,
}
}
fn json_filter_value_to_bson(value: &serde_json::Value, field_name: Option<&str>) -> Bson {
if field_name == Some("_id") {
if let Some(id) = value.as_str() {
@@ -1225,6 +1268,9 @@ fn json_filter_value_to_bson(value: &serde_json::Value, field_name: Option<&str>
return Bson::ObjectId(oid);
}
}
if let Some(value) = parse_extended_json_int64(obj) {
return Bson::Int64(value);
}
// Extended JSON dates must be decoded in filters too, otherwise
// {"$date": ...} reaches the server as a raw document: a bare
// { field: {"$date": ...} } fails with "unknown operator: $date"
@@ -1373,7 +1419,7 @@ mod tests {
#[test]
fn document_id_filters_try_object_id_then_string_for_hex_ids() {
let id = "507f1f77bcf86cd799439011";
let filters = document_id_filters(id);
let filters = document_id_filters(&id);
assert_eq!(filters.len(), 2);
assert!(matches!(filters[0].get("_id"), Some(Bson::ObjectId(_))));
@@ -1383,12 +1429,40 @@ mod tests {
#[test]
fn document_id_filters_use_string_only_for_non_hex_ids() {
let id = "customer-42";
let filters = document_id_filters(id);
let filters = document_id_filters(&id);
assert_eq!(filters.len(), 1);
assert!(matches!(filters[0].get("_id"), Some(Bson::String(value)) if value == id));
}
#[test]
fn document_id_filters_preserve_extended_json_int64_ids() {
let filters = document_id_filters(r#"{"$numberLong":"2048938405781032962"}"#);
assert_eq!(filters.len(), 1);
assert!(matches!(filters[0].get("_id"), Some(Bson::Int64(2_048_938_405_781_032_962))));
}
#[test]
fn document_id_filters_decode_explicit_string_ids_before_extended_json() {
let original = r#"{"$numberLong":"2048938405781032962"}"#;
let id = format!("__dbx_mongo_string_id__{}", serde_json::to_string(original).unwrap());
let filters = document_id_filters(&id);
assert_eq!(filters.len(), 1);
assert!(
matches!(filters[0].get("_id"), Some(Bson::String(value)) if value == r####"{"$numberLong":"2048938405781032962"}"####)
);
}
#[test]
fn json_filter_to_document_preserves_extended_json_int64_values() {
let filter = serde_json::json!({ "snowflake": { "$numberLong": "2048938405781032962" } });
let document = json_filter_to_document(&filter).unwrap();
assert!(matches!(document.get("snowflake"), Some(Bson::Int64(2_048_938_405_781_032_962))));
}
#[test]
fn json_filter_to_document_matches_object_id_and_string_for_id_hex() {
let id = "507f1f77bcf86cd799439011";
@@ -1468,6 +1542,17 @@ mod tests {
assert_eq!(value, serde_json::json!("2326645729978441729"));
}
#[test]
fn bson_to_json_preserves_int64_id_type_for_updates() {
let value = bson_to_json(&Bson::Document(doc! {
"_id": Bson::Int64(2_048_938_405_781_032_962),
"snowflake": Bson::Int64(2_048_938_405_781_032_962),
}));
assert_eq!(value["_id"], serde_json::json!({ "$numberLong": "2048938405781032962" }));
assert_eq!(value["snowflake"], serde_json::json!("2048938405781032962"));
}
#[test]
fn bson_to_json_keeps_safe_int64_as_number() {
let value = bson_to_json(&Bson::Int64(42));