diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py index 9787901b07..7a71c76bd3 100644 --- a/django/db/backends/base/features.py +++ b/django/db/backends/base/features.py @@ -1,5 +1,4 @@ -from django.db.models.aggregates import StdDev -from django.db.utils import NotSupportedError, ProgrammingError +from django.db.utils import ProgrammingError from django.utils.functional import cached_property @@ -298,12 +297,3 @@ class BaseDatabaseFeatures: count, = cursor.fetchone() cursor.execute('DROP TABLE ROLLBACK_TEST') return count == 0 - - @cached_property - def supports_stddev(self): - """Confirm support for STDDEV and related stats functions.""" - try: - self.connection.ops.check_expression_support(StdDev(1)) - except NotSupportedError: - return False - return True diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 1c39fb4add..5b1c80d27c 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -7,6 +7,7 @@ import functools import math import operator import re +import statistics import warnings from itertools import chain from sqlite3 import dbapi2 as Database @@ -49,6 +50,14 @@ def none_guard(func): return wrapper +def list_aggregate(function): + """ + Return an aggregate class that accumulates values in a list and applies + the provided function to the data. + """ + return type('ListAggregate', (list,), {'finalize': function, 'step': list.append}) + + Database.register_converter("bool", b'1'.__eq__) Database.register_converter("time", decoder(parse_time)) Database.register_converter("datetime", decoder(parse_datetime)) @@ -210,6 +219,10 @@ class DatabaseWrapper(BaseDatabaseWrapper): conn.create_function('SIN', 1, none_guard(math.sin)) conn.create_function('SQRT', 1, none_guard(math.sqrt)) conn.create_function('TAN', 1, none_guard(math.tan)) + conn.create_aggregate('STDDEV_POP', 1, list_aggregate(statistics.pstdev)) + conn.create_aggregate('STDDEV_SAMP', 1, list_aggregate(statistics.stdev)) + conn.create_aggregate('VAR_POP', 1, list_aggregate(statistics.pvariance)) + conn.create_aggregate('VAR_SAMP', 1, list_aggregate(statistics.variance)) conn.execute('PRAGMA foreign_keys = ON') return conn diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index d76a1275f5..c57b8e1934 100644 --- a/django/db/backends/sqlite3/features.py +++ b/django/db/backends/sqlite3/features.py @@ -1,8 +1,6 @@ import sys -from django.db import utils from django.db.backends.base.features import BaseDatabaseFeatures -from django.utils.functional import cached_property from .base import Database @@ -41,22 +39,3 @@ class DatabaseFeatures(BaseDatabaseFeatures): # reasonably performant way. supports_pragma_foreign_key_check = Database.sqlite_version_info >= (3, 20, 0) can_defer_constraint_checks = supports_pragma_foreign_key_check - - @cached_property - def supports_stddev(self): - """ - Confirm support for STDDEV and related stats functions. - - SQLite supports STDDEV as an extension package; so - connection.ops.check_expression_support() can't unilaterally - rule out support for STDDEV. Manually check whether the call works. - """ - with self.connection.cursor() as cursor: - cursor.execute('CREATE TABLE STDDEV_TEST (X INT)') - try: - cursor.execute('SELECT STDDEV(*) FROM STDDEV_TEST') - has_support = True - except utils.DatabaseError: - has_support = False - cursor.execute('DROP TABLE STDDEV_TEST') - return has_support diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 81a82fe7b6..9dd661e6a7 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -3400,12 +3400,9 @@ by the aggregate. By default, ``StdDev`` returns the population standard deviation. However, if ``sample=True``, the return value will be the sample standard deviation. - .. admonition:: SQLite + .. versionchanged:: 2.2 - SQLite doesn't provide ``StdDev`` out of the box. An implementation - is available as an extension module for SQLite. Consult the `SQLite - documentation`_ for instructions on obtaining and installing this - extension. + SQLite support was added. ``Sum`` ~~~~~~~ @@ -3434,14 +3431,9 @@ by the aggregate. By default, ``Variance`` returns the population variance. However, if ``sample=True``, the return value will be the sample variance. - .. admonition:: SQLite + .. versionchanged:: 2.2 - SQLite doesn't provide ``Variance`` out of the box. An implementation - is available as an extension module for SQLite. Consult the `SQLite - documentation`_ for instructions on obtaining and installing this - extension. - -.. _SQLite documentation: https://www.sqlite.org/contrib + SQLite support was added. Query-related tools =================== diff --git a/docs/releases/2.2.txt b/docs/releases/2.2.txt index f2e822f5a5..f731562640 100644 --- a/docs/releases/2.2.txt +++ b/docs/releases/2.2.txt @@ -235,6 +235,9 @@ Models ``Model.delete()``. This improves the performance of autocommit by reducing the number of database round trips. +* Added SQLite support for the :class:`~django.db.models.StdDev` and + :class:`~django.db.models.Variance` functions. + Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/aggregation_regress/tests.py b/tests/aggregation_regress/tests.py index 893b22ae69..29b32c4987 100644 --- a/tests/aggregation_regress/tests.py +++ b/tests/aggregation_regress/tests.py @@ -1116,7 +1116,6 @@ class AggregationTests(TestCase): lambda b: (b.name, b.authorCount) ) - @skipUnlessDBFeature('supports_stddev') def test_stddev(self): self.assertEqual( Book.objects.aggregate(StdDev('pages')), diff --git a/tests/backends/tests.py b/tests/backends/tests.py index ee6bee3a02..d1b89950c0 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -340,7 +340,6 @@ class BackendTestCase(TransactionTestCase): def test_cached_db_features(self): self.assertIn(connection.features.supports_transactions, (True, False)) - self.assertIn(connection.features.supports_stddev, (True, False)) self.assertIn(connection.features.can_introspect_foreign_keys, (True, False)) def test_duplicate_table_error(self):