Repository: django/django
File: django/db/models/fields/json.py (Python)

Code before:
    Mixin to allow case-insensitive comparison of JSON values on MySQL.
    MySQL handles strings used in JSON context using the utf8mb4_bin collation.
    Because utf8mb4_bin is a binary collation, comparison of JSON values is
    case-sensitive.
    """

    def process_lhs(self, compiler, connection):
        lhs, lhs_params = super().process_lhs(compiler, connection)
        if connection.vendor == "mysql":
            return "LOWER(%s)" % lhs, lhs_params
        return lhs, lhs_params

    def process_rhs(self, compiler, connection):
        rhs, rhs_params = super().process_rhs(compiler, connection)
        if connection.vendor == "mysql":
            return "LOWER(%s)" % rhs, rhs_params
        return rhs, rhs_params


class JSONExact(lookups.Exact):
    # RemovedInDjango70Warning: When the deprecation period is over, remove
    # the following line.
    can_use_none_as_rhs = True

    def process_rhs(self, compiler, connection):
        if self.rhs is None and isinstance(self.lhs, expressions.Col):
            warnings.warn(
                "Using None as the right-hand side of an exact lookup on JSONField "
                "to mean JSON scalar 'null' is deprecated. Use JSONNull() instead.",
                RemovedInDjango70Warning,
                stacklevel=2,
            )

        rhs, rhs_params = super().process_rhs(compiler, connection)

        # RemovedInDjango70Warning: When the deprecation period is over, remove
        # The following if-block entirely.
        # Treat None lookup values as null.
        if rhs == "%s" and rhs_params == [None]:
            rhs_params = ["null"]
        if connection.vendor == "mysql" and not isinstance(self.rhs, JSONNull):
            func = ["JSON_EXTRACT(%s, '$')"] * len(rhs_params)
            rhs %= tuple(func)
        return rhs, rhs_params

    def as_oracle(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        if connection.features.supports_primitives_in_json_field:
            lhs = f"JSON({lhs})"
            rhs = f"JSON({rhs})"

Review comment (bug):
There might be some edge cases here to handle besides just `Col`[0].

This test unexpectedly passes for me:

```diff
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index d9102c5e65..aeb4d1b9a0 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -410,6 +410,8 @@ class JSONModel(models.Model):
         required_db_features = {"supports_json_field"}
 
 
+# TODO: move this test & model to contrib.postgres
+from django.contrib.postgres.fields import ArrayField
 class NullableJSONModel(models.Model):
     value = models.JSONField(blank=True, null=True)
     value_custom = models.JSONField(
@@ -417,6 +419,7 @@ class NullableJSONModel(models.Model):
         decoder=CustomJSONDecoder,
         null=True,
     )
+    array = ArrayField(models.JSONField(), size=2, default=list)
 
     class Meta:
         required_db_features = {"supports_json_field"}
diff --git a/tests/model_fields/test_jsonfield.py b/tests/model_fields/test_jsonfield.py
index deeb71fe14..593d670d4e 100644
--- a/tests/model_fields/test_jsonfield.py
+++ b/tests/model_fields/test_jsonfield.py
@@ -1291,6 +1291,14 @@ class JSONNullTests(TestCase):
             NullableJSONModel.objects.filter(value__isnull=True), []
         )
 
+    def test_filter_array_field(self):
+        obj = NullableJSONModel.objects.create(array=[None, None])
+        self.assertSequenceEqual(
+            NullableJSONModel.objects.filter(array__0=None),
+            [obj],  # checking scalar null, should have raised warning
+            # in Django 7 will return [], e.g. SQL NULL
+        )
+
     def test_case_expression(self):
         with self.subTest("JSONNull() in When.then"):
             obj = NullableJSONModel.objects.create(value={"key": "value"})
```
***
I would expect a DeprecationWarning to be emitted and prevent this test from passing (because when I time travel to Django 7, the test returns `[]`, indicating a behavior change without a warning).

When I comment out the `and isinstance(self.lhs, expressions.Col):` I get a warning as expected.

[0] either that, or this really is a special case for ArrayField deserving special handling _there._

Code after:
    Mixin to allow case-insensitive comparison of JSON values on MySQL.
    MySQL handles strings used in JSON context using the utf8mb4_bin collation.
    Because utf8mb4_bin is a binary collation, comparison of JSON values is
    case-sensitive.
    """

    def process_lhs(self, compiler, connection):
        lhs, lhs_params = super().process_lhs(compiler, connection)
        if connection.vendor == "mysql":
            return "LOWER(%s)" % lhs, lhs_params
        return lhs, lhs_params

    def process_rhs(self, compiler, connection):
        rhs, rhs_params = super().process_rhs(compiler, connection)
        if connection.vendor == "mysql":
            return "LOWER(%s)" % rhs, rhs_params
        return rhs, rhs_params


class JSONExact(lookups.Exact):
    # RemovedInDjango70Warning: When the deprecation period is over, remove
    # the following line.
    can_use_none_as_rhs = True

    def process_rhs(self, compiler, connection):
        if self.rhs is None and not isinstance(self.lhs, KeyTransform):
            warnings.warn(
                "Using None as the right-hand side of an exact lookup on JSONField to "
                "mean JSON scalar 'null' is deprecated. Use JSONNull() instead (or use "
                "the __isnull lookup if you meant SQL NULL).",
                RemovedInDjango70Warning,
                skip_file_prefixes=django_file_prefixes(),
            )

        rhs, rhs_params = super().process_rhs(compiler, connection)

        # RemovedInDjango70Warning: When the deprecation period is over, remove
        # The following if-block entirely.
        # Treat None lookup values as null.
        if rhs == "%s" and (*rhs_params,) == (None,):
            rhs_params = ("null",)
        if connection.vendor == "mysql" and not isinstance(self.rhs, JSONNull):
            func = ["JSON_EXTRACT(%s, '$')"] * len(rhs_params)
            rhs %= tuple(func)
        return rhs, rhs_params

    def as_oracle(self, compiler, connection):
        lhs, lhs_params = self.process_lhs(compiler, connection)
        rhs, rhs_params = self.process_rhs(compiler, connection)
        if connection.features.supports_primitives_in_json_field:
            lhs = f"JSON({lhs})"