Browse Source

Fixed #33966 -- Added support for using KeyTextTransform from lookup.

Allen Jonathan David 2 years ago
parent
commit
10178197d5

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

@@ -4,6 +4,7 @@ from django import forms
 from django.core import checks, exceptions
 from django.core import checks, exceptions
 from django.db import NotSupportedError, connections, router
 from django.db import NotSupportedError, connections, router
 from django.db.models import lookups
 from django.db.models import lookups
+from django.db.models.constants import LOOKUP_SEP
 from django.db.models.fields import TextField
 from django.db.models.fields import TextField
 from django.db.models.lookups import PostgresOperatorLookup, Transform
 from django.db.models.lookups import PostgresOperatorLookup, Transform
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
@@ -379,6 +380,18 @@ class KeyTextTransform(KeyTransform):
             json_path = compile_json_path(key_transforms)
             json_path = compile_json_path(key_transforms)
             return "(%s ->> %%s)" % lhs, tuple(params) + (json_path,)
             return "(%s ->> %%s)" % lhs, tuple(params) + (json_path,)
 
 
+    @classmethod
+    def from_lookup(cls, lookup):
+        transform, *keys = lookup.split(LOOKUP_SEP)
+        if not keys:
+            raise ValueError("Lookup must contain key or index transforms.")
+        for key in keys:
+            transform = cls(key, transform)
+        return transform
+
+
+KT = KeyTextTransform.from_lookup
+
 
 
 class KeyTransformTextLookupMixin:
 class KeyTransformTextLookupMixin:
     """
     """

+ 4 - 0
docs/releases/4.2.txt

@@ -216,6 +216,10 @@ Models
   allows performing actions that can fail after a database transaction is
   allows performing actions that can fail after a database transaction is
   successfully committed.
   successfully committed.
 
 
+* The new :class:`KT() <django.db.models.fields.json.KT>` expression represents
+  the text value of a key, index, or path transform of
+  :class:`~django.db.models.JSONField`.
+
 Requests and Responses
 Requests and Responses
 ~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~
 
 

+ 27 - 0
docs/topics/db/queries.txt

@@ -1059,6 +1059,33 @@ To query for missing keys, use the ``isnull`` lookup::
     :lookup:`istartswith`, :lookup:`lt`, :lookup:`lte`, :lookup:`gt`, and
     :lookup:`istartswith`, :lookup:`lt`, :lookup:`lte`, :lookup:`gt`, and
     :lookup:`gte`, as well as with :ref:`containment-and-key-lookups`.
     :lookup:`gte`, as well as with :ref:`containment-and-key-lookups`.
 
 
+``KT()`` expressions
+~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 4.2
+
+.. module:: django.db.models.fields.json
+
+.. class:: KT(lookup)
+
+    Represents the text value of a key, index, or path transform of
+    :class:`~django.db.models.JSONField`. You can use the double underscore
+    notation in ``lookup`` to chain dictionary key and index transforms.
+
+    For example::
+
+        >>> from django.db.models.fields.json import KT
+        >>> Dog.objects.create(name="Shep", data={
+        ...     "owner": {"name": "Bob"},
+        ...     "breed": ["collie", "lhasa apso"],
+        ... })
+        <Dog: Shep>
+        >>> Dogs.objects.annotate(
+        ...     first_breed=KT("data__breed__1"),
+        ...     owner_name=KT("data__owner__name")
+        ... ).filter(first_breed__startswith="lhasa", owner_name="Bob")
+        <QuerySet [<Dog: Shep>]>
+
 .. note::
 .. note::
 
 
     Due to the way in which key-path queries work,
     Due to the way in which key-path queries work,

+ 19 - 5
tests/model_fields/test_jsonfield.py

@@ -27,6 +27,7 @@ from django.db.models import (
 )
 )
 from django.db.models.expressions import RawSQL
 from django.db.models.expressions import RawSQL
 from django.db.models.fields.json import (
 from django.db.models.fields.json import (
+    KT,
     KeyTextTransform,
     KeyTextTransform,
     KeyTransform,
     KeyTransform,
     KeyTransformFactory,
     KeyTransformFactory,
@@ -374,11 +375,7 @@ class TestQuerying(TestCase):
         qs = NullableJSONModel.objects.filter(value__isnull=False)
         qs = NullableJSONModel.objects.filter(value__isnull=False)
         self.assertQuerysetEqual(
         self.assertQuerysetEqual(
             qs.filter(value__isnull=False)
             qs.filter(value__isnull=False)
-            .annotate(
+            .annotate(key=KT("value__d__1__f"))
-                key=KeyTextTransform(
-                    "f", KeyTransform("1", KeyTransform("d", "value"))
-                ),
-            )
             .values("key")
             .values("key")
             .annotate(count=Count("key"))
             .annotate(count=Count("key"))
             .order_by("count"),
             .order_by("count"),
@@ -1078,3 +1075,20 @@ class TestQuerying(TestCase):
             ).filter(chain=F("related_key__0")),
             ).filter(chain=F("related_key__0")),
             [related_obj],
             [related_obj],
         )
         )
+
+    def test_key_text_transform_from_lookup(self):
+        qs = NullableJSONModel.objects.annotate(b=KT("value__bax__foo")).filter(
+            b__contains="ar",
+        )
+        self.assertSequenceEqual(qs, [self.objs[7]])
+        qs = NullableJSONModel.objects.annotate(c=KT("value__o")).filter(
+            c__contains="uot",
+        )
+        self.assertSequenceEqual(qs, [self.objs[4]])
+
+    def test_key_text_transform_from_lookup_invalid(self):
+        msg = "Lookup must contain key or index transforms."
+        with self.assertRaisesMessage(ValueError, msg):
+            KT("value")
+        with self.assertRaisesMessage(ValueError, msg):
+            KT("")