From 63d239db037f02d98b7771c90422840bbb4a319a Mon Sep 17 00:00:00 2001
From: Hasan Ramezani <hasan.r67@gmail.com>
Date: Fri, 5 Feb 2021 12:20:38 +0100
Subject: [PATCH] Fixed #32411 -- Fixed __icontains lookup for JSONField on
 MySQL.

---
 django/db/models/fields/json.py      | 45 +++++++++++++++-------------
 tests/model_fields/test_jsonfield.py |  6 ++++
 2 files changed, 31 insertions(+), 20 deletions(-)

diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py
index 1b0601018e..bd12bba7ac 100644
--- a/django/db/models/fields/json.py
+++ b/django/db/models/fields/json.py
@@ -237,6 +237,26 @@ class HasAnyKeys(HasKeys):
     logical_operator = ' OR '
 
 
+class CaseInsensitiveMixin:
+    """
+    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):
     can_use_none_as_rhs = True
 
@@ -260,12 +280,17 @@ class JSONExact(lookups.Exact):
         return rhs, rhs_params
 
 
+class JSONIContains(CaseInsensitiveMixin, lookups.IContains):
+    pass
+
+
 JSONField.register_lookup(DataContains)
 JSONField.register_lookup(ContainedBy)
 JSONField.register_lookup(HasKey)
 JSONField.register_lookup(HasKeys)
 JSONField.register_lookup(HasAnyKeys)
 JSONField.register_lookup(JSONExact)
+JSONField.register_lookup(JSONIContains)
 
 
 class KeyTransform(Transform):
@@ -343,26 +368,6 @@ class KeyTransformTextLookupMixin:
         super().__init__(key_text_transform, *args, **kwargs)
 
 
-class CaseInsensitiveMixin:
-    """
-    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 KeyTransformIsNull(lookups.IsNull):
     # key__isnull=False is the same as has_key='key'
     def as_oracle(self, compiler, connection):
diff --git a/tests/model_fields/test_jsonfield.py b/tests/model_fields/test_jsonfield.py
index c6b2f85e1e..89b78de708 100644
--- a/tests/model_fields/test_jsonfield.py
+++ b/tests/model_fields/test_jsonfield.py
@@ -313,6 +313,12 @@ class TestQuerying(TestCase):
             [self.objs[3]],
         )
 
+    def test_icontains(self):
+        self.assertSequenceEqual(
+            NullableJSONModel.objects.filter(value__icontains='BaX'),
+            self.objs[6:8],
+        )
+
     def test_isnull(self):
         self.assertSequenceEqual(
             NullableJSONModel.objects.filter(value__isnull=True),