From 3434dbd39d373df7193ad006b970c09c1a909ea3 Mon Sep 17 00:00:00 2001
From: Simon Charette <charette.s@gmail.com>
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 91342a864a..8528fcda5c 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 a4e2471158..23ba728da5 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 ab3d4dc1e0..3ea5cf2323 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 7e3d20e40c..f6571084b0 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