diff options
| author | Simon Charette <charette.s@gmail.com> | 2021-05-21 22:32:16 -0400 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2021-07-02 07:25:42 +0200 |
| commit | 9f3cce172f6913c5ac74272fa5fc07f847b4e112 (patch) | |
| tree | 027dec2a7a1bef8c02c2e58ffded8718401b28d1 /django | |
| parent | f3112fde981052801e0c2121027424496c03efdf (diff) | |
Refs #26430 -- Re-introduced empty aggregation optimization.
The introduction of the Expression.empty_aggregate_value interface
allows the compilation stage to enable the EmptyResultSet optimization
if all the aggregates expressions implement it.
This also removes unnecessary RegrCount/Count.convert_value() methods.
Disabling the empty result set aggregation optimization when it wasn't
appropriate prevented None returned for a Count aggregation value.
Thanks Nick Pope for the review.
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/postgres/aggregates/statistics.py | 4 | ||||
| -rw-r--r-- | django/db/models/aggregates.py | 5 | ||||
| -rw-r--r-- | django/db/models/expressions.py | 5 | ||||
| -rw-r--r-- | django/db/models/functions/comparison.py | 8 | ||||
| -rw-r--r-- | django/db/models/sql/query.py | 9 |
5 files changed, 24 insertions, 7 deletions
diff --git a/django/contrib/postgres/aggregates/statistics.py b/django/contrib/postgres/aggregates/statistics.py index c3254e3677..f3e1450d17 100644 --- a/django/contrib/postgres/aggregates/statistics.py +++ b/django/contrib/postgres/aggregates/statistics.py @@ -36,9 +36,7 @@ class RegrAvgY(StatAggregate): class RegrCount(StatAggregate): function = 'REGR_COUNT' output_field = IntegerField() - - def convert_value(self, value, expression, connection): - return 0 if value is None else value + empty_aggregate_value = 0 class RegrIntercept(StatAggregate): diff --git a/django/db/models/aggregates.py b/django/db/models/aggregates.py index 8b10829eb8..8598ba9178 100644 --- a/django/db/models/aggregates.py +++ b/django/db/models/aggregates.py @@ -20,6 +20,7 @@ class Aggregate(Func): filter_template = '%s FILTER (WHERE %%(filter)s)' window_compatible = True allow_distinct = False + empty_aggregate_value = None def __init__(self, *expressions, distinct=False, filter=None, **extra): if distinct and not self.allow_distinct: @@ -107,6 +108,7 @@ class Count(Aggregate): name = 'Count' output_field = IntegerField() allow_distinct = True + empty_aggregate_value = 0 def __init__(self, expression, filter=None, **extra): if expression == '*': @@ -115,9 +117,6 @@ class Count(Aggregate): raise ValueError('Star cannot be used with filter. Please specify a field.') super().__init__(expression, filter=filter, **extra) - def convert_value(self, value, expression, connection): - return 0 if value is None else value - class Max(Aggregate): function = 'MAX' diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index feb04d4585..b30d1f959b 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -153,6 +153,7 @@ class BaseExpression: # aggregate specific fields is_summary = False _output_field_resolved_to_none = False + empty_aggregate_value = NotImplemented # Can the expression be used in a WHERE clause? filterable = True # Can the expression can be used as a source expression in Window? @@ -795,6 +796,10 @@ class Value(SQLiteNumericMixin, Expression): if isinstance(self.value, UUID): return fields.UUIDField() + @property + def empty_aggregate_value(self): + return self.value + class RawSQL(Expression): def __init__(self, sql, params, output_field=None): diff --git a/django/db/models/functions/comparison.py b/django/db/models/functions/comparison.py index 8a1c34430b..2dac47a0d2 100644 --- a/django/db/models/functions/comparison.py +++ b/django/db/models/functions/comparison.py @@ -65,6 +65,14 @@ class Coalesce(Func): raise ValueError('Coalesce must take at least two expressions') super().__init__(*expressions, **extra) + @property + def empty_aggregate_value(self): + for expression in self.get_source_expressions(): + result = expression.empty_aggregate_value + if result is NotImplemented or result is not None: + return result + return None + def as_oracle(self, compiler, connection, **extra_context): # Oracle prohibits mixing TextField (NCLOB) and CharField (NVARCHAR2), # so convert all fields to NCLOB when that type is expected. diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index fabb346418..87be8ea9f0 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -490,12 +490,19 @@ class Query(BaseExpression): self.default_cols = False self.extra = {} + empty_aggregate_result = [ + expression.empty_aggregate_value + for expression in outer_query.annotation_select.values() + ] + elide_empty = not any(result is NotImplemented for result in empty_aggregate_result) outer_query.clear_ordering(force=True) outer_query.clear_limits() outer_query.select_for_update = False outer_query.select_related = False - compiler = outer_query.get_compiler(using, elide_empty=False) + compiler = outer_query.get_compiler(using, elide_empty=elide_empty) result = compiler.execute_sql(SINGLE) + if result is None: + result = empty_aggregate_result converters = compiler.get_converters(outer_query.annotation_select.values()) result = next(compiler.apply_converters((result,), converters)) |
