Support for $position with the $push operator (#1566)
This commit is contained in:
		
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							| @@ -244,3 +244,4 @@ that much better: | |||||||
|  * Stanislav Kaledin (https://github.com/sallyruthstruik) |  * Stanislav Kaledin (https://github.com/sallyruthstruik) | ||||||
|  * Dmitry Yantsen (https://github.com/mrTable) |  * Dmitry Yantsen (https://github.com/mrTable) | ||||||
|  * Renjianxin (https://github.com/Davidrjx) |  * Renjianxin (https://github.com/Davidrjx) | ||||||
|  |  * Erdenezul Batmunkh (https://github.com/erdenezul) | ||||||
| @@ -565,6 +565,15 @@ cannot use the `$` syntax in keyword arguments it has been mapped to `S`:: | |||||||
|     >>> post.tags |     >>> post.tags | ||||||
|     ['database', 'mongodb'] |     ['database', 'mongodb'] | ||||||
|  |  | ||||||
|  | From MongoDB version 2.6, push operator supports $position value which allows | ||||||
|  | to push values with index. | ||||||
|  |     >>> post = BlogPost(title="Test", tags=["mongo"]) | ||||||
|  |     >>> post.save() | ||||||
|  |     >>> post.update(push__tags__0=["database", "code"]) | ||||||
|  |     >>> post.reload() | ||||||
|  |     >>> post.tags | ||||||
|  |     ['database', 'code', 'mongo'] | ||||||
|  |  | ||||||
| .. note:: | .. note:: | ||||||
|     Currently only top level lists are handled, future versions of mongodb / |     Currently only top level lists are handled, future versions of mongodb / | ||||||
|     pymongo plan to support nested positional operators.  See `The $ positional |     pymongo plan to support nested positional operators.  See `The $ positional | ||||||
|   | |||||||
| @@ -284,7 +284,9 @@ def update(_doc_cls=None, **update): | |||||||
|             if isinstance(field, GeoJsonBaseField): |             if isinstance(field, GeoJsonBaseField): | ||||||
|                 value = field.to_mongo(value) |                 value = field.to_mongo(value) | ||||||
|  |  | ||||||
|             if op in (None, 'set', 'push', 'pull'): |             if op == 'push' and isinstance(value, (list, tuple, set)): | ||||||
|  |                 value = [field.prepare_query_value(op, v) for v in value] | ||||||
|  |             elif op in (None, 'set', 'push', 'pull'): | ||||||
|                 if field.required or value is not None: |                 if field.required or value is not None: | ||||||
|                     value = field.prepare_query_value(op, value) |                     value = field.prepare_query_value(op, value) | ||||||
|             elif op in ('pushAll', 'pullAll'): |             elif op in ('pushAll', 'pullAll'): | ||||||
| @@ -333,10 +335,22 @@ def update(_doc_cls=None, **update): | |||||||
|                 value = {key: value} |                 value = {key: value} | ||||||
|         elif op == 'addToSet' and isinstance(value, list): |         elif op == 'addToSet' and isinstance(value, list): | ||||||
|             value = {key: {'$each': value}} |             value = {key: {'$each': value}} | ||||||
|  |         elif op == 'push': | ||||||
|  |             if parts[-1].isdigit(): | ||||||
|  |                 key = parts[0] | ||||||
|  |                 position = int(parts[-1]) | ||||||
|  |                 # $position expects an iterable. If pushing a single value, | ||||||
|  |                 # wrap it in a list. | ||||||
|  |                 if not isinstance(value, (set, tuple, list)): | ||||||
|  |                     value = [value] | ||||||
|  |                 value = {key: {'$each': value, '$position': position}} | ||||||
|  |             elif isinstance(value, list): | ||||||
|  |                 value = {key: {'$each': value}} | ||||||
|  |             else: | ||||||
|  |                 value = {key: value} | ||||||
|         else: |         else: | ||||||
|             value = {key: value} |             value = {key: value} | ||||||
|         key = '$' + op |         key = '$' + op | ||||||
|  |  | ||||||
|         if key not in mongo_update: |         if key not in mongo_update: | ||||||
|             mongo_update[key] = value |             mongo_update[key] = value | ||||||
|         elif key in mongo_update and isinstance(mongo_update[key], dict): |         elif key in mongo_update and isinstance(mongo_update[key], dict): | ||||||
|   | |||||||
| @@ -22,6 +22,8 @@ from mongoengine.queryset import NULLIFY, Q | |||||||
| from mongoengine.context_managers import switch_db, query_counter | from mongoengine.context_managers import switch_db, query_counter | ||||||
| from mongoengine import signals | from mongoengine import signals | ||||||
|  |  | ||||||
|  | from tests.utils import needs_mongodb_v26 | ||||||
|  |  | ||||||
| TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), | TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), | ||||||
|                                '../fields/mongoengine.png') |                                '../fields/mongoengine.png') | ||||||
|  |  | ||||||
| @@ -826,6 +828,22 @@ class InstanceTest(unittest.TestCase): | |||||||
|  |  | ||||||
|         self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())]) |         self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())]) | ||||||
|  |  | ||||||
|  |     @needs_mongodb_v26 | ||||||
|  |     def test_modify_with_positional_push(self): | ||||||
|  |         class BlogPost(Document): | ||||||
|  |             tags = ListField(StringField()) | ||||||
|  |  | ||||||
|  |         post = BlogPost.objects.create(tags=['python']) | ||||||
|  |         self.assertEqual(post.tags, ['python']) | ||||||
|  |         post.modify(push__tags__0=['code', 'mongo']) | ||||||
|  |         self.assertEqual(post.tags, ['code', 'mongo', 'python']) | ||||||
|  |  | ||||||
|  |         # Assert same order of the list items is maintained in the db | ||||||
|  |         self.assertEqual( | ||||||
|  |             BlogPost._get_collection().find_one({'_id': post.pk})['tags'], | ||||||
|  |             ['code', 'mongo', 'python'] | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def test_save(self): |     def test_save(self): | ||||||
|         """Ensure that a document may be saved in the database.""" |         """Ensure that a document may be saved in the database.""" | ||||||
|  |  | ||||||
| @@ -3149,6 +3167,22 @@ class InstanceTest(unittest.TestCase): | |||||||
|  |  | ||||||
|         person.update(set__height=2.0) |         person.update(set__height=2.0) | ||||||
|  |  | ||||||
|  |     @needs_mongodb_v26 | ||||||
|  |     def test_push_with_position(self): | ||||||
|  |         """Ensure that push with position works properly for an instance.""" | ||||||
|  |         class BlogPost(Document): | ||||||
|  |             slug = StringField() | ||||||
|  |             tags = ListField(StringField()) | ||||||
|  |  | ||||||
|  |         blog = BlogPost() | ||||||
|  |         blog.slug = "ABC" | ||||||
|  |         blog.tags = ["python"] | ||||||
|  |         blog.save() | ||||||
|  |  | ||||||
|  |         blog.update(push__tags__0=["mongodb", "code"]) | ||||||
|  |         blog.reload() | ||||||
|  |         self.assertEqual(blog.tags, ['mongodb', 'code', 'python']) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     unittest.main() |     unittest.main() | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| import unittest | import unittest | ||||||
|  |  | ||||||
| from mongoengine import connect, Document, IntField | from mongoengine import connect, Document, IntField, StringField, ListField | ||||||
|  |  | ||||||
|  | from tests.utils import needs_mongodb_v26 | ||||||
|  |  | ||||||
| __all__ = ("FindAndModifyTest",) | __all__ = ("FindAndModifyTest",) | ||||||
|  |  | ||||||
| @@ -94,6 +96,37 @@ class FindAndModifyTest(unittest.TestCase): | |||||||
|         self.assertEqual(old_doc.to_mongo(), {"_id": 1}) |         self.assertEqual(old_doc.to_mongo(), {"_id": 1}) | ||||||
|         self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) |         self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) | ||||||
|  |  | ||||||
|  |     @needs_mongodb_v26 | ||||||
|  |     def test_modify_with_push(self): | ||||||
|  |         class BlogPost(Document): | ||||||
|  |             tags = ListField(StringField()) | ||||||
|  |  | ||||||
|  |         BlogPost.drop_collection() | ||||||
|  |  | ||||||
|  |         blog = BlogPost.objects.create() | ||||||
|  |  | ||||||
|  |         # Push a new tag via modify with new=False (default). | ||||||
|  |         BlogPost(id=blog.id).modify(push__tags='code') | ||||||
|  |         self.assertEqual(blog.tags, []) | ||||||
|  |         blog.reload() | ||||||
|  |         self.assertEqual(blog.tags, ['code']) | ||||||
|  |  | ||||||
|  |         # Push a new tag via modify with new=True. | ||||||
|  |         blog = BlogPost.objects(id=blog.id).modify(push__tags='java', new=True) | ||||||
|  |         self.assertEqual(blog.tags, ['code', 'java']) | ||||||
|  |  | ||||||
|  |         # Push a new tag with a positional argument. | ||||||
|  |         blog = BlogPost.objects(id=blog.id).modify( | ||||||
|  |             push__tags__0='python', | ||||||
|  |             new=True) | ||||||
|  |         self.assertEqual(blog.tags, ['python', 'code', 'java']) | ||||||
|  |  | ||||||
|  |         # Push multiple new tags with a positional argument. | ||||||
|  |         blog = BlogPost.objects(id=blog.id).modify( | ||||||
|  |             push__tags__1=['go', 'rust'], | ||||||
|  |             new=True) | ||||||
|  |         self.assertEqual(blog.tags, ['python', 'go', 'rust', 'code', 'java']) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     unittest.main() |     unittest.main() | ||||||
|   | |||||||
| @@ -1903,6 +1903,32 @@ class QuerySetTest(unittest.TestCase): | |||||||
|  |  | ||||||
|         BlogPost.drop_collection() |         BlogPost.drop_collection() | ||||||
|  |  | ||||||
|  |     @needs_mongodb_v26 | ||||||
|  |     def test_update_push_with_position(self): | ||||||
|  |         """Ensure that the 'push' update with position works properly. | ||||||
|  |         """ | ||||||
|  |         class BlogPost(Document): | ||||||
|  |             slug = StringField() | ||||||
|  |             tags = ListField(StringField()) | ||||||
|  |  | ||||||
|  |         BlogPost.drop_collection() | ||||||
|  |  | ||||||
|  |         post = BlogPost.objects.create(slug="test") | ||||||
|  |  | ||||||
|  |         BlogPost.objects.filter(id=post.id).update(push__tags="code") | ||||||
|  |         BlogPost.objects.filter(id=post.id).update(push__tags__0=["mongodb", "python"]) | ||||||
|  |         post.reload() | ||||||
|  |         self.assertEqual(post.tags, ['mongodb', 'python', 'code']) | ||||||
|  |  | ||||||
|  |         BlogPost.objects.filter(id=post.id).update(set__tags__2="java") | ||||||
|  |         post.reload() | ||||||
|  |         self.assertEqual(post.tags, ['mongodb', 'python', 'java']) | ||||||
|  |  | ||||||
|  |         #test push with singular value | ||||||
|  |         BlogPost.objects.filter(id=post.id).update(push__tags__0='scala') | ||||||
|  |         post.reload() | ||||||
|  |         self.assertEqual(post.tags, ['scala', 'mongodb', 'python', 'java']) | ||||||
|  |  | ||||||
|     def test_update_push_and_pull_add_to_set(self): |     def test_update_push_and_pull_add_to_set(self): | ||||||
|         """Ensure that the 'pull' update operation works correctly. |         """Ensure that the 'pull' update operation works correctly. | ||||||
|         """ |         """ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user