Skip to content

Commit cb67abe

Browse files
committed
Fix Model.save() updates and QuerySet.update() for $-prefixed strings
1 parent 2a1b8ae commit cb67abe

File tree

6 files changed

+105
-4
lines changed

6 files changed

+105
-4
lines changed

django_mongodb_backend/compiler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,9 @@ def execute_sql(self, result_type):
883883
f"{field.__class__.__name__}."
884884
)
885885
prepared = field.get_db_prep_save(value, connection=self.connection)
886-
if hasattr(value, "as_mql"):
886+
if is_direct_value(value):
887+
prepared = {"$literal": prepared}
888+
else:
887889
prepared = prepared.as_mql(self, self.connection, as_expr=True)
888890
values[field.column] = prepared
889891
try:

django_mongodb_backend/expressions/builtins.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,8 @@ def when(self, compiler, connection):
211211

212212
def value(self, compiler, connection, as_expr=False): # noqa: ARG001
213213
value = self.value
214-
if isinstance(value, (list, int, str)) and as_expr:
215-
# Wrap lists, numbers, and strings in $literal to avoid ambiguity when Value
214+
if isinstance(value, (list, int, str, dict, tuple)) and as_expr:
215+
# Wrap lists, numbers, strings, dict and tuple in $literal to avoid ambiguity when Value
216216
# is used in queries' aggregate or update_many's $set.
217217
return {"$literal": value}
218218
if isinstance(value, Decimal):

django_mongodb_backend/test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,18 @@ def assertAggregateQuery(self, query, expected_collection, expected_pipeline):
1919
eval(pipeline[:-1], {"SON": SON, "ObjectId": ObjectId, "Decimal128": Decimal128}, {}), # noqa: S307
2020
expected_pipeline,
2121
)
22+
23+
def assertUpdateQuery(self, query, expected_collection, expected_condition, expected_values):
24+
"""
25+
Assert that the logged query is equal to:
26+
db.{expected_collection}.update_many({expected_condition}, {expected_values})
27+
"""
28+
prefix, pipeline = query.split("(", 1)
29+
_, collection, operator = prefix.split(".")
30+
self.assertEqual(operator, "update_many")
31+
self.assertEqual(collection, expected_collection)
32+
condition, values = eval( # noqa: S307
33+
pipeline[:-1], {"SON": SON, "ObjectId": ObjectId, "Decimal128": Decimal128}, {}
34+
)
35+
self.assertEqual(condition, expected_condition)
36+
self.assertEqual(values, expected_values)

tests/basic_/models.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ class Author(models.Model):
66

77
def __str__(self):
88
return self.name
9+
10+
11+
class Book(models.Model):
12+
name = models.CharField(max_length=10)
13+
data = models.JSONField(null=True)

tests/basic_/tests.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from django_mongodb_backend.test import MongoTestCaseMixin
55

6-
from .models import Author
6+
from .models import Author, Book
77

88

99
class SaveDollarPrefixTests(MongoTestCaseMixin, TestCase):
@@ -14,6 +14,70 @@ def test_insert_dollar_prefix(self):
1414
self.assertEqual(obj.name, "$foobar")
1515

1616

17+
class UpdateDollarPrefixTests(MongoTestCaseMixin, TestCase):
18+
def test_update_dollar_prefix(self):
19+
"""$-prefixed values are correctly saved on update."""
20+
obj = Author.objects.create(name="foobar")
21+
obj.name = "$updated"
22+
with self.assertNumQueries(1) as ctx:
23+
obj.save()
24+
obj.refresh_from_db()
25+
self.assertEqual(obj.name, "$updated")
26+
self.assertUpdateQuery(
27+
ctx.captured_queries[0]["sql"],
28+
"basic__author",
29+
{"_id": obj.id},
30+
[{"$set": {"name": {"$literal": "$updated"}}}],
31+
)
32+
33+
def test_update_dollar_prefix_in_value_expression(self):
34+
"""$-prefixed Value() expressions are correctly handled on update."""
35+
obj = Author.objects.create(name="foobar")
36+
obj.name = Value("$updated")
37+
with self.assertNumQueries(1) as ctx:
38+
obj.save()
39+
obj.refresh_from_db()
40+
self.assertEqual(obj.name, "$updated")
41+
self.assertUpdateQuery(
42+
ctx.captured_queries[0]["sql"],
43+
"basic__author",
44+
{"_id": obj.id},
45+
[{"$set": {"name": {"$literal": "$updated"}}}],
46+
)
47+
48+
def test_update_dict_value(self):
49+
"""MQL-like dict Value() expressions are correctly handled on update."""
50+
obj = Book.objects.create(name="foobar", data={})
51+
obj.data = Value({"$concat": ["$name", "-", "$name"]})
52+
obj.save()
53+
obj.refresh_from_db()
54+
self.assertEqual(obj.data, {"$concat": ["$name", "-", "$name"]})
55+
56+
def test_update_dict(self):
57+
"""MQL-like dict updates are correctly handled on update."""
58+
obj = Book.objects.create(name="foobar")
59+
obj.data = {"$concat": ["$name", "-", "$name"]}
60+
obj.save()
61+
obj.refresh_from_db()
62+
self.assertEqual(obj.data, {"$concat": ["$name", "-", "$name"]})
63+
64+
def test_update_tuple(self):
65+
"""MQL-like tuple updates are correctly handled on update."""
66+
obj = Book.objects.create(name="foobar")
67+
obj.data = ("$name", "-", "$name")
68+
obj.save()
69+
obj.refresh_from_db()
70+
self.assertEqual(obj.data, ["$name", "-", "$name"])
71+
72+
def test_update_tuple_value(self):
73+
"""MQL-like tuple Value() expressions are correctly handled on update."""
74+
obj = Book.objects.create(name="foobar")
75+
obj.data = Value(("$name", "-", "$name"))
76+
obj.save()
77+
obj.refresh_from_db()
78+
self.assertEqual(obj.data, ["$name", "-", "$name"])
79+
80+
1781
class QueryDollarPrefixTests(MongoTestCaseMixin, TestCase):
1882
def test_query_injection(self):
1983
"""$-prefixed Value() expressions are correctly handled on update."""

tests/expressions_/test_value.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,24 @@ def test_int(self):
4141
def test_str(self):
4242
self.assertEqual(Value("foo").as_mql(None, None), "foo")
4343

44+
def test_array_expr(self):
45+
self.assertEqual(
46+
Value(["$foo", "$bar"]).as_mql(None, None, True), {"$literal": ["$foo", "$bar"]}
47+
)
48+
49+
def test_dict_expr(self):
50+
self.assertEqual(
51+
Value({"$foo": "$bar"}).as_mql(None, None, True), {"$literal": {"$foo": "$bar"}}
52+
)
53+
4454
def test_str_expr(self):
4555
self.assertEqual(Value("$foo").as_mql(None, None, True), {"$literal": "$foo"})
4656

57+
def test_tuple_expr(self):
58+
self.assertEqual(
59+
Value(("$foo", "$bar")).as_mql(None, None, True), {"$literal": ("$foo", "$bar")}
60+
)
61+
4762
def test_uuid(self):
4863
value = uuid.UUID(int=1)
4964
self.assertEqual(Value(value).as_mql(None, None), "00000000000000000000000000000001")

0 commit comments

Comments
 (0)