Skip to content

Commit 3794b18

Browse files
authored
Support for $position with the $push operator (#1566)
2 parents a7cab51 + f09256a commit 3794b18

File tree

6 files changed

+120
-3
lines changed

6 files changed

+120
-3
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,4 @@ that much better:
244244
* Stanislav Kaledin (https://github.com/sallyruthstruik)
245245
* Dmitry Yantsen (https://github.com/mrTable)
246246
* Renjianxin (https://github.com/Davidrjx)
247+
* Erdenezul Batmunkh (https://github.com/erdenezul)

docs/guide/querying.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,15 @@ cannot use the `$` syntax in keyword arguments it has been mapped to `S`::
565565
>>> post.tags
566566
['database', 'mongodb']
567567

568+
From MongoDB version 2.6, push operator supports $position value which allows
569+
to push values with index.
570+
>>> post = BlogPost(title="Test", tags=["mongo"])
571+
>>> post.save()
572+
>>> post.update(push__tags__0=["database", "code"])
573+
>>> post.reload()
574+
>>> post.tags
575+
['database', 'code', 'mongo']
576+
568577
.. note::
569578
Currently only top level lists are handled, future versions of mongodb /
570579
pymongo plan to support nested positional operators. See `The $ positional

mongoengine/queryset/transform.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ def update(_doc_cls=None, **update):
284284
if isinstance(field, GeoJsonBaseField):
285285
value = field.to_mongo(value)
286286

287-
if op in (None, 'set', 'push', 'pull'):
287+
if op == 'push' and isinstance(value, (list, tuple, set)):
288+
value = [field.prepare_query_value(op, v) for v in value]
289+
elif op in (None, 'set', 'push', 'pull'):
288290
if field.required or value is not None:
289291
value = field.prepare_query_value(op, value)
290292
elif op in ('pushAll', 'pullAll'):
@@ -333,10 +335,22 @@ def update(_doc_cls=None, **update):
333335
value = {key: value}
334336
elif op == 'addToSet' and isinstance(value, list):
335337
value = {key: {'$each': value}}
338+
elif op == 'push':
339+
if parts[-1].isdigit():
340+
key = parts[0]
341+
position = int(parts[-1])
342+
# $position expects an iterable. If pushing a single value,
343+
# wrap it in a list.
344+
if not isinstance(value, (set, tuple, list)):
345+
value = [value]
346+
value = {key: {'$each': value, '$position': position}}
347+
elif isinstance(value, list):
348+
value = {key: {'$each': value}}
349+
else:
350+
value = {key: value}
336351
else:
337352
value = {key: value}
338353
key = '$' + op
339-
340354
if key not in mongo_update:
341355
mongo_update[key] = value
342356
elif key in mongo_update and isinstance(mongo_update[key], dict):

tests/document/instance.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from mongoengine.context_managers import switch_db, query_counter
2323
from mongoengine import signals
2424

25+
from tests.utils import needs_mongodb_v26
26+
2527
TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__),
2628
'../fields/mongoengine.png')
2729

@@ -826,6 +828,22 @@ def test_modify_update(self):
826828

827829
self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())])
828830

831+
@needs_mongodb_v26
832+
def test_modify_with_positional_push(self):
833+
class BlogPost(Document):
834+
tags = ListField(StringField())
835+
836+
post = BlogPost.objects.create(tags=['python'])
837+
self.assertEqual(post.tags, ['python'])
838+
post.modify(push__tags__0=['code', 'mongo'])
839+
self.assertEqual(post.tags, ['code', 'mongo', 'python'])
840+
841+
# Assert same order of the list items is maintained in the db
842+
self.assertEqual(
843+
BlogPost._get_collection().find_one({'_id': post.pk})['tags'],
844+
['code', 'mongo', 'python']
845+
)
846+
829847
def test_save(self):
830848
"""Ensure that a document may be saved in the database."""
831849

@@ -3149,6 +3167,22 @@ class Person(Document):
31493167

31503168
person.update(set__height=2.0)
31513169

3170+
@needs_mongodb_v26
3171+
def test_push_with_position(self):
3172+
"""Ensure that push with position works properly for an instance."""
3173+
class BlogPost(Document):
3174+
slug = StringField()
3175+
tags = ListField(StringField())
3176+
3177+
blog = BlogPost()
3178+
blog.slug = "ABC"
3179+
blog.tags = ["python"]
3180+
blog.save()
3181+
3182+
blog.update(push__tags__0=["mongodb", "code"])
3183+
blog.reload()
3184+
self.assertEqual(blog.tags, ['mongodb', 'code', 'python'])
3185+
31523186

31533187
if __name__ == '__main__':
31543188
unittest.main()

tests/queryset/modify.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import unittest
22

3-
from mongoengine import connect, Document, IntField
3+
from mongoengine import connect, Document, IntField, StringField, ListField
4+
5+
from tests.utils import needs_mongodb_v26
46

57
__all__ = ("FindAndModifyTest",)
68

@@ -94,6 +96,37 @@ def test_modify_with_fields(self):
9496
self.assertEqual(old_doc.to_mongo(), {"_id": 1})
9597
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
9698

99+
@needs_mongodb_v26
100+
def test_modify_with_push(self):
101+
class BlogPost(Document):
102+
tags = ListField(StringField())
103+
104+
BlogPost.drop_collection()
105+
106+
blog = BlogPost.objects.create()
107+
108+
# Push a new tag via modify with new=False (default).
109+
BlogPost(id=blog.id).modify(push__tags='code')
110+
self.assertEqual(blog.tags, [])
111+
blog.reload()
112+
self.assertEqual(blog.tags, ['code'])
113+
114+
# Push a new tag via modify with new=True.
115+
blog = BlogPost.objects(id=blog.id).modify(push__tags='java', new=True)
116+
self.assertEqual(blog.tags, ['code', 'java'])
117+
118+
# Push a new tag with a positional argument.
119+
blog = BlogPost.objects(id=blog.id).modify(
120+
push__tags__0='python',
121+
new=True)
122+
self.assertEqual(blog.tags, ['python', 'code', 'java'])
123+
124+
# Push multiple new tags with a positional argument.
125+
blog = BlogPost.objects(id=blog.id).modify(
126+
push__tags__1=['go', 'rust'],
127+
new=True)
128+
self.assertEqual(blog.tags, ['python', 'go', 'rust', 'code', 'java'])
129+
97130

98131
if __name__ == '__main__':
99132
unittest.main()

tests/queryset/queryset.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1903,6 +1903,32 @@ class BlogPost(Document):
19031903

19041904
BlogPost.drop_collection()
19051905

1906+
@needs_mongodb_v26
1907+
def test_update_push_with_position(self):
1908+
"""Ensure that the 'push' update with position works properly.
1909+
"""
1910+
class BlogPost(Document):
1911+
slug = StringField()
1912+
tags = ListField(StringField())
1913+
1914+
BlogPost.drop_collection()
1915+
1916+
post = BlogPost.objects.create(slug="test")
1917+
1918+
BlogPost.objects.filter(id=post.id).update(push__tags="code")
1919+
BlogPost.objects.filter(id=post.id).update(push__tags__0=["mongodb", "python"])
1920+
post.reload()
1921+
self.assertEqual(post.tags, ['mongodb', 'python', 'code'])
1922+
1923+
BlogPost.objects.filter(id=post.id).update(set__tags__2="java")
1924+
post.reload()
1925+
self.assertEqual(post.tags, ['mongodb', 'python', 'java'])
1926+
1927+
#test push with singular value
1928+
BlogPost.objects.filter(id=post.id).update(push__tags__0='scala')
1929+
post.reload()
1930+
self.assertEqual(post.tags, ['scala', 'mongodb', 'python', 'java'])
1931+
19061932
def test_update_push_and_pull_add_to_set(self):
19071933
"""Ensure that the 'pull' update operation works correctly.
19081934
"""

0 commit comments

Comments
 (0)