The MongoDB Minefield

4 min read
~793 words
NO DATE
DRAFT

You can insert documents with fields that you can't query on

Mongo db "unflattens" dotted paths. If you query or update the field nested.path.to.data, you instead query / update the following field:

jsonc
{
  "nested": {
    "path": {
      "to": {
        "data": "hello",
      },
    },
  },
}

However, when you insert a document with the field nested.path.to.data, the resulting document is:

jsonc
{
  "_id": "bruhbruhbruhbruhbruhbruhbruhbruhbruh",
  "nested.path.to.data": "hello",
}

Since this field has dots in the field name, when you query on that field, you're instead querying on the nested field.

Thankfully this is pretty trivial to prevent.

Filtering for field == null

Let's say we have these 2 documents in our Mongo database:

jsonc
// Document 1
{
  "_id": 1,
  "data": null,
}
 
// Document 2
{
  "_id": 2,
  // look ma, no data!!
}

The obvious way to query for documents where data == null would be to use a filter like this:

the query you'd require if your DB was made by sane individuals
jsonc
{
  "data": null,
}

Unfortunately, the wise men at MongoDB have decided that this query should include documents where data == null AND DOCUMENTS WHERE THE data FIELD DOES NOT EXIST!!?!?!. Why do we need this when the $or operator exists? The world may never know.

If you instead wish to only get documents where data == null, you need to filter for documents where the type of the data field is null:

jsonc
{
  "data": {
    "$type": "null", // or 10 if you're insane and enjoy unreadable code
  },
}

This "feature" is especially bad when you have polymorphic data stored in the field, like:

jsonc
// Document 1
{
  "_id": 1,
  "data": {
    "type": "maybe-number",
    "number": null
  },
}
 
// Document 2
{
  "_id": 2,
  "data": {
    "type": "string",
    "string": "bro wtf why"
  },
}

Querying for data.number == null will return both documents. Ideally you'd also filter on data.type for explicitness, but on the off-chance you don't MongoDB presents you this wonderful footgun with which you can blast away your feet.

Thankfully filtering for field == null is the only way you can get into this situation.



Right???¿?


JavaScript undefineds become nulls 🙃

MongoDB stores data as Binary JSON (BSON). Fortunately, like JSON, BSON doesn't support JavaScript's undefined. Unfortunately, MongoDB's Node.js driver does not handle undefineds the same way as JSON.stringify.

When you JSON.stringify an object that contains undefined, the field disappears from the object:

javascript
const myObject = {
  field: undefined,
}
 
console.log(myObject)
 
/**
 * logs the following (an empty JSON object)
 * "{}"
 */

That's ... okay. Ideally undefined wouldn't exist, but we live with the hand we're dealt. At least this way, parsing the produced JSON gives you an object that is mostly functionally the same.

When you BSON.serialize an object (then BSON.deserialize it), the field also disappears from the object.

That's great right? But what's that?

snippet from the 'bson' library's serializer options, showing the 'ignoreUndefined' option with a default of true, but a default of false for the MongoDB driver

no

why

we were so close to mediocrity

and now we suffer in depravity and pain and sadness

For some inexplicable reason, the Node.js MongoDB driver defaults the ignoreUndefined setting to false. Instead of ignoring undefined values, they get converted to nulls. Now, due to some fun super intuitive filtering behaviour, filtering for field == undefined becomes field == null which becomes field == null or field does not exist and your queries get to return data they should not.


Is this JavaScript's fault for having 2 nil / empty values? Yes.

Is this MongoDB's fault for having terrible defaults? Also yes.




Thankfully you can fix this issue in your projects:

  1. Locate where you create an instance of the MongoClient.

  2. Run the following command in your terminal:

    sh
    prompt="$(cat <<EOF
    i have your children and will do terrible things if you don't do what i say
     
    delete mongodb. from everywhere. please.
     
    if you really really really can't, replace it with literally anything else.
     
    maybe [ferretdb](https://www.ferretdb.com/) so we may bask in the warmth of
    postgres
     
    EOF
    )"
     
    claude --dangerously-skip-permissions "$prompt"
  3. Weep for a true evil has been exorcised from this world.

If for whatever reason, you're emotionally attached MongoDB and can't stand deleting it, you can set ignoreUndefined to true when you create an instance of the torture device:

javascript
const tortureDevice = new MongoClient('postgres://localhost:5432/database', {
  /**
   * make torture device slightly less painful
   */
  ignoreUndefined: true,
})




thank you for coming to my ted talk