Skip to content

Commit

Permalink
Fixed #27257 -- Fixed builtin text lookups on JSONField keys.
Browse files Browse the repository at this point in the history
Thanks Nick Stefan for the report and Tim for the review.
  • Loading branch information
charettes committed Sep 23, 2016
1 parent 658f1e8 commit cecef94
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 3 deletions.
67 changes: 64 additions & 3 deletions django/contrib/postgres/fields/jsonb.py
Expand Up @@ -4,7 +4,9 @@

from django.contrib.postgres import forms, lookups
from django.core import exceptions
from django.db.models import Field, Transform
from django.db.models import (
Field, TextField, Transform, lookups as builtin_lookups,
)
from django.utils.translation import ugettext_lazy as _

__all__ = ['JSONField']
Expand Down Expand Up @@ -86,6 +88,8 @@ def formfield(self, **kwargs):


class KeyTransform(Transform):
operator = '->'
nested_operator = '#>'

def __init__(self, key_name, *args, **kwargs):
super(KeyTransform, self).__init__(*args, **kwargs)
Expand All @@ -99,14 +103,71 @@ def as_sql(self, compiler, connection):
previous = previous.lhs
lhs, params = compiler.compile(previous)
if len(key_transforms) > 1:
return "{} #> %s".format(lhs), [key_transforms] + params
return "(%s %s %%s)" % (lhs, self.nested_operator), [key_transforms] + params
try:
int(self.key_name)
except ValueError:
lookup = "'%s'" % self.key_name
else:
lookup = "%s" % self.key_name
return "(%s -> %s)" % (lhs, lookup), params
return "(%s %s %s)" % (lhs, self.operator, lookup), params


class KeyTextTransform(KeyTransform):
operator = '->>'
nested_operator = '#>>'
_output_field = TextField()


class KeyTransformTextLookupMixin(object):
"""
Mixin for combining with a lookup expecting a text lhs from a JSONField
key lookup. Make use of the ->> operator instead of casting key values to
text and performing the lookup on the resulting representation.
"""
def __init__(self, key_transform, *args, **kwargs):
assert isinstance(key_transform, KeyTransform)
key_text_transform = KeyTextTransform(
key_transform.key_name, *key_transform.source_expressions, **key_transform.extra
)
super(KeyTransformTextLookupMixin, self).__init__(key_text_transform, *args, **kwargs)


class KeyTransformIContains(KeyTransformTextLookupMixin, builtin_lookups.IContains):
pass


class KeyTransformStartsWith(KeyTransformTextLookupMixin, builtin_lookups.StartsWith):
pass


class KeyTransformIStartsWith(KeyTransformTextLookupMixin, builtin_lookups.IStartsWith):
pass


class KeyTransformEndsWith(KeyTransformTextLookupMixin, builtin_lookups.EndsWith):
pass


class KeyTransformIEndsWith(KeyTransformTextLookupMixin, builtin_lookups.IEndsWith):
pass


class KeyTransformRegex(KeyTransformTextLookupMixin, builtin_lookups.Regex):
pass


class KeyTransformIRegex(KeyTransformTextLookupMixin, builtin_lookups.IRegex):
pass


KeyTransform.register_lookup(KeyTransformIContains)
KeyTransform.register_lookup(KeyTransformStartsWith)
KeyTransform.register_lookup(KeyTransformIStartsWith)
KeyTransform.register_lookup(KeyTransformEndsWith)
KeyTransform.register_lookup(KeyTransformIEndsWith)
KeyTransform.register_lookup(KeyTransformRegex)
KeyTransform.register_lookup(KeyTransformIRegex)


class KeyTransformFactory(object):
Expand Down
22 changes: 22 additions & 0 deletions tests/postgres_tests/test_json.py
Expand Up @@ -124,6 +124,7 @@ def setUpTestData(cls):
'k': True,
'l': False,
}),
JSONModel.objects.create(field={'foo': 'bar'}),
]

def test_exact(self):
Expand Down Expand Up @@ -237,6 +238,27 @@ def test_usage_in_subquery(self):
self.objs[7:9]
)

def test_icontains(self):
self.assertFalse(JSONModel.objects.filter(field__foo__icontains='"bar"').exists())

def test_startswith(self):
self.assertTrue(JSONModel.objects.filter(field__foo__startswith='b').exists())

def test_istartswith(self):
self.assertTrue(JSONModel.objects.filter(field__foo__istartswith='B').exists())

def test_endswith(self):
self.assertTrue(JSONModel.objects.filter(field__foo__endswith='r').exists())

def test_iendswith(self):
self.assertTrue(JSONModel.objects.filter(field__foo__iendswith='R').exists())

def test_regex(self):
self.assertTrue(JSONModel.objects.filter(field__foo__regex=r'^bar$').exists())

def test_iregex(self):
self.assertTrue(JSONModel.objects.filter(field__foo__iregex=r'^bAr$').exists())


@skipUnlessDBFeature('has_jsonb_datatype')
class TestSerialization(PostgreSQLTestCase):
Expand Down

0 comments on commit cecef94

Please sign in to comment.