From b2a0978610413e4cd5ebb716b8bfa7803dff8d5b Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Wed, 29 Sep 2021 00:00:50 +0200 Subject: [PATCH] [4.0.x] Fixed #33018 -- Fixed annotations with empty queryset. Thanks Simon Charette for the review and implementation idea. Backport of dd1fa3a31b4680c0d3712e6ae122b878138580c7 from main --- django/db/models/expressions.py | 9 ++++++++- django/db/models/sql/compiler.py | 8 ++++++-- django/db/models/sql/query.py | 1 + tests/annotations/tests.py | 6 ++++++ tests/db_functions/comparison/test_coalesce.py | 13 ++++++++++++- 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 d1c0c333f5..6436b25bae 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)