What's wrong with the following snippet:
...
let collection: Collection<CodeData> = db.collection(CODE_DATA_COLLECTION);
let mut memory_breakpoints_update = Document::new();
for address in start_address..=end_address {
memory_breakpoints_update
.insert(address.to_string(), doc! { "read": read, "write": write });
}
match collection
.update_many(
doc! { "game": game_name},
doc! { "$set": {
"memory_blocks": {
"$mergeObjects": [ memory_breakpoints_update, "$memory_blocks" ]
}
}},
None,
)
.await
...
Expected that subdocument memory_blocks will be updated. Instead, I got
"memory_breakpoints": {
"$mergeObjects": [
{
"240": {
"read": true,
"write": false
},
"241": {
"read": true,
"write": false
},
"242": {
"read": true,
"write": false
},
"243": {
"read": true,
"write": false
},
"244": {
"read": true,
"write": false
},
"245": {
"read": true,
"write": false
}
},
"$memory_breakpoints"
]
},
On other side, this code works fine in Mongo playground:
db.collection.update({},
[
{
$set: {
"memory_breakpoints": {
$mergeObjects: [
{
"10": {
"read": false,
"write": true
},
"11": {
"read": false,
"write": true
}
},
"$memory_breakpoints"
]
}
}
}
])
As a temp solution, had to use a brutal force:
pub async fn update_memory_breakpoints(
db: &State<Database>,
game_name: &String,
start_address: i32,
end_address: i32,
read: bool,
write: bool,
) -> Result<bool, ....> {
let collection: Collection<CodeData> = db.collection(CODE_DATA_COLLECTION);
if !read && !write {
// delete
}
// update
// find the code_data document in order to read the current memory breakpoints map
let code_data: CodeData;
match collection.find_one(doc! { "game": game_name }, None).await {
Ok(result) => match result {
Some(document) => {
code_data = document;
}
None => {
return ....;
}
},
Err(_error) => {
return ...;
}
}
// create a sequence of breakpoints from start_address to end_address
let mut memory_breakpoints_update = HashMap::new();
for address in start_address..=end_address {
memory_breakpoints_update.insert(address.to_string(), MemoryBreakpoint { read, write });
}
println!("memory_breakpoints_update {:?}", &memory_breakpoints_update);
// current breakpoints overwritten by the new sequence
let memory_breakpoints: HashMap<String, MemoryBreakpoint> = code_data
.memory_breakpoints
.into_iter()
.chain(memory_breakpoints_update)
.collect();
println!("memory_breakpoints {:?}", &memory_breakpoints);
let mut memory_breakpoints_bson = Document::new();
memory_breakpoints.iter().for_each(|(k, v)| {
memory_breakpoints_bson.insert(k.to_string(), doc! { "read": v.read, "write": v.write });
});
println!("memory_breakpoints_bson {}", memory_breakpoints_bson);
match collection
.update_many(
doc! { "game": game_name },
doc! { "$set": {
"memory_breakpoints": memory_breakpoints_bson
}},
None,
)
.await
{
...
}
}
Update
The solution based on the aggregation pipeline
let collection: Collection<CodeData> = db.collection(CODE_DATA_COLLECTION);
// TODO: use fold
let mut update_object = Document::new();
for address in start_address..=end_address {
update_object.insert(address.to_string(), doc! { "read": read, "write": write });
}
let append = vec![doc! { "$set": {
"memory_breakpoints": {
"$mergeObjects": [ update_object, "$memory_breakpoints" ]
}
}}];
let delete_object: Vec<String> = (start_address..=end_address)
.map(|address| format!("memory_breakpoints.{}", address))
.collect();
println!("delete_object {:?}", delete_object);
let delete = vec![doc! { "$unset":
(delete_object)
}];
println!("delete {:?}", delete);
let update_op = match read || write {
true => append,
false => delete,
};
match collection
.update_many(doc! { "game": game_name }, update_op, None)
.await
CodePudding user response:
There is a subtle difference between your shell example and the Rust code that is leading to the different outcomes.
If I take your playground and modify it just slightly we effectively see the same output that you are getting with the Rust code:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"memory_breakpoints": {
"$mergeObjects": [
{
"10": {
"read": false,
"write": true
},
"11": {
"read": false,
"write": true
}
},
"$memory_breakpoints"
]
}
}
]
What did I change? I removed the [
and ]
wrapping the second argument to the update
which describes the modification that should happen. This matches the syntax that you currently have with the Rust code hence the similar results.
So what is going on? The second argument takes either a document or a pipeline. This makes a big difference as $mergeObjects
is an aggregation operator. So when you use the document syntax of the update
the database doesn't know that you are trying to instruct it to do something with $mergeObjects
and instead just interprets it as a field name directly.
Also confusing, $set
(along with other operators) mean different things in different contexts. It is both an update operator as well as an alias to the $addFields
aggregation stage. So by changing from the (aggregation) pipeline syntax to the document syntax you are also changing which $set
operator is running (also contributing to the change in interpretation mentioned above).
The solution here is to wrap the second argument to the update_many
method in your original Rust code with square brackets to make it an array.