From 658f936334aa5e6529b6dcd8ce4902cc31f363c0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 2 May 2025 21:33:44 -0400 Subject: [PATCH 1/3] add update.tests.AdvancedTests.test_update_values_annotation to skipped tests --- django_mongodb_backend/features.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_mongodb_backend/features.py b/django_mongodb_backend/features.py index 0df1615b3..41bfb3725 100644 --- a/django_mongodb_backend/features.py +++ b/django_mongodb_backend/features.py @@ -278,6 +278,7 @@ def django_test_expected_failures(self): "update.tests.AdvancedTests.test_update_annotated_multi_table_queryset", "update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation", "update.tests.AdvancedTests.test_update_ordered_by_m2m_annotation_desc", + "update.tests.AdvancedTests.test_update_values_annotation", }, "QuerySet.dates() is not supported on MongoDB.": { "admin_changelist.tests.ChangeListTests.test_computed_list_display_localization", From 85f02ef77bd1bcf7786eb0607eea22ad4f205fdf Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 1 May 2025 22:08:34 -0400 Subject: [PATCH 2/3] Fix Trunc database function with tzinfo parameter --- django_mongodb_backend/features.py | 4 ---- django_mongodb_backend/functions.py | 32 ++++++++++++++++++++++++++++ django_mongodb_backend/operations.py | 14 +++++++++--- docs/source/releases/5.1.x.rst | 2 ++ docs/source/releases/5.2.x.rst | 2 ++ docs/source/topics/known-issues.rst | 4 ---- 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/django_mongodb_backend/features.py b/django_mongodb_backend/features.py index 41bfb3725..2617fbccb 100644 --- a/django_mongodb_backend/features.py +++ b/django_mongodb_backend/features.py @@ -57,10 +57,6 @@ class DatabaseFeatures(BaseDatabaseFeatures): # Pattern lookups that use regexMatch don't work on JSONField: # Unsupported conversion from array to string in $convert "model_fields.test_jsonfield.TestQuerying.test_icontains", - # Truncating in another timezone doesn't work becauase MongoDB converts - # the result back to UTC. - "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_func_with_timezone", - "db_functions.datetime.test_extract_trunc.DateFunctionWithTimeZoneTests.test_trunc_timezone_applied_before_truncation", # Unexpected alias_refcount in alias_map. "queries.tests.Queries1Tests.test_order_by_tables", # The $sum aggregation returns 0 instead of None for null. diff --git a/django_mongodb_backend/functions.py b/django_mongodb_backend/functions.py index 42838d1a7..f33c6434c 100644 --- a/django_mongodb_backend/functions.py +++ b/django_mongodb_backend/functions.py @@ -1,4 +1,8 @@ +from datetime import datetime + +from django.conf import settings from django.db import NotSupportedError +from django.db.models import DateField, DateTimeField, TimeField from django.db.models.expressions import Func from django.db.models.functions import JSONArray from django.db.models.functions.comparison import Cast, Coalesce, Greatest, Least, NullIf @@ -196,6 +200,33 @@ def trunc(self, compiler, connection): return {"$dateTrunc": lhs_mql} +def trunc_convert_value(self, value, expression, connection): + if connection.vendor == "mongodb": + # A custom TruncBase.convert_value() for MongoDB. + if value is None: + return None + convert_to_tz = settings.USE_TZ and self.get_tzname() != "UTC" + if isinstance(self.output_field, DateTimeField): + if convert_to_tz: + # Unlike other databases, MongoDB returns the value in UTC, + # so rather than setting the time zone equal to self.tzinfo, + # the value must be converted to tzinfo. + value = value.astimezone(self.tzinfo) + elif isinstance(value, datetime): + if isinstance(self.output_field, DateField): + if convert_to_tz: + value = value.astimezone(self.tzinfo) + # Truncate for Trunc(..., output_field=DateField) + value = value.date() + elif isinstance(self.output_field, TimeField): + if convert_to_tz: + value = value.astimezone(self.tzinfo) + # Truncate for Trunc(..., output_field=TimeField) + value = value.time() + return value + return self.convert_value(value, expression, connection) + + def trunc_date(self, compiler, connection): # Cast to date rather than truncate to date. lhs_mql = process_lhs(self, compiler, connection) @@ -256,6 +287,7 @@ def register_functions(): Substr.as_mql = substr Trim.as_mql = trim("trim") TruncBase.as_mql = trunc + TruncBase.convert_value = trunc_convert_value TruncDate.as_mql = trunc_date TruncTime.as_mql = trunc_time Upper.as_mql = preserve_null("toUpper") diff --git a/django_mongodb_backend/operations.py b/django_mongodb_backend/operations.py index d2f5869c1..4a63eccb7 100644 --- a/django_mongodb_backend/operations.py +++ b/django_mongodb_backend/operations.py @@ -10,7 +10,7 @@ from django.db.backends.base.operations import BaseDatabaseOperations from django.db.models import TextField from django.db.models.expressions import Combinable, Expression -from django.db.models.functions import Cast +from django.db.models.functions import Cast, Trunc from django.utils import timezone from django.utils.regex_helper import _lazy_re_compile @@ -97,7 +97,11 @@ def get_db_converters(self, expression): ] ) elif internal_type == "DateField": - converters.append(self.convert_datefield_value) + # Trunc(... output_field="DateField") values must remain datetime + # until Trunc.convert_value() so they can be converted from UTC + # before truncation. + if not isinstance(expression, Trunc): + converters.append(self.convert_datefield_value) elif internal_type == "DateTimeField": if settings.USE_TZ: converters.append(self.convert_datetimefield_value) @@ -106,7 +110,11 @@ def get_db_converters(self, expression): elif internal_type == "JSONField": converters.append(self.convert_jsonfield_value) elif internal_type == "TimeField": - converters.append(self.convert_timefield_value) + # Trunc(... output_field="TimeField") values must remain datetime + # until Trunc.convert_value() so they can be converted from UTC + # before truncation. + if not isinstance(expression, Trunc): + converters.append(self.convert_timefield_value) elif internal_type == "UUIDField": converters.append(self.convert_uuidfield_value) return converters diff --git a/docs/source/releases/5.1.x.rst b/docs/source/releases/5.1.x.rst index dde2e03a2..ce3a5877b 100644 --- a/docs/source/releases/5.1.x.rst +++ b/docs/source/releases/5.1.x.rst @@ -10,6 +10,8 @@ Django MongoDB Backend 5.1.x - Added support for a field's custom lookups and transforms in ``EmbeddedModelField``, e.g. ``ArrayField``’s ``contains``, ``contained__by``, ``len``, etc. +- Fixed the results of queries that use the ``tzinfo`` parameter of the + ``Trunc`` database functions. .. _django-mongodb-backend-5.1.0-beta-2: diff --git a/docs/source/releases/5.2.x.rst b/docs/source/releases/5.2.x.rst index 61b6b9e61..54c1dd124 100644 --- a/docs/source/releases/5.2.x.rst +++ b/docs/source/releases/5.2.x.rst @@ -19,3 +19,5 @@ Bug fixes - Added support for a field's custom lookups and transforms in ``EmbeddedModelField``, e.g. ``ArrayField``’s ``contains``, ``contained__by``, ``len``, etc. +- Fixed the results of queries that use the ``tzinfo`` parameter of the + ``Trunc`` database functions. diff --git a/docs/source/topics/known-issues.rst b/docs/source/topics/known-issues.rst index 4b34fecfd..deaef7424 100644 --- a/docs/source/topics/known-issues.rst +++ b/docs/source/topics/known-issues.rst @@ -75,10 +75,6 @@ Database functions :class:`~django.db.models.functions.SHA512` - :class:`~django.db.models.functions.Sign` -- The ``tzinfo`` parameter of the :class:`~django.db.models.functions.Trunc` - database functions doesn't work properly because MongoDB converts the result - back to UTC. - Transaction management ====================== From d04cbc5bf2f6ad5baae49b030eb7f00a033ca90f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 2 May 2025 20:32:41 -0400 Subject: [PATCH 3/3] Document the tzinfo parameter of TruncDate/TruncTime as unsupported Add the same exception raising from TruncDate to TruncTime and add tests for both functions. --- django_mongodb_backend/functions.py | 3 +++ docs/source/topics/known-issues.rst | 5 +++++ tests/db_functions_/models.py | 5 +++++ tests/db_functions_/test_datetime.py | 26 ++++++++++++++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 tests/db_functions_/models.py create mode 100644 tests/db_functions_/test_datetime.py diff --git a/django_mongodb_backend/functions.py b/django_mongodb_backend/functions.py index f33c6434c..492316709 100644 --- a/django_mongodb_backend/functions.py +++ b/django_mongodb_backend/functions.py @@ -249,6 +249,9 @@ def trunc_date(self, compiler, connection): def trunc_time(self, compiler, connection): + tzname = self.get_tzname() + if tzname and tzname != "UTC": + raise NotSupportedError(f"TruncTime with tzinfo ({tzname}) isn't supported on MongoDB.") lhs_mql = process_lhs(self, compiler, connection) return { "$dateFromString": { diff --git a/docs/source/topics/known-issues.rst b/docs/source/topics/known-issues.rst index deaef7424..ac9a5d306 100644 --- a/docs/source/topics/known-issues.rst +++ b/docs/source/topics/known-issues.rst @@ -75,6 +75,11 @@ Database functions :class:`~django.db.models.functions.SHA512` - :class:`~django.db.models.functions.Sign` +- The ``tzinfo`` parameter of the + :class:`~django.db.models.functions.TruncDate` and + :class:`~django.db.models.functions.TruncTime` database functions isn't + supported. + Transaction management ====================== diff --git a/tests/db_functions_/models.py b/tests/db_functions_/models.py new file mode 100644 index 000000000..17b9ad1a7 --- /dev/null +++ b/tests/db_functions_/models.py @@ -0,0 +1,5 @@ +from django.db import models + + +class DTModel(models.Model): + start_datetime = models.DateTimeField(null=True, blank=True) diff --git a/tests/db_functions_/test_datetime.py b/tests/db_functions_/test_datetime.py new file mode 100644 index 000000000..e0df28801 --- /dev/null +++ b/tests/db_functions_/test_datetime.py @@ -0,0 +1,26 @@ +from zoneinfo import ZoneInfo + +from django.db import NotSupportedError +from django.db.models.functions import TruncDate, TruncTime +from django.test import TestCase, override_settings + +from .models import DTModel + + +@override_settings(USE_TZ=True) +class TruncTests(TestCase): + melb = ZoneInfo("Australia/Melbourne") + + def test_truncdate_tzinfo(self): + msg = "TruncDate with tzinfo (Australia/Melbourne) isn't supported on MongoDB." + with self.assertRaisesMessage(NotSupportedError, msg): + DTModel.objects.annotate( + melb_date=TruncDate("start_datetime", tzinfo=self.melb), + ).get() + + def test_trunctime_tzinfo(self): + msg = "TruncTime with tzinfo (Australia/Melbourne) isn't supported on MongoDB." + with self.assertRaisesMessage(NotSupportedError, msg): + DTModel.objects.annotate( + melb_date=TruncTime("start_datetime", tzinfo=self.melb), + ).get()