From 996aebbe34e5723e273ff9d74233b3ab19587350 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 1 Jan 2025 09:32:52 -0500 Subject: [PATCH 1/3] add tests for Value.as_mql() --- tests/expressions_/__init__.py | 0 tests/expressions_/test_value.py | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/expressions_/__init__.py create mode 100644 tests/expressions_/test_value.py diff --git a/tests/expressions_/__init__.py b/tests/expressions_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/expressions_/test_value.py b/tests/expressions_/test_value.py new file mode 100644 index 000000000..3325092cf --- /dev/null +++ b/tests/expressions_/test_value.py @@ -0,0 +1,47 @@ +import datetime +import uuid +from decimal import Decimal + +from bson import Decimal128 +from django.db.models import Value +from django.test import SimpleTestCase + + +class ValueTests(SimpleTestCase): + def test_date(self): + self.assertEqual( + Value(datetime.date(2025, 1, 1)).as_mql(None, None), + {"$literal": datetime.datetime(2025, 1, 1)}, + ) + + def test_datetime(self): + self.assertEqual( + Value(datetime.datetime(2025, 1, 1, 9, 8, 7)).as_mql(None, None), + {"$literal": datetime.datetime(2025, 1, 1)}, + ) + + def test_decimal(self): + self.assertEqual(Value(Decimal("1.0")).as_mql(None, None), {"$literal": Decimal128("1.0")}) + + def test_time(self): + self.assertEqual( + Value(datetime.time(9, 8, 7)).as_mql(None, None), + {"$literal": datetime.datetime(1, 1, 1, 9, 8, 7)}, + ) + + def test_timedelta(self): + self.assertEqual( + Value(datetime.timedelta(3600)).as_mql(None, None), {"$literal": 311040000000.0} + ) + + def test_int(self): + self.assertEqual(Value(1).as_mql(None, None), {"$literal": 1}) + + def test_str(self): + self.assertEqual(Value("foo").as_mql(None, None), {"$literal": "foo"}) + + def test_uuid(self): + value = uuid.UUID(int=1) + self.assertEqual( + Value(value).as_mql(None, None), {"$literal": "00000000000000000000000000000001"} + ) From 0309b8a534c4410d078e8d303b6d7c291038ca8e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 1 Jan 2025 09:35:15 -0500 Subject: [PATCH 2/3] remove unnecessary $literal wrapping in Value.as_mql() Only integers need to be wrapped. --- django_mongodb/expressions.py | 24 ++++++++++++++---------- tests/expressions_/test_value.py | 18 +++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/django_mongodb/expressions.py b/django_mongodb/expressions.py index 957c5f155..477d0101b 100644 --- a/django_mongodb/expressions.py +++ b/django_mongodb/expressions.py @@ -203,20 +203,24 @@ def when(self, compiler, connection): def value(self, compiler, connection): # noqa: ARG001 value = self.value + if isinstance(value, int): + # Wrap numbers in $literal to prevent ambiguity when Value appears in + # $project. + return {"$literal": value} if isinstance(value, Decimal): - value = Decimal128(value) - elif isinstance(value, datetime.date): + return Decimal128(value) + if isinstance(value, datetime.date): # Turn dates into datetimes since BSON doesn't support dates. - value = datetime.datetime.combine(value, datetime.datetime.min.time()) - elif isinstance(value, datetime.time): + return datetime.datetime.combine(value, datetime.datetime.min.time()) + if isinstance(value, datetime.time): # Turn times into datetimes since BSON doesn't support times. - value = datetime.datetime.combine(datetime.datetime.min.date(), value) - elif isinstance(value, datetime.timedelta): + return datetime.datetime.combine(datetime.datetime.min.date(), value) + if isinstance(value, datetime.timedelta): # DurationField stores milliseconds rather than microseconds. - value /= datetime.timedelta(milliseconds=1) - elif isinstance(value, UUID): - value = value.hex - return {"$literal": value} + return value / datetime.timedelta(milliseconds=1) + if isinstance(value, UUID): + return value.hex + return value def register_expressions(): diff --git a/tests/expressions_/test_value.py b/tests/expressions_/test_value.py index 3325092cf..51ac2d280 100644 --- a/tests/expressions_/test_value.py +++ b/tests/expressions_/test_value.py @@ -11,37 +11,33 @@ class ValueTests(SimpleTestCase): def test_date(self): self.assertEqual( Value(datetime.date(2025, 1, 1)).as_mql(None, None), - {"$literal": datetime.datetime(2025, 1, 1)}, + datetime.datetime(2025, 1, 1), ) def test_datetime(self): self.assertEqual( Value(datetime.datetime(2025, 1, 1, 9, 8, 7)).as_mql(None, None), - {"$literal": datetime.datetime(2025, 1, 1)}, + datetime.datetime(2025, 1, 1), ) def test_decimal(self): - self.assertEqual(Value(Decimal("1.0")).as_mql(None, None), {"$literal": Decimal128("1.0")}) + self.assertEqual(Value(Decimal("1.0")).as_mql(None, None), Decimal128("1.0")) def test_time(self): self.assertEqual( Value(datetime.time(9, 8, 7)).as_mql(None, None), - {"$literal": datetime.datetime(1, 1, 1, 9, 8, 7)}, + datetime.datetime(1, 1, 1, 9, 8, 7), ) def test_timedelta(self): - self.assertEqual( - Value(datetime.timedelta(3600)).as_mql(None, None), {"$literal": 311040000000.0} - ) + self.assertEqual(Value(datetime.timedelta(3600)).as_mql(None, None), 311040000000.0) def test_int(self): self.assertEqual(Value(1).as_mql(None, None), {"$literal": 1}) def test_str(self): - self.assertEqual(Value("foo").as_mql(None, None), {"$literal": "foo"}) + self.assertEqual(Value("foo").as_mql(None, None), "foo") def test_uuid(self): value = uuid.UUID(int=1) - self.assertEqual( - Value(value).as_mql(None, None), {"$literal": "00000000000000000000000000000001"} - ) + self.assertEqual(Value(value).as_mql(None, None), "00000000000000000000000000000001") From b4c24e48dbbc6d795072e110ec0bbdd1801fdd34 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 1 Jan 2025 09:56:26 -0500 Subject: [PATCH 3/3] fix loss of time component in Value(datetime) --- django_mongodb/expressions.py | 2 ++ tests/expressions_/test_value.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/django_mongodb/expressions.py b/django_mongodb/expressions.py index 477d0101b..2d14f3d16 100644 --- a/django_mongodb/expressions.py +++ b/django_mongodb/expressions.py @@ -209,6 +209,8 @@ def value(self, compiler, connection): # noqa: ARG001 return {"$literal": value} if isinstance(value, Decimal): return Decimal128(value) + if isinstance(value, datetime.datetime): + return value if isinstance(value, datetime.date): # Turn dates into datetimes since BSON doesn't support dates. return datetime.datetime.combine(value, datetime.datetime.min.time()) diff --git a/tests/expressions_/test_value.py b/tests/expressions_/test_value.py index 51ac2d280..c57c2f032 100644 --- a/tests/expressions_/test_value.py +++ b/tests/expressions_/test_value.py @@ -17,7 +17,7 @@ def test_date(self): def test_datetime(self): self.assertEqual( Value(datetime.datetime(2025, 1, 1, 9, 8, 7)).as_mql(None, None), - datetime.datetime(2025, 1, 1), + datetime.datetime(2025, 1, 1, 9, 8, 7), ) def test_decimal(self):