From 14be7ba2e202b8bc7c0f8b7bc729aeb59d3f3e0a Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 21 Jun 2011 14:50:11 +0100 Subject: [PATCH] Added support for the $ positional operator closes #205 --- docs/changelog.rst | 1 + docs/guide/querying.rst | 34 ++++++++++++++------ mongoengine/queryset.py | 7 ++-- tests/queryset.py | 71 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 11 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0737171c..4fb5d627 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in dev ============== +- Added support for the positional operator - Updated geo index checking to be recursive and check in embedded documents - Updated default collection naming convention - Added Document Mixin support diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 1caed2d7..4f36e964 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -23,7 +23,7 @@ fetch documents from the database:: Filtering queries ================= The query may be filtered by calling the -:class:`~mongoengine.queryset.QuerySet` object with field lookup keyword +:class:`~mongoengine.queryset.QuerySet` object with field lookup keyword arguments. The keys in the keyword arguments correspond to fields on the :class:`~mongoengine.Document` you are querying:: @@ -84,7 +84,7 @@ Available operators are as follows: * ``nin`` -- value is not in list (a list of values should be provided) * ``mod`` -- ``value % x == y``, where ``x`` and ``y`` are two provided values * ``all`` -- every item in list of values provided is in array -* ``size`` -- the size of the array is +* ``size`` -- the size of the array is * ``exists`` -- value for field exists The following operators are available as shortcuts to querying with regular @@ -163,9 +163,9 @@ To retrieve a result that should be unique in the collection, use and :class:`~mongoengine.queryset.MultipleObjectsReturned` if more than one document matched the query. -A variation of this method exists, +A variation of this method exists, :meth:`~mongoengine.queryset.Queryset.get_or_create`, that will create a new -document with the query arguments if no documents match the query. An +document with the query arguments if no documents match the query. An additional keyword argument, :attr:`defaults` may be provided, which will be used as default values for the new document, in the case that it should need to be created:: @@ -240,7 +240,7 @@ Javascript code that is executed on the database server. Counting results ---------------- Just as with limiting and skipping results, there is a method on -:class:`~mongoengine.queryset.QuerySet` objects -- +:class:`~mongoengine.queryset.QuerySet` objects -- :meth:`~mongoengine.queryset.QuerySet.count`, but there is also a more Pythonic way of achieving this:: @@ -309,11 +309,11 @@ Advanced queries ================ Sometimes calling a :class:`~mongoengine.queryset.QuerySet` object with keyword arguments can't fully express the query you want to use -- for example if you -need to combine a number of constraints using *and* and *or*. This is made +need to combine a number of constraints using *and* and *or*. This is made possible in MongoEngine through the :class:`~mongoengine.queryset.Q` class. A :class:`~mongoengine.queryset.Q` object represents part of a query, and can be initialised using the same keyword-argument syntax you use to query -documents. To build a complex query, you may combine +documents. To build a complex query, you may combine :class:`~mongoengine.queryset.Q` objects using the ``&`` (and) and ``|`` (or) operators. To use a :class:`~mongoengine.queryset.Q` object, pass it in as the first positional argument to :attr:`Document.objects` when you filter it by @@ -434,7 +434,7 @@ Atomic updates ============== Documents may be updated atomically by using the :meth:`~mongoengine.queryset.QuerySet.update_one` and -:meth:`~mongoengine.queryset.QuerySet.update` methods on a +:meth:`~mongoengine.queryset.QuerySet.update` methods on a :meth:`~mongoengine.queryset.QuerySet`. There are several different "modifiers" that you may use with these methods: @@ -450,7 +450,7 @@ that you may use with these methods: * ``pull_all`` -- remove several values from a list * ``add_to_set`` -- add value to a list only if its not in the list already -The syntax for atomic updates is similar to the querying syntax, but the +The syntax for atomic updates is similar to the querying syntax, but the modifier comes before the field, not after it:: >>> post = BlogPost(title='Test', page_views=0, tags=['database']) @@ -467,3 +467,19 @@ modifier comes before the field, not after it:: >>> post.reload() >>> post.tags ['database', 'nosql'] + +The positional operator allows you to update list items without knowing the +index position, therefore making the update a single atomic operation. As we +cannot use the `$` syntax in keyword arguments it has been mapped to `S`:: + + >>> post = BlogPost(title='Test', page_views=0, tags=['database', 'mongo']) + >>> post.save() + >>> BlogPost.objects(id=post.id, tags='mongo').update(set__tags__S='mongodb') + >>> post.reload() + >>> post.tags + ['database', 'mongodb'] + +.. note :: + Currently only top level lists are handled, future versions of mongodb / + pymongo plan to support nested positional operators. See `The $ positional + operator `_. \ No newline at end of file diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index e2947a00..82138fec 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -1215,6 +1215,9 @@ class QuerySet(object): append_field = True for field in fields: if isinstance(field, str): + # Convert the S operator to $ + if field == 'S': + field = '$' parts.append(field) append_field = False else: @@ -1243,7 +1246,7 @@ class QuerySet(object): return mongo_update - def update(self, safe_update=True, upsert=False, write_options=None, **update): + def update(self, safe_update=True, upsert=False, multi=True, write_options=None, **update): """Perform an atomic update on the fields matched by the query. When ``safe_update`` is used, the number of affected documents is returned. @@ -1261,7 +1264,7 @@ class QuerySet(object): update = QuerySet._transform_update(self._document, **update) try: - ret = self._collection.update(self._query, update, multi=True, + ret = self._collection.update(self._query, update, multi=multi, upsert=upsert, safe=safe_update, **write_options) if ret is not None and 'n' in ret: diff --git a/tests/queryset.py b/tests/queryset.py index c5f177c2..c0860b5c 100644 --- a/tests/queryset.py +++ b/tests/queryset.py @@ -260,6 +260,77 @@ class QuerySetTest(unittest.TestCase): Blog.drop_collection() + def test_update_using_positional_operator(self): + """Ensure that the list fields can be updated using the positional + operator.""" + + class Comment(EmbeddedDocument): + by = StringField() + votes = IntField() + + class BlogPost(Document): + title = StringField() + comments = ListField(EmbeddedDocumentField(Comment)) + + BlogPost.drop_collection() + + c1 = Comment(by="joe", votes=3) + c2 = Comment(by="jane", votes=7) + + BlogPost(title="ABC", comments=[c1, c2]).save() + + BlogPost.objects(comments__by="joe").update(inc__comments__S__votes=1) + + post = BlogPost.objects.first() + self.assertEquals(post.comments[0].by, 'joe') + self.assertEquals(post.comments[0].votes, 4) + + # Currently the $ operator only applies to the first matched item in + # the query + + class Simple(Document): + x = ListField() + + Simple.drop_collection() + Simple(x=[1, 2, 3, 2]).save() + Simple.objects(x=2).update(inc__x__S=1) + + simple = Simple.objects.first() + self.assertEquals(simple.x, [1, 3, 3, 2]) + Simple.drop_collection() + + # You can set multiples + Simple.drop_collection() + Simple(x=[1, 2, 3, 4]).save() + Simple(x=[2, 3, 4, 5]).save() + Simple(x=[3, 4, 5, 6]).save() + Simple(x=[4, 5, 6, 7]).save() + Simple.objects(x=3).update(set__x__S=0) + + s = Simple.objects() + self.assertEquals(s[0].x, [1, 2, 0, 4]) + self.assertEquals(s[1].x, [2, 0, 4, 5]) + self.assertEquals(s[2].x, [0, 4, 5, 6]) + self.assertEquals(s[3].x, [4, 5, 6, 7]) + + # Using "$unset" with an expression like this "array.$" will result in + # the array item becoming None, not being removed. + Simple.drop_collection() + Simple(x=[1, 2, 3, 4, 3, 2, 3, 4]).save() + Simple.objects(x=3).update(unset__x__S=1) + simple = Simple.objects.first() + self.assertEquals(simple.x, [1, 2, None, 4, 3, 2, 3, 4]) + + # Nested updates arent supported yet.. + def update_nested(): + Simple.drop_collection() + Simple(x=[{'test': [1, 2, 3, 4]}]).save() + Simple.objects(x__test=2).update(set__x__S__test__S=3) + self.assertEquals(simple.x, [1, 2, 3, 4]) + + self.assertRaises(OperationError, update_nested) + Simple.drop_collection() + def test_mapfield_update(self): """Ensure that the MapField can be updated.""" class Member(EmbeddedDocument):