From 155b31d4ec138664d62665eb2d8a442469045b78 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 7 Aug 2018 18:08:39 +0200 Subject: [PATCH] Fixed #29648 -- Fixed crash when using subqueries inside datetime truncation functions. --- django/db/models/functions/datetime.py | 2 -- tests/db_functions/models.py | 1 + tests/db_functions/test_datetime.py | 36 ++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py index 74b88b77ca..1876aa7d5c 100644 --- a/django/db/models/functions/datetime.py +++ b/django/db/models/functions/datetime.py @@ -164,8 +164,6 @@ class TruncBase(TimezoneMixin, Transform): def as_sql(self, compiler, connection): inner_sql, inner_params = compiler.compile(self.lhs) - # Escape any params because trunc_sql will format the string. - inner_sql = inner_sql.replace('%s', '%%s') if isinstance(self.output_field, DateTimeField): tzname = self.get_tzname() sql = connection.ops.datetime_trunc_sql(self.kind, inner_sql, tzname) diff --git a/tests/db_functions/models.py b/tests/db_functions/models.py index c31de39b85..083655e80f 100644 --- a/tests/db_functions/models.py +++ b/tests/db_functions/models.py @@ -32,6 +32,7 @@ class Fan(models.Model): name = models.CharField(max_length=50) age = models.PositiveSmallIntegerField(default=30) author = models.ForeignKey(Author, models.CASCADE, related_name='fans') + fan_since = models.DateTimeField(null=True, blank=True) def __str__(self): return self.name diff --git a/tests/db_functions/test_datetime.py b/tests/db_functions/test_datetime.py index dc4c911ab9..07b6b0438a 100644 --- a/tests/db_functions/test_datetime.py +++ b/tests/db_functions/test_datetime.py @@ -3,7 +3,9 @@ from datetime import datetime, timedelta import pytz from django.conf import settings -from django.db.models import DateField, DateTimeField, IntegerField, TimeField +from django.db.models import ( + DateField, DateTimeField, IntegerField, Max, OuterRef, Subquery, TimeField, +) from django.db.models.functions import ( Extract, ExtractDay, ExtractHour, ExtractMinute, ExtractMonth, ExtractQuarter, ExtractSecond, ExtractWeek, ExtractWeekDay, ExtractYear, @@ -15,7 +17,7 @@ from django.test import ( ) from django.utils import timezone -from .models import DTModel +from .models import Author, DTModel, Fan def truncate_to(value, kind, tzinfo=None): @@ -854,6 +856,36 @@ class DateFunctionTests(TestCase): with self.assertRaisesMessage(ValueError, "Cannot truncate DateField 'start_date' to DateTimeField"): list(DTModel.objects.annotate(truncated=TruncSecond('start_date', output_field=DateField()))) + def test_trunc_subquery_with_parameters(self): + author_1 = Author.objects.create(name='J. R. R. Tolkien') + author_2 = Author.objects.create(name='G. R. R. Martin') + fan_since_1 = datetime(2016, 2, 3, 15, 0, 0) + fan_since_2 = datetime(2015, 2, 3, 15, 0, 0) + fan_since_3 = datetime(2017, 2, 3, 15, 0, 0) + if settings.USE_TZ: + fan_since_1 = timezone.make_aware(fan_since_1, is_dst=False) + fan_since_2 = timezone.make_aware(fan_since_2, is_dst=False) + fan_since_3 = timezone.make_aware(fan_since_3, is_dst=False) + Fan.objects.create(author=author_1, name='Tom', fan_since=fan_since_1) + Fan.objects.create(author=author_1, name='Emma', fan_since=fan_since_2) + Fan.objects.create(author=author_2, name='Isabella', fan_since=fan_since_3) + + inner = Fan.objects.filter( + author=OuterRef('pk'), + name__in=('Emma', 'Isabella', 'Tom') + ).values('author').annotate(newest_fan=Max('fan_since')).values('newest_fan') + outer = Author.objects.annotate( + newest_fan_year=TruncYear(Subquery(inner, output_field=DateTimeField())) + ) + tz = pytz.UTC if settings.USE_TZ else None + self.assertSequenceEqual( + outer.order_by('name').values('name', 'newest_fan_year'), + [ + {'name': 'G. R. R. Martin', 'newest_fan_year': datetime(2017, 1, 1, 0, 0, tzinfo=tz)}, + {'name': 'J. R. R. Tolkien', 'newest_fan_year': datetime(2016, 1, 1, 0, 0, tzinfo=tz)}, + ] + ) + @override_settings(USE_TZ=True, TIME_ZONE='UTC') class DateFunctionWithTimeZoneTests(DateFunctionTests):