diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index 9344979c56..4b41906966 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -1,5 +1,6 @@ import itertools import math +import warnings from copy import copy from django.core.exceptions import EmptyResultSet @@ -9,6 +10,7 @@ from django.db.models.fields import ( ) from django.db.models.query_utils import RegisterLookupMixin from django.utils.datastructures import OrderedSet +from django.utils.deprecation import RemovedInDjango40Warning from django.utils.functional import cached_property @@ -463,6 +465,17 @@ class IsNull(BuiltinLookup): prepare_rhs = False def as_sql(self, compiler, connection): + if not isinstance(self.rhs, bool): + # When the deprecation ends, replace with: + # raise ValueError( + # 'The QuerySet value for an isnull lookup must be True or ' + # 'False.' + # ) + warnings.warn( + 'Using a non-boolean value for an isnull lookup is ' + 'deprecated, use True or False instead.', + RemovedInDjango40Warning, + ) sql, params = compiler.compile(self.lhs) if self.rhs: return "%s IS NULL" % sql, params diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index d79641a19c..b2a21b12a9 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -36,6 +36,9 @@ details on these changes. * The ``PASSWORD_RESET_TIMEOUT_DAYS`` setting will be removed. +* The undocumented usage of the :lookup:`isnull` lookup with non-boolean values + as the right-hand side will no longer be allowed. + See the :ref:`Django 3.1 release notes ` for more details on these changes. diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 219245576a..ef489b8467 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -3281,6 +3281,11 @@ SQL equivalent: SELECT ... WHERE pub_date IS NULL; +.. deprecated:: 3.1 + + Using non-boolean values as the right-hand side is deprecated, use ``True`` + or ``False`` instead. In Django 4.0, the exception will be raised. + .. fieldlookup:: regex ``regex`` diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt index a8aaa8e4f9..656649f914 100644 --- a/docs/releases/3.1.txt +++ b/docs/releases/3.1.txt @@ -274,6 +274,9 @@ Miscellaneous * ``PASSWORD_RESET_TIMEOUT_DAYS`` setting is deprecated in favor of :setting:`PASSWORD_RESET_TIMEOUT`. +* The undocumented usage of the :lookup:`isnull` lookup with non-boolean values + as the right-hand side is deprecated, use ``True`` or ``False`` instead. + .. _removed-features-3.1: Features removed in 3.1 diff --git a/tests/lookup/models.py b/tests/lookup/models.py index 8c8cb67827..fbc9fa606f 100644 --- a/tests/lookup/models.py +++ b/tests/lookup/models.py @@ -96,3 +96,15 @@ class Product(models.Model): class Stock(models.Model): product = models.ForeignKey(Product, models.CASCADE) qty_available = models.DecimalField(max_digits=6, decimal_places=2) + + +class Freebie(models.Model): + gift_product = models.ForeignKey(Product, models.CASCADE) + stock_id = models.IntegerField(blank=True, null=True) + + stock = models.ForeignObject( + Stock, + from_fields=['stock_id', 'gift_product'], + to_fields=['id', 'product'], + on_delete=models.CASCADE, + ) diff --git a/tests/lookup/tests.py b/tests/lookup/tests.py index 1958b995b2..e6334fece1 100644 --- a/tests/lookup/tests.py +++ b/tests/lookup/tests.py @@ -9,9 +9,10 @@ from django.db.models import Max from django.db.models.expressions import Exists, OuterRef from django.db.models.functions import Substr from django.test import TestCase, skipUnlessDBFeature +from django.utils.deprecation import RemovedInDjango40Warning from .models import ( - Article, Author, Game, IsNullWithNoneAsRHS, Player, Season, Tag, + Article, Author, Freebie, Game, IsNullWithNoneAsRHS, Player, Season, Tag, ) @@ -969,3 +970,24 @@ class LookupTests(TestCase): ).values('max_id') authors = Author.objects.filter(id=authors_max_ids[:1]) self.assertEqual(authors.get(), newest_author) + + def test_isnull_non_boolean_value(self): + # These tests will catch ValueError in Django 4.0 when using + # non-boolean values for an isnull lookup becomes forbidden. + # msg = ( + # 'The QuerySet value for an isnull lookup must be True or False.' + # ) + msg = ( + 'Using a non-boolean value for an isnull lookup is deprecated, ' + 'use True or False instead.' + ) + tests = [ + Author.objects.filter(alias__isnull=1), + Article.objects.filter(author__isnull=1), + Season.objects.filter(games__isnull=1), + Freebie.objects.filter(stock__isnull=1), + ] + for qs in tests: + with self.subTest(qs=qs): + with self.assertWarnsMessage(RemovedInDjango40Warning, msg): + qs.exists()