diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 1fcc222c80b..f4fd1cc379a 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -18,6 +18,8 @@ from django.db.backends.signals import connection_created from django.db.backends.sqlite3.client import DatabaseClient from django.db.backends.sqlite3.creation import DatabaseCreation from django.db.backends.sqlite3.introspection import DatabaseIntrospection +from django.db.models import fields +from django.db.models.sql import aggregates from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.functional import cached_property from django.utils.safestring import SafeBytes @@ -127,6 +129,17 @@ class DatabaseOperations(BaseDatabaseOperations): limit = 999 if len(fields) > 1 else 500 return (limit // len(fields)) if len(fields) > 0 else len(objs) + def check_aggregate_support(self, aggregate): + bad_fields = (fields.DateField, fields.DateTimeField, fields.TimeField) + bad_aggregates = (aggregates.Sum, aggregates.Avg, + aggregates.Variance, aggregates.StdDev) + if (isinstance(aggregate.source, bad_fields) and + isinstance(aggregate, bad_aggregates)): + raise NotImplementedError( + 'You cannot use Sum, Avg, StdDev and Variance aggregations ' + 'on date/time fields in sqlite3 ' + 'since date/time is saved as text.') + def date_extract_sql(self, lookup_type, field_name): # sqlite doesn't support extract, so we fake it with the user-defined # function django_extract that's registered in connect(). Note that diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 2bbd895fd49..71049703c90 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -2188,6 +2188,14 @@ Django provides the following aggregation functions in the aggregate functions, see :doc:`the topic guide on aggregation `. +.. warning:: + + SQLite can't handle aggregation on date/time fields out of the box. + This is because there are no native date/time fields in SQLite and Django + currently emulates these features using a text field. Attempts to use + aggregation on date/time fields in SQLite will raise + ``NotImplementedError``. + Avg ~~~ diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py index 344cf4c798d..a92aa71e174 100644 --- a/tests/regressiontests/backends/models.py +++ b/tests/regressiontests/backends/models.py @@ -75,3 +75,14 @@ class Article(models.Model): def __str__(self): return self.headline + + +@python_2_unicode_compatible +class Item(models.Model): + name = models.CharField(max_length=30) + date = models.DateField() + time = models.TimeField() + last_modified = models.DateTimeField() + + def __str__(self): + return self.name diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index 791f4c1daa0..b29384739d8 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -12,6 +12,7 @@ from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS, IntegrityError, transaction) from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version +from django.db.models import fields, Sum, Avg, Variance, StdDev from django.db.utils import ConnectionHandler, DatabaseError, load_backend from django.test import (TestCase, skipUnlessDBFeature, skipIfDBFeature, TransactionTestCase) @@ -362,6 +363,22 @@ class EscapingChecks(TestCase): self.assertTrue(int(response)) +class SqlliteAggregationTests(TestCase): + """ + #19360: Raise NotImplementedError when aggregating on date/time fields. + """ + @unittest.skipUnless(connection.vendor == 'sqlite', + "No need to check SQLite aggregation semantics") + def test_aggregation(self): + for aggregate in (Sum, Avg, Variance, StdDev): + self.assertRaises(NotImplementedError, + models.Item.objects.all().aggregate, aggregate('time')) + self.assertRaises(NotImplementedError, + models.Item.objects.all().aggregate, aggregate('date')) + self.assertRaises(NotImplementedError, + models.Item.objects.all().aggregate, aggregate('last_modified')) + + class BackendTestCase(TestCase): def create_squares_with_executemany(self, args): @@ -400,7 +417,6 @@ class BackendTestCase(TestCase): self.create_squares_with_executemany(args) self.assertEqual(models.Square.objects.count(), 9) - def test_unicode_fetches(self): #6254: fetchone, fetchmany, fetchall return strings as unicode objects qn = connection.ops.quote_name