Fixed #33018 -- Fixed annotations with empty queryset.

Thanks Simon Charette for the review and implementation idea.
This commit is contained in:
David Wobrock 2021-09-29 00:00:50 +02:00 committed by Mariusz Felisiak
parent ad36a198a1
commit dd1fa3a31b
5 changed files with 33 additions and 4 deletions

View File

@ -702,7 +702,13 @@ class Func(SQLiteNumericMixin, Expression):
sql_parts = [] sql_parts = []
params = [] params = []
for arg in self.source_expressions: for arg in self.source_expressions:
try:
arg_sql, arg_params = compiler.compile(arg) 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) sql_parts.append(arg_sql)
params.extend(arg_params) params.extend(arg_params)
data = {**self.extra, **extra_context} data = {**self.extra, **extra_context}
@ -1114,6 +1120,7 @@ class Subquery(BaseExpression, Combinable):
""" """
template = '(%(subquery)s)' template = '(%(subquery)s)'
contains_aggregate = False contains_aggregate = False
empty_result_set_value = None
def __init__(self, queryset, output_field=None, **extra): def __init__(self, queryset, output_field=None, **extra):
# Allow the usage of both QuerySet and sql.Query objects. # Allow the usage of both QuerySet and sql.Query objects.

View File

@ -266,8 +266,12 @@ class SQLCompiler:
try: try:
sql, params = self.compile(col) sql, params = self.compile(col)
except EmptyResultSet: except EmptyResultSet:
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. # Select a predicate that's always False.
sql, params = '0', () sql, params = '0', ()
else:
sql, params = self.compile(Value(empty_result_set_value))
else: else:
sql, params = col.select_format(self, sql, params) sql, params = col.select_format(self, sql, params)
ret.append((col, (sql, params), alias)) ret.append((col, (sql, params), alias))

View File

@ -143,6 +143,7 @@ class Query(BaseExpression):
"""A single SQL query.""" """A single SQL query."""
alias_prefix = 'T' alias_prefix = 'T'
empty_result_set_value = None
subq_aliases = frozenset([alias_prefix]) subq_aliases = frozenset([alias_prefix])
compiler = 'SQLCompiler' compiler = 'SQLCompiler'

View File

@ -210,6 +210,12 @@ class NonAggregateAnnotationTestCase(TestCase):
self.assertEqual(len(books), Book.objects.count()) self.assertEqual(len(books), Book.objects.count())
self.assertTrue(all(not book.selected for book in books)) 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): def test_annotate_with_aggregation(self):
books = Book.objects.annotate(is_book=Value(1), rating_count=Count('rating')) books = Book.objects.annotate(is_book=Value(1), rating_count=Count('rating'))
for book in books: for book in books:

View File

@ -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.db.models.functions import Coalesce, Lower
from django.test import TestCase from django.test import TestCase
from django.utils import timezone from django.utils import timezone
@ -70,3 +70,14 @@ class CoalesceTests(TestCase):
authors, ['John Smith', 'Rhonda'], authors, ['John Smith', 'Rhonda'],
lambda a: a.name 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)