From 3434dbd39d373df7193ad006b970c09c1a909ea3 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Wed, 2 Aug 2023 20:47:49 -0400 Subject: [PATCH] Fixed #34754 -- Fixed JSONField check constraints validation on NULL values. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The __isnull lookup of JSONField must special case Value(None, JSONField()) left-hand-side in order to be coherent with its convoluted null handling. Since psycopg>=3 offers no way to pass a NULL::jsonb the issue is resolved by optimizing IsNull(Value(None), True | False) to True | False. Regression in 5c23d9f0c32f166c81ecb6f3f01d5077a6084318. Thanks Alexandre Collet for the report. --- django/db/models/lookups.py | 5 +++++ docs/releases/4.2.5.txt | 4 +++- tests/constraints/models.py | 7 +++++++ tests/constraints/tests.py | 20 ++++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/django/db/models/lookups.py b/django/db/models/lookups.py index 91342a864af..8528fcda5ce 100644 --- a/django/db/models/lookups.py +++ b/django/db/models/lookups.py @@ -607,6 +607,11 @@ class IsNull(BuiltinLookup): raise ValueError( "The QuerySet value for an isnull lookup must be True or False." ) + if isinstance(self.lhs, Value) and self.lhs.value is None: + if self.rhs: + raise FullResultSet + else: + raise EmptyResultSet sql, params = self.process_lhs(compiler, connection) if self.rhs: return "%s IS NULL" % sql, params diff --git a/docs/releases/4.2.5.txt b/docs/releases/4.2.5.txt index a4e24711584..23ba728da56 100644 --- a/docs/releases/4.2.5.txt +++ b/docs/releases/4.2.5.txt @@ -9,4 +9,6 @@ Django 4.2.5 fixes several bugs in 4.2.4. Bugfixes ======== -* ... +* Fixed a regression in Django 4.2 that caused an incorrect validation of + ``CheckConstraints`` on ``__isnull`` lookups against ``JSONField`` + (:ticket:`34754`). diff --git a/tests/constraints/models.py b/tests/constraints/models.py index ab3d4dc1e0c..3ea5cf2323b 100644 --- a/tests/constraints/models.py +++ b/tests/constraints/models.py @@ -121,3 +121,10 @@ class AbstractModel(models.Model): class ChildModel(AbstractModel): pass + + +class JSONFieldModel(models.Model): + data = models.JSONField(null=True) + + class Meta: + required_db_features = {"supports_json_field"} diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index 7e3d20e40cc..f6571084b05 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -13,6 +13,7 @@ from django.utils.deprecation import RemovedInDjango60Warning from .models import ( ChildModel, ChildUniqueConstraintProduct, + JSONFieldModel, Product, UniqueConstraintConditionProduct, UniqueConstraintDeferrable, @@ -332,6 +333,25 @@ class CheckConstraintTests(TestCase): ) constraint.validate(Product, Product()) + @skipUnlessDBFeature("supports_json_field") + def test_validate_nullable_jsonfield(self): + is_null_constraint = models.CheckConstraint( + check=models.Q(data__isnull=True), + name="nullable_data", + ) + is_not_null_constraint = models.CheckConstraint( + check=models.Q(data__isnull=False), + name="nullable_data", + ) + is_null_constraint.validate(JSONFieldModel, JSONFieldModel(data=None)) + msg = f"Constraint “{is_null_constraint.name}” is violated." + with self.assertRaisesMessage(ValidationError, msg): + is_null_constraint.validate(JSONFieldModel, JSONFieldModel(data={})) + msg = f"Constraint “{is_not_null_constraint.name}” is violated." + with self.assertRaisesMessage(ValidationError, msg): + is_not_null_constraint.validate(JSONFieldModel, JSONFieldModel(data=None)) + is_not_null_constraint.validate(JSONFieldModel, JSONFieldModel(data={})) + class UniqueConstraintTests(TestCase): @classmethod