From 0f6d51e6a0a22e37e45c4bf452ddb49723e2f956 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 13 Oct 2015 20:25:06 +0200 Subject: [PATCH] Fixed #25470 -- Avoided unnecessary, expensive DATETIME typecast on MySQL. --- django/db/backends/mysql/operations.py | 18 ++++++-------- tests/dates/models.py | 2 ++ tests/dates/tests.py | 34 ++++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py index 85854dc1b6..8fc67ff8e3 100644 --- a/django/db/backends/mysql/operations.py +++ b/django/db/backends/mysql/operations.py @@ -27,17 +27,15 @@ class DatabaseOperations(BaseDatabaseOperations): return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) def date_trunc_sql(self, lookup_type, field_name): - fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] - format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. - format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') - try: - i = fields.index(lookup_type) + 1 - except ValueError: - sql = field_name + fields = { + 'year': '%%Y-01-01', + 'month': '%%Y-%%m-01', + } # Use double percents to escape. + if lookup_type in fields: + format_str = fields[lookup_type] + return "CAST(DATE_FORMAT(%s, '%s') AS DATE)" % (field_name, format_str) else: - format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) - sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) - return sql + return "DATE(%s)" % (field_name) def _convert_field_to_tz(self, field_name, tzname): if settings.USE_TZ: diff --git a/tests/dates/models.py b/tests/dates/models.py index 58e6d10d91..2161b2b356 100644 --- a/tests/dates/models.py +++ b/tests/dates/models.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from django.db import models +from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible @@ -8,6 +9,7 @@ from django.utils.encoding import python_2_unicode_compatible class Article(models.Model): title = models.CharField(max_length=100) pub_date = models.DateField() + pub_datetime = models.DateTimeField(default=timezone.now()) categories = models.ManyToManyField("Category", related_name="articles") diff --git a/tests/dates/tests.py b/tests/dates/tests.py index c0f399b091..3b6fb5505b 100644 --- a/tests/dates/tests.py +++ b/tests/dates/tests.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals import datetime +from unittest import skipUnless from django.core.exceptions import FieldError -from django.test import TestCase +from django.db import connection +from django.test import TestCase, override_settings from django.utils import six from .models import Article, Category, Comment @@ -95,7 +97,7 @@ class DatesTests(TestCase): self, FieldError, "Cannot resolve keyword u?'invalid_field' into field. Choices are: " - "categories, comments, id, pub_date, title", + "categories, comments, id, pub_date, pub_datetime, title", Article.objects.dates, "invalid_field", "year", @@ -121,3 +123,31 @@ class DatesTests(TestCase): "year", order="bad order", ) + + @override_settings(USE_TZ=False) + def test_dates_trunc_datetime_fields(self): + Article.objects.bulk_create( + Article(pub_date=pub_datetime.date(), pub_datetime=pub_datetime) + for pub_datetime in [ + datetime.datetime(2015, 10, 21, 18, 1), + datetime.datetime(2015, 10, 21, 18, 2), + datetime.datetime(2015, 10, 22, 18, 1), + datetime.datetime(2015, 10, 22, 18, 2), + ] + ) + self.assertQuerysetEqual( + Article.objects.dates('pub_datetime', 'day', order='ASC'), [ + "datetime.date(2015, 10, 21)", + "datetime.date(2015, 10, 22)", + ] + ) + + @skipUnless(connection.vendor == 'mysql', "Test checks MySQL query syntax") + def test_dates_avoid_datetime_cast(self): + Article.objects.create(pub_date=datetime.date(2015, 10, 21)) + for kind in ['day', 'month', 'year']: + qs = Article.objects.dates('pub_date', kind) + if kind == 'day': + self.assertIn('DATE(', str(qs.query)) + else: + self.assertIn(' AS DATE)', str(qs.query))