Browse Source

Fixed #31829 -- Used JSONField __contains lookup on key transforms.

sage 4 years ago
parent
commit
2d8dcba03a
3 changed files with 33 additions and 14 deletions
  1. 0 5
      django/db/models/fields/json.py
  2. 8 6
      docs/topics/db/queries.txt
  3. 25 3
      tests/model_fields/test_jsonfield.py

+ 0 - 5
django/db/models/fields/json.py

@@ -431,10 +431,6 @@ class KeyTransformIContains(CaseInsensitiveMixin, KeyTransformTextLookupMixin, l
     pass
 
 
-class KeyTransformContains(KeyTransformTextLookupMixin, lookups.Contains):
-    pass
-
-
 class KeyTransformStartsWith(KeyTransformTextLookupMixin, lookups.StartsWith):
     pass
 
@@ -486,7 +482,6 @@ class KeyTransformGte(KeyTransformNumericLookupMixin, lookups.GreaterThanOrEqual
 KeyTransform.register_lookup(KeyTransformExact)
 KeyTransform.register_lookup(KeyTransformIExact)
 KeyTransform.register_lookup(KeyTransformIsNull)
-KeyTransform.register_lookup(KeyTransformContains)
 KeyTransform.register_lookup(KeyTransformIContains)
 KeyTransform.register_lookup(KeyTransformStartsWith)
 KeyTransform.register_lookup(KeyTransformIStartsWith)

+ 8 - 6
docs/topics/db/queries.txt

@@ -905,10 +905,10 @@ To query for missing keys, use the ``isnull`` lookup::
 
     The lookup examples given above implicitly use the :lookup:`exact` lookup.
     Key, index, and path transforms can also be chained with:
-    :lookup:`contains`, :lookup:`icontains`, :lookup:`endswith`,
-    :lookup:`iendswith`, :lookup:`iexact`, :lookup:`regex`, :lookup:`iregex`,
-    :lookup:`startswith`, :lookup:`istartswith`, :lookup:`lt`, :lookup:`lte`,
-    :lookup:`gt`, and :lookup:`gte` lookups.
+    :lookup:`icontains`, :lookup:`endswith`, :lookup:`iendswith`,
+    :lookup:`iexact`, :lookup:`regex`, :lookup:`iregex`, :lookup:`startswith`,
+    :lookup:`istartswith`, :lookup:`lt`, :lookup:`lte`, :lookup:`gt`, and
+    :lookup:`gte`, as well as with :ref:`containment-and-key-lookups`.
 
 .. warning::
 
@@ -937,8 +937,10 @@ To query for missing keys, use the ``isnull`` lookup::
     On PostgreSQL, if only one key or index is used, the SQL operator ``->`` is
     used. If multiple operators are used then the ``#>`` operator is used.
 
-Containment and key operations
-------------------------------
+.. _containment-and-key-lookups:
+
+Containment and key lookups
+---------------------------
 
 .. fieldlookup:: jsonfield.contains
 

+ 25 - 3
tests/model_fields/test_jsonfield.py

@@ -608,12 +608,29 @@ class TestQuerying(TestCase):
             self.objs[3:5],
         )
 
+    @skipUnlessDBFeature('supports_json_field_contains')
+    def test_array_key_contains(self):
+        tests = [
+            ([], [self.objs[7]]),
+            ('bar', [self.objs[7]]),
+            (['bar'], [self.objs[7]]),
+            ('ar', []),
+        ]
+        for value, expected in tests:
+            with self.subTest(value=value):
+                self.assertSequenceEqual(
+                    NullableJSONModel.objects.filter(value__bar__contains=value),
+                    expected,
+                )
+
     def test_key_iexact(self):
         self.assertIs(NullableJSONModel.objects.filter(value__foo__iexact='BaR').exists(), True)
         self.assertIs(NullableJSONModel.objects.filter(value__foo__iexact='"BaR"').exists(), False)
 
+    @skipUnlessDBFeature('supports_json_field_contains')
     def test_key_contains(self):
-        self.assertIs(NullableJSONModel.objects.filter(value__foo__contains='ar').exists(), True)
+        self.assertIs(NullableJSONModel.objects.filter(value__foo__contains='ar').exists(), False)
+        self.assertIs(NullableJSONModel.objects.filter(value__foo__contains='bar').exists(), True)
 
     def test_key_icontains(self):
         self.assertIs(NullableJSONModel.objects.filter(value__foo__icontains='Ar').exists(), True)
@@ -670,7 +687,6 @@ class TestQuerying(TestCase):
 
     def test_lookups_with_key_transform(self):
         tests = (
-            ('value__d__contains', 'e'),
             ('value__baz__has_key', 'c'),
             ('value__baz__has_keys', ['a', 'c']),
             ('value__baz__has_any_keys', ['a', 'x']),
@@ -685,7 +701,10 @@ class TestQuerying(TestCase):
     @skipUnlessDBFeature('supports_json_field_contains')
     def test_contains_contained_by_with_key_transform(self):
         tests = [
+            ('value__d__contains', 'e'),
+            ('value__d__contains', [{'f': 'g'}]),
             ('value__contains', KeyTransform('bax', 'value')),
+            ('value__baz__contains', {'a': 'b'}),
             ('value__baz__contained_by', {'a': 'b', 'c': 'd', 'e': 'f'}),
             (
                 'value__contained_by',
@@ -695,8 +714,11 @@ class TestQuerying(TestCase):
                 )),
             ),
         ]
+        # PostgreSQL requires a layer of nesting.
+        if connection.vendor != 'postgresql':
+            tests.append(('value__d__contains', {'f': 'g'}))
         for lookup, value in tests:
-            with self.subTest(lookup=lookup):
+            with self.subTest(lookup=lookup, value=value):
                 self.assertIs(NullableJSONModel.objects.filter(
                     **{lookup: value},
                 ).exists(), True)