summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Wobrock <david.wobrock@gmail.com>2021-09-29 00:00:50 +0200
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-09-29 20:23:29 +0200
commitdd1fa3a31b4680c0d3712e6ae122b878138580c7 (patch)
tree10205352e74cd3708581c7c262d25840be6aa12a
parentad36a198a12df4dff65992191b3eb0a474e2daac (diff)
Fixed #33018 -- Fixed annotations with empty queryset.
Thanks Simon Charette for the review and implementation idea.
-rw-r--r--django/db/models/expressions.py9
-rw-r--r--django/db/models/sql/compiler.py8
-rw-r--r--django/db/models/sql/query.py1
-rw-r--r--tests/annotations/tests.py6
-rw-r--r--tests/db_functions/comparison/test_coalesce.py13
5 files changed, 33 insertions, 4 deletions
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 100da26ee6..a3317bc28c 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -702,7 +702,13 @@ class Func(SQLiteNumericMixin, Expression):
sql_parts = []
params = []
for arg in self.source_expressions:
- arg_sql, arg_params = compiler.compile(arg)
+ try:
+ arg_sql, arg_params = compiler.compile(arg)
+ except EmptyResultSet:
+ empty_result_set_value = getattr(arg, 'empty_result_set_value', NotImplemented)
+ if empty_result_set_value is NotImplemented:
+ raise
+ arg_sql, arg_params = compiler.compile(Value(empty_result_set_value))
sql_parts.append(arg_sql)
params.extend(arg_params)
data = {**self.extra, **extra_context}
@@ -1114,6 +1120,7 @@ class Subquery(BaseExpression, Combinable):
"""
template = '(%(subquery)s)'
contains_aggregate = False
+ empty_result_set_value = None
def __init__(self, queryset, output_field=None, **extra):
# Allow the usage of both QuerySet and sql.Query objects.
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index 97288c83d7..d1009847e7 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -266,8 +266,12 @@ class SQLCompiler:
try:
sql, params = self.compile(col)
except EmptyResultSet:
- # Select a predicate that's always False.
- sql, params = '0', ()
+ empty_result_set_value = getattr(col, 'empty_result_set_value', NotImplemented)
+ if empty_result_set_value is NotImplemented:
+ # Select a predicate that's always False.
+ sql, params = '0', ()
+ else:
+ sql, params = self.compile(Value(empty_result_set_value))
else:
sql, params = col.select_format(self, sql, params)
ret.append((col, (sql, params), alias))
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index f772f7048b..b02d19071b 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -143,6 +143,7 @@ class Query(BaseExpression):
"""A single SQL query."""
alias_prefix = 'T'
+ empty_result_set_value = None
subq_aliases = frozenset([alias_prefix])
compiler = 'SQLCompiler'
diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py
index c0fe308a07..62912ee99c 100644
--- a/tests/annotations/tests.py
+++ b/tests/annotations/tests.py
@@ -210,6 +210,12 @@ class NonAggregateAnnotationTestCase(TestCase):
self.assertEqual(len(books), Book.objects.count())
self.assertTrue(all(not book.selected for book in books))
+ def test_empty_queryset_annotation(self):
+ qs = Author.objects.annotate(
+ empty=Subquery(Author.objects.values('id').none())
+ )
+ self.assertIsNone(qs.first().empty)
+
def test_annotate_with_aggregation(self):
books = Book.objects.annotate(is_book=Value(1), rating_count=Count('rating'))
for book in books:
diff --git a/tests/db_functions/comparison/test_coalesce.py b/tests/db_functions/comparison/test_coalesce.py
index 8ba4b01fe6..1093079d68 100644
--- a/tests/db_functions/comparison/test_coalesce.py
+++ b/tests/db_functions/comparison/test_coalesce.py
@@ -1,4 +1,4 @@
-from django.db.models import TextField
+from django.db.models import Subquery, TextField
from django.db.models.functions import Coalesce, Lower
from django.test import TestCase
from django.utils import timezone
@@ -70,3 +70,14 @@ class CoalesceTests(TestCase):
authors, ['John Smith', 'Rhonda'],
lambda a: a.name
)
+
+ def test_empty_queryset(self):
+ Author.objects.create(name='John Smith')
+ tests = [
+ Author.objects.none(),
+ Subquery(Author.objects.none()),
+ ]
+ for empty_query in tests:
+ with self.subTest(empty_query.__class__.__name__):
+ qs = Author.objects.annotate(annotation=Coalesce(empty_query, 42))
+ self.assertEqual(qs.first().annotation, 42)