From ae59919a6c06b844e2aba3f442648dbc68635dda Mon Sep 17 00:00:00 2001 From: Ben Taylor Date: Sun, 28 Jul 2013 15:12:38 +0100 Subject: [PATCH 1/5] Add andModify --- mongoengine/queryset/base.py | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index d3bb4c4b3..23f6db594 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -467,7 +467,40 @@ def update_one(self, upsert=False, write_concern=None, **update): """ return self.update( upsert=upsert, multi=False, write_concern=write_concern, **update) + + def andModify(self, upsert=False, sort=None, full_response=False, **update): + """Perform an atomic update on the fields matched by the query. Returns + a document. Essentially a wrapper for PyMongo's find_and_modify; itself + a wrapper for MongoDB's findAndModify. + :param upsert: Any existing document with that "_id" is overwritten. + :param sort: a list of (key, direction) pairs specifying the sort order + for this query. + :param full_response: return the entire response object from the server. + :param update: Django-style update keyword arguments + + .. versionadded:: TBC + """ + if not update and not upsert: + raise OperationError("No update parameters, must either update or remove") + + queryset = self.clone() + query = queryset._query + update = transform.update(queryset._document, **update) + + try: + result = queryset._collection.find_and_modify(query, update, upsert=upsert, sort=sort, full_response=full_response) + if full_response: + if not result['value'] is None: + result['value'] = self._document._from_son(result['value']) + else: + if not result is None: + result = self._document._from_son(result) + return result + + except pymongo.errors.OperationFailure, err: + raise OperationError(u'findAndModify failed (%s)' % unicode(err)) + def with_id(self, object_id): """Retrieve the object matching the id provided. Uses `object_id` only and raises InvalidQueryError if a filter has been applied. Returns @@ -1476,4 +1509,4 @@ def _ensure_indexes(self): msg = ("Doc.objects()._ensure_indexes() is deprecated. " "Use Doc.ensure_indexes() instead.") warnings.warn(msg, DeprecationWarning) - self._document.__class__.ensure_indexes() \ No newline at end of file + self._document.__class__.ensure_indexes() From 38b1c07bed8a9199ab2bb27f47651f1ec2ee4643 Mon Sep 17 00:00:00 2001 From: Ben Taylor Date: Sun, 28 Jul 2013 15:50:15 +0100 Subject: [PATCH 2/5] Add first andModify test --- tests/queryset/queryset.py | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c56b31eb7..77c3fff33 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1445,6 +1445,58 @@ class BlogPost(Document): BlogPost.drop_collection() + def test_andModify(self): + """Ensure that atomic updates work properly. + """ + class BlogPost(Document): + title = StringField() + hits = IntField() + tags = ListField(StringField()) + + BlogPost.drop_collection() + + post = BlogPost(name="Test Post", hits=5, tags=['test']) + post.save() + + result = BlogPost.objects(name="Test Post").andModify(set__hits=10) + self.assertEqual(result.hits, 5) + + result = BlogPost.objects(name="Test Post").andModify(inc__hits=1) + self.assertEqual(result.hits, 10) + + result = BlogPost.objects(name="Test Post").andModify(dec__hits=1) + self.assertEqual(result.hits, 11) + + result = BlogPost.objects(name="Test Post").andModify(push__tags='mongo') + self.assertEqual(result.hits, 10) + + result = BlogPost.objects(name="Test Post").andModify(push_all__tags=['db', 'nosql']) + self.assertTrue('mongo' in result.tags) + + post.reload() + self.assertTrue('db' in post.tags and 'nosql' in post.tags) + + tags = post.tags + result = BlogPost.objects(name="Test Post").andModify(pop__tags=1) + self.assertEqual(result.tags, tags) + post.reload() + self.assertEqual(post.tags, tags[:-1]) + + result = BlogPost.objects(name="Test Post").andModify(add_to_set__tags='unique') + self.assertEqual(result.tags.count('unique'), 0) + result = BlogPost.objects(name="Test Post").andModify(add_to_set__tags='unique') + self.assertEqual(result.tags.count('unique'), 1) + post.reload() + self.assertEqual(post.tags.count('unique'), 1) + + self.assertNotEqual(post.hits, None) + result = BlogPost.objects(name="Test Post").andModify(unset__hits=1) + self.assertEqual(result.hits, 10) + post.reload() + self.assertEqual(post.hits, None) + + BlogPost.drop_collection() + def test_update_push_and_pull_add_to_set(self): """Ensure that the 'pull' update operation works correctly. """ From 4bd6812752f337e72dac8f6dd74164a954154d5e Mon Sep 17 00:00:00 2001 From: Ben Taylor Date: Sun, 28 Jul 2013 17:58:02 +0100 Subject: [PATCH 3/5] Fix test_andModify; add upsert test for andModify --- tests/queryset/queryset.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 77c3fff33..d4cb88259 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -566,6 +566,23 @@ def test_upsert(self): bob = self.Person.objects.first() self.assertEqual("Bob", bob.name) self.assertEqual(30, bob.age) + + def test_andModify_upsert(self): + self.Person.drop_collection() + + result = self.Person.objects(name="Bob").andModify(full_response=True, set__age=30) + self.assertEqual(result['value'], None) + bob = self.Person.objects(name="Bob").first() + self.assertEqual(bob, None) + + result = self.Person.objects(name="Bob").andModify(upsert=True, full_response=True, set__age=30) + self.assertEqual(result['value'], None) + self.assertEqual(result['lastErrorObject']['updatedExisting'], False) + self.assertTrue(isinstance(result['lastErrorObject']['upserted'], ObjectId)) + + bob = self.Person.objects(name="Bob").first() + self.assertEqual("Bob", bob.name) + self.assertEqual(30, bob.age) def test_upsert_one(self): self.Person.drop_collection() @@ -1455,42 +1472,42 @@ class BlogPost(Document): BlogPost.drop_collection() - post = BlogPost(name="Test Post", hits=5, tags=['test']) + post = BlogPost(title="Test Post", hits=5, tags=['test']) post.save() - result = BlogPost.objects(name="Test Post").andModify(set__hits=10) + result = BlogPost.objects(title="Test Post").andModify(set__hits=10) self.assertEqual(result.hits, 5) - result = BlogPost.objects(name="Test Post").andModify(inc__hits=1) + result = BlogPost.objects(title="Test Post").andModify(inc__hits=1) self.assertEqual(result.hits, 10) - result = BlogPost.objects(name="Test Post").andModify(dec__hits=1) + result = BlogPost.objects(title="Test Post").andModify(dec__hits=1) self.assertEqual(result.hits, 11) - result = BlogPost.objects(name="Test Post").andModify(push__tags='mongo') + result = BlogPost.objects(title="Test Post").andModify(push__tags='mongo') self.assertEqual(result.hits, 10) - result = BlogPost.objects(name="Test Post").andModify(push_all__tags=['db', 'nosql']) + result = BlogPost.objects(title="Test Post").andModify(push_all__tags=['db', 'nosql']) self.assertTrue('mongo' in result.tags) post.reload() self.assertTrue('db' in post.tags and 'nosql' in post.tags) tags = post.tags - result = BlogPost.objects(name="Test Post").andModify(pop__tags=1) + result = BlogPost.objects(title="Test Post").andModify(pop__tags=1) self.assertEqual(result.tags, tags) post.reload() self.assertEqual(post.tags, tags[:-1]) - result = BlogPost.objects(name="Test Post").andModify(add_to_set__tags='unique') + result = BlogPost.objects(title="Test Post").andModify(add_to_set__tags='unique') self.assertEqual(result.tags.count('unique'), 0) - result = BlogPost.objects(name="Test Post").andModify(add_to_set__tags='unique') + result = BlogPost.objects(title="Test Post").andModify(add_to_set__tags='unique') self.assertEqual(result.tags.count('unique'), 1) post.reload() self.assertEqual(post.tags.count('unique'), 1) self.assertNotEqual(post.hits, None) - result = BlogPost.objects(name="Test Post").andModify(unset__hits=1) + result = BlogPost.objects(title="Test Post").andModify(unset__hits=1) self.assertEqual(result.hits, 10) post.reload() self.assertEqual(post.hits, None) From b341e836312205d657268c60b32dc3e6007c1166 Mon Sep 17 00:00:00 2001 From: Ben Taylor Date: Sun, 28 Jul 2013 18:30:46 +0100 Subject: [PATCH 4/5] Add sort test to andModify --- tests/queryset/queryset.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index d4cb88259..8229b8db0 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -567,6 +567,15 @@ def test_upsert(self): self.assertEqual("Bob", bob.name) self.assertEqual(30, bob.age) + def test_upsert_one(self): + self.Person.drop_collection() + + self.Person.objects(name="Bob", age=30).update_one(upsert=True) + + bob = self.Person.objects.first() + self.assertEqual("Bob", bob.name) + self.assertEqual(30, bob.age) + def test_andModify_upsert(self): self.Person.drop_collection() @@ -584,15 +593,23 @@ def test_andModify_upsert(self): self.assertEqual("Bob", bob.name) self.assertEqual(30, bob.age) - def test_upsert_one(self): + def test_andModify_sort(self): self.Person.drop_collection() - - self.Person.objects(name="Bob", age=30).update_one(upsert=True) - - bob = self.Person.objects.first() - self.assertEqual("Bob", bob.name) - self.assertEqual(30, bob.age) - + + bob = self.Person(name="Bob", age=30); bob.save() + betty = self.Person(name="Betty", age=30); betty.save() + + result = self.Person.objects(age=30).andModify(sort=[('name', 1)], set__age=31) + self.assertEqual("Betty", result.name) + result = self.Person.objects(age=30).andModify(sort=[('name', 1)], set__age=31) + self.assertEqual("Bob", result.name) + + result = self.Person.objects(age=31).andModify(sort=[('name', -1)], set__age=32) + self.assertEqual("Bob", result.name) + + result = self.Person.objects.andModify(sort=[('age', 1)], set__age=32) + self.assertEqual("Betty", result.name) + def test_set_on_insert(self): self.Person.drop_collection() From e34d69d5b662c4538f81b93cc7315ad787e2d224 Mon Sep 17 00:00:00 2001 From: Ben Taylor Date: Sun, 28 Jul 2013 19:16:57 +0100 Subject: [PATCH 5/5] Add doc strings to andModify tests --- tests/queryset/queryset.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 8229b8db0..845b18f2d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -577,6 +577,9 @@ def test_upsert_one(self): self.assertEqual(30, bob.age) def test_andModify_upsert(self): + """Ensure that andModify can add a new document + """ + self.Person.drop_collection() result = self.Person.objects(name="Bob").andModify(full_response=True, set__age=30) @@ -594,6 +597,9 @@ def test_andModify_upsert(self): self.assertEqual(30, bob.age) def test_andModify_sort(self): + """Ensure sort can be used to select the record to find_and_modify + """ + self.Person.drop_collection() bob = self.Person(name="Bob", age=30); bob.save() @@ -1480,8 +1486,10 @@ class BlogPost(Document): BlogPost.drop_collection() def test_andModify(self): - """Ensure that atomic updates work properly. + """Ensure that andModify updates a record atomically and returns the + old record. """ + class BlogPost(Document): title = StringField() hits = IntField()