diff --git a/docs/changelog.rst b/docs/changelog.rst index acf6b2a75..42e325285 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,7 @@ Development =========== - (Fill this out as you fix issues and develop your features). - Fix for uuidRepresentation not read when provided in URI #2741 +- Add option to user array_filters https://www.mongodb.com/docs/manual/reference/operator/update/positional-filtered/ #2769 - Fix combination of __raw__ and mongoengine syntax #2773 - Add tests against MongoDB 6.0 and MongoDB 7.0 in the pipeline - Fix validate() not being called when inheritance is used in EmbeddedDocument and validate is overriden #2784 diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index a11c7a9bd..d5ac70b07 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -252,6 +252,23 @@ and provide the pipeline as a list .. versionadded:: 0.23.2 +Update with Array Operator +-------------------------------- +It is possible to update specific value in array by use array_filters (arrayFilters) operator. +This is done by using ``__raw__`` keyword argument to the update method and provide the arrayFilters as a list. + +`Update with Array Operator `_ +:: + + # assuming an initial 'tags' field == ['test1', 'test2', 'test3'] + Page.objects().update(__raw__={'$set': {"tags.$[element]": 'test11111'}}, + array_filters=[{"element": {'$eq': 'test2'}}], + + # updated 'tags' field == ['test1', 'test11111', 'test3'] + + ) + + Sorting/Ordering results ======================== It is possible to order the results by 1 or more keys using :meth:`~mongoengine.queryset.QuerySet.order_by`. diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index d628ce16b..e0fb9cb26 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -531,6 +531,7 @@ def update( write_concern=None, read_concern=None, full_result=False, + array_filters=None, **update, ): """Perform an atomic update on the fields matched by the query. @@ -546,6 +547,7 @@ def update( :param read_concern: Override the read concern for the operation :param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number updated items + :param array_filters: A list of filters specifying which array elements an update should apply. :param update: Django-style update keyword arguments :returns the number of updated documents (unless ``full_result`` is True) @@ -560,7 +562,9 @@ def update( queryset = self.clone() query = queryset._query - if "__raw__" in update and isinstance(update["__raw__"], list): + if "__raw__" in update and isinstance( + update["__raw__"], list + ): # Case of Update with Aggregation Pipeline update = [ transform.update(queryset._document, **{"__raw__": u}) for u in update["__raw__"] @@ -581,7 +585,9 @@ def update( update_func = collection.update_one if multi: update_func = collection.update_many - result = update_func(query, update, upsert=upsert) + result = update_func( + query, update, upsert=upsert, array_filters=array_filters + ) if full_result: return result elif result.raw_result: diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 00cdf6c7a..59c6577e7 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -592,6 +592,65 @@ class Blog(Document): Blog.drop_collection() + def test_update_array_filters(self): + """Ensure that updating by array_filters works.""" + + class Comment(EmbeddedDocument): + comment_tags = ListField(StringField()) + + class Blog(Document): + tags = ListField(StringField()) + comments = EmbeddedDocumentField(Comment) + + Blog.drop_collection() + + # update one + Blog.objects.create(tags=["test1", "test2", "test3"]) + + Blog.objects().update_one( + __raw__={"$set": {"tags.$[element]": "test11111"}}, + array_filters=[{"element": {"$eq": "test2"}}], + ) + testc_blogs = Blog.objects(tags="test11111") + + assert testc_blogs.count() == 1 + + Blog.drop_collection() + + # update one inner list + comments = Comment(comment_tags=["test1", "test2", "test3"]) + Blog.objects.create(comments=comments) + + Blog.objects().update_one( + __raw__={"$set": {"comments.comment_tags.$[element]": "test11111"}}, + array_filters=[{"element": {"$eq": "test2"}}], + ) + testc_blogs = Blog.objects(comments__comment_tags="test11111") + + assert testc_blogs.count() == 1 + + # update many + Blog.drop_collection() + + Blog.objects.create(tags=["test1", "test2", "test3", "test_all"]) + Blog.objects.create(tags=["test4", "test5", "test6", "test_all"]) + + Blog.objects().update( + __raw__={"$set": {"tags.$[element]": "test11111"}}, + array_filters=[{"element": {"$eq": "test2"}}], + ) + testc_blogs = Blog.objects(tags="test11111") + + assert testc_blogs.count() == 1 + + Blog.objects().update( + __raw__={"$set": {"tags.$[element]": "test_all1234577"}}, + array_filters=[{"element": {"$eq": "test_all"}}], + ) + testc_blogs = Blog.objects(tags="test_all1234577") + + assert testc_blogs.count() == 2 + def test_update_using_positional_operator(self): """Ensure that the list fields can be updated using the positional operator."""