Home > Enterprise >  Subdocument merge issue when MongoDB Rust Driver is used
Subdocument merge issue when MongoDB Rust Driver is used

Time:10-16

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"
        ]
      }
    }
  }
])

Playground

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.

  • Related