$pull doesn't work the same in Go with mongo-driver and in Mongosh

Issue

What?

Trying to remove an element from an array of objects in a mongo document.

I use:
go version go1.17.1 darwin/amd64;
go.mongodb.org/mongo-driver v 1.8.4;
MongoDB 5.0.6 Enterprise

Shortened version of the function with everything as plain as possible:

func (r *Repo) UpdateModelByID(ctx context.Context, id string) error {
    objID, err := primitive.ObjectIDFromHex(id)
    // I actually handle err here, it's nil

    filter := bson.D{{"_id", objID}}

    update = bson.D{{
       Key: "$pull",
       Value: bson.E{
          Key: "data",
          Value: bson.E{
             Key: "field_type",
             Value: bson.E{
                Key:   "$in",
                Value: []string{"field_type_1", "field_type_2"},
             },
          },
       },
    }}
    
    log.Debugln("UPDATE", update)

    result, err = cdm.getTemplatesCollection().UpdateOne(ctx, filter, update)
    // I actually handle err here, it's nil
    log.Debugf("RESULT_OF_UPDATE: %+v", result)
}

Expected:

Document with id I provided will no longer have elements in array "data" that have a field "field_type" equal to "field_type_1" or "field_type_2"

Got:

DEBU[0023] UPDATE [{$pull {data {field_type {$in [field_type_1, field_type_2]}}}}] 
DEBU[0023] RESULT_OF_UPDATE: &{MatchedCount:1 ModifiedCount:0 UpsertedCount:0 UpsertedID:<nil>}

Same command through mongosh:

db.templates.updateOne(
   { _id: ObjectId("6228a89d621d19a2f7977d2f") },
   { $pull: { data: {field_type: {$in: ["field_type_1", "field_type_2"]}}}
   }
)
{ acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0 }

And the elem-s are gone from the array.

Why could this be?

I did notice that native query has single curly brackets (just an object), while Go code uses bson.D which technically is an array of those same objects (bson.E) -> [{}].

I tried changing mongosh command to test:

    db.templates.updateOne(
   { _id: ObjectId("6228a89d621d19a2f7977d2f") },
   [{ $pull: { data: {field_type: {$in: ["field_type_1", "field_type_2"]}}}
   }]
)
MongoServerError: Unrecognized pipeline stage name: '$pull'

In Go I tried using bson.E instead of bson.D (though the latter is recommended for commands per documentation) and got this:

DEBU[0030] UPDATE {$pull {data {field_type {$in [field_type_1, field_type_2]}}}} 
ERRO[0030] cannot update model by id: update document must contain key beginning with '$'  method=UpdateModelByID templateId=6228a89d621d19a2f7977d2f

Solution

bson.D models a document, with an ordered list of key-value pairs, where the key is the property name, value is the property’s value.

So if you intend to use bson.D to model documents, you always have to write a bson.D composite literal where there’s a document in the equivalent shell command.

So your update document must look like this:

update := bson.D{{
    Key: "$pull", Value: bson.D{{
        Key: "data", Value: bson.D{{
            Key: "field_type", Value: bson.D{{
                Key: "$in", Value: []string{"field_type_1", "field_type_2"},
            }}},
        }},
    }},
}

If you omit the named fields from the composite literals, it’s reduced to this:

update := bson.D{
    {"$pull", bson.D{{
        "data", bson.D{{
            "field_type", bson.D{{
                "$in", []string{"field_type_1", "field_type_2"},
            }}},
        }},
    }},
}

Yes, it’s kinda ugly. Please note that if order of elements does not matter, it’s much easier to use bson.M values to define documents (it’s a map). For details, see bson.D vs bson.M for find queries

Your update could look like this defined with bson.M:

update := bson.M{
    "$pull": bson.M{
        "data": bson.M{
            "field_type": bson.M{
                "$in": []string{"field_type_1", "field_type_2"},
            },
        },
    },
}

Answered By – icza

Answer Checked By – Mary Flores (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.