Fixed #25718 -- Made a JSONField lookup value of None match keys that have a null value.
This commit is contained in:
parent
4fe5d84666
commit
c979c0a2b8
|
@ -88,6 +88,7 @@ JSONField.register_lookup(lookups.ContainedBy)
|
|||
JSONField.register_lookup(lookups.HasKey)
|
||||
JSONField.register_lookup(lookups.HasKeys)
|
||||
JSONField.register_lookup(lookups.HasAnyKeys)
|
||||
JSONField.register_lookup(lookups.JSONExact)
|
||||
|
||||
|
||||
class KeyTransform(Transform):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.db.models import Lookup, Transform
|
||||
from django.db.models.lookups import Exact
|
||||
|
||||
from .search import SearchVector, SearchVectorExact, SearchVectorField
|
||||
|
||||
|
@ -64,3 +65,12 @@ class SearchLookup(SearchVectorExact):
|
|||
class TrigramSimilar(PostgresSimpleLookup):
|
||||
lookup_name = 'trigram_similar'
|
||||
operator = '%%'
|
||||
|
||||
|
||||
class JSONExact(Exact):
|
||||
can_use_none_as_rhs = True
|
||||
|
||||
def process_rhs(self, compiler, connection):
|
||||
result = super().process_rhs(compiler, connection)
|
||||
# Treat None lookup values as null.
|
||||
return ("'null'", []) if result == ('%s', [None]) else result
|
||||
|
|
|
@ -12,6 +12,7 @@ from django.utils.functional import cached_property
|
|||
class Lookup:
|
||||
lookup_name = None
|
||||
prepare_rhs = True
|
||||
can_use_none_as_rhs = False
|
||||
|
||||
def __init__(self, lhs, rhs):
|
||||
self.lhs, self.rhs = lhs, rhs
|
||||
|
|
|
@ -1083,8 +1083,8 @@ class Query:
|
|||
|
||||
lookup = lookup_class(lhs, rhs)
|
||||
# Interpret '__exact=None' as the sql 'is NULL'; otherwise, reject all
|
||||
# uses of None as a query value.
|
||||
if lookup.rhs is None:
|
||||
# uses of None as a query value unless the lookup supports it.
|
||||
if lookup.rhs is None and not lookup.can_use_none_as_rhs:
|
||||
if lookup_name not in ('exact', 'iexact'):
|
||||
raise ValueError("Cannot use None as a query value")
|
||||
return lhs.get_lookup('isnull')(lhs, True)
|
||||
|
@ -1215,7 +1215,7 @@ class Query:
|
|||
clause.add(condition, AND)
|
||||
|
||||
require_outer = lookup_type == 'isnull' and condition.rhs is True and not current_negated
|
||||
if current_negated and (lookup_type != 'isnull' or condition.rhs is False):
|
||||
if current_negated and (lookup_type != 'isnull' or condition.rhs is False) and condition.rhs is not None:
|
||||
require_outer = True
|
||||
if (lookup_type != 'isnull' and (
|
||||
self.is_nullable(targets[0]) or
|
||||
|
|
|
@ -544,7 +544,7 @@ name::
|
|||
... }],
|
||||
... },
|
||||
... })
|
||||
>>> Dog.objects.create(name='Meg', data={'breed': 'collie'})
|
||||
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': None})
|
||||
|
||||
>>> Dog.objects.filter(data__breed='collie')
|
||||
<QuerySet [<Dog: Meg>]>
|
||||
|
@ -566,6 +566,23 @@ the :lookup:`jsonfield.contains` lookup instead.
|
|||
If only one key or index is used, the SQL operator ``->`` is used. If multiple
|
||||
operators are used then the ``#>`` operator is used.
|
||||
|
||||
To query for ``null`` in JSON data, use ``None`` as a value::
|
||||
|
||||
>>> Dog.objects.filter(data__owner=None)
|
||||
<QuerySet [<Dog: Meg>]>
|
||||
|
||||
To query for missing keys, use the ``isnull`` lookup::
|
||||
|
||||
>>> Dog.objects.create(name='Shep', data={'breed': 'collie'})
|
||||
>>> Dog.objects.filter(data__owner__isnull=True)
|
||||
<QuerySet [<Dog: Shep>]>
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
In older versions, using ``None`` as a lookup value matches objects that
|
||||
don't have the key rather than objects that have the key with a ``None``
|
||||
value.
|
||||
|
||||
.. warning::
|
||||
|
||||
Since any string could be a key in a JSON object, any lookup other than
|
||||
|
|
|
@ -372,6 +372,10 @@ Miscellaneous
|
|||
* Since migrations are now loaded from ``.pyc`` files, you might need to delete
|
||||
them if you're working in a mixed Python 2 and Python 3 environment.
|
||||
|
||||
* Using ``None`` as a :class:`~django.contrib.postgres.fields.JSONField` lookup
|
||||
value now matches objects that have the specified key and a null value rather
|
||||
than objects that don't have the key.
|
||||
|
||||
.. _deprecated-features-2.1:
|
||||
|
||||
Features deprecated in 2.1
|
||||
|
|
|
@ -5,6 +5,7 @@ This demonstrates features of the database API.
|
|||
"""
|
||||
|
||||
from django.db import models
|
||||
from django.db.models.lookups import IsNull
|
||||
|
||||
|
||||
class Alarm(models.Model):
|
||||
|
@ -55,6 +56,12 @@ class NulledTransform(models.Transform):
|
|||
template = 'NULL'
|
||||
|
||||
|
||||
@NulledTextField.register_lookup
|
||||
class IsNullWithNoneAsRHS(IsNull):
|
||||
lookup_name = 'isnull_none_rhs'
|
||||
can_use_none_as_rhs = True
|
||||
|
||||
|
||||
class Season(models.Model):
|
||||
year = models.PositiveSmallIntegerField()
|
||||
gt = models.IntegerField(null=True, blank=True)
|
||||
|
|
|
@ -8,7 +8,9 @@ from django.db import connection
|
|||
from django.db.models.functions import Substr
|
||||
from django.test import TestCase, skipUnlessDBFeature
|
||||
|
||||
from .models import Article, Author, Game, Player, Season, Tag
|
||||
from .models import (
|
||||
Article, Author, Game, IsNullWithNoneAsRHS, Player, Season, Tag,
|
||||
)
|
||||
|
||||
|
||||
class LookupTests(TestCase):
|
||||
|
@ -895,3 +897,11 @@ class LookupTests(TestCase):
|
|||
with self.subTest(lookup=lookup):
|
||||
authors = Author.objects.filter(**{'name__%s' % lookup: Substr('alias', 1, 3)})
|
||||
self.assertCountEqual(authors, result)
|
||||
|
||||
def test_custom_lookup_none_rhs(self):
|
||||
"""Lookup.can_use_none_as_rhs=True allows None as a lookup value."""
|
||||
season = Season.objects.create(year=2012, nulled_text_field=None)
|
||||
query = Season.objects.get_queryset().query
|
||||
field = query.model._meta.get_field('nulled_text_field')
|
||||
self.assertIsInstance(query.build_lookup(['isnull_none_rhs'], field, None), IsNullWithNoneAsRHS)
|
||||
self.assertTrue(Season.objects.filter(pk=season.pk, nulled_text_field__isnull_none_rhs=True))
|
||||
|
|
|
@ -4,6 +4,7 @@ from decimal import Decimal
|
|||
|
||||
from django.core import checks, exceptions, serializers
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.db.models import Q
|
||||
from django.forms import CharField, Form, widgets
|
||||
from django.test.utils import isolate_apps
|
||||
from django.utils.html import escape
|
||||
|
@ -177,6 +178,20 @@ class TestQuerying(PostgreSQLTestCase):
|
|||
[self.objs[7], self.objs[8]]
|
||||
)
|
||||
|
||||
def test_none_key(self):
|
||||
self.assertSequenceEqual(JSONModel.objects.filter(field__j=None), [self.objs[8]])
|
||||
|
||||
def test_none_key_exclude(self):
|
||||
obj = JSONModel.objects.create(field={'j': 1})
|
||||
self.assertSequenceEqual(JSONModel.objects.exclude(field__j=None), [obj])
|
||||
|
||||
def test_isnull_key_or_none(self):
|
||||
obj = JSONModel.objects.create(field={'a': None})
|
||||
self.assertSequenceEqual(
|
||||
JSONModel.objects.filter(Q(field__a__isnull=True) | Q(field__a=None)),
|
||||
self.objs[:7] + self.objs[9:] + [obj]
|
||||
)
|
||||
|
||||
def test_contains(self):
|
||||
self.assertSequenceEqual(
|
||||
JSONModel.objects.filter(field__contains={'a': 'b'}),
|
||||
|
|
Loading…
Reference in New Issue