Fixed #31836 -- Dropped support for JSONField __contains and __contained_by lookups on SQLite.
The current implementation works only for basic examples without supporting nested structures and doesn't follow "the general principle that the contained object must match the containing object as to structure and data contents, possibly after discarding some non-matching array elements or object key/value pairs from the containing object".
This commit is contained in:
parent
7dbbe65647
commit
ba691933ce
|
@ -295,6 +295,9 @@ class BaseDatabaseFeatures:
|
||||||
has_native_json_field = False
|
has_native_json_field = False
|
||||||
# Does the backend use PostgreSQL-style JSON operators like '->'?
|
# Does the backend use PostgreSQL-style JSON operators like '->'?
|
||||||
has_json_operators = False
|
has_json_operators = False
|
||||||
|
# Does the backend support __contains and __contained_by lookups for
|
||||||
|
# a JSONField?
|
||||||
|
supports_json_field_contains = True
|
||||||
|
|
||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
|
|
|
@ -60,6 +60,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
allows_multiple_constraints_on_same_fields = False
|
allows_multiple_constraints_on_same_fields = False
|
||||||
supports_boolean_expr_in_select_clause = False
|
supports_boolean_expr_in_select_clause = False
|
||||||
supports_primitives_in_json_field = False
|
supports_primitives_in_json_field = False
|
||||||
|
supports_json_field_contains = False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def introspected_field_types(self):
|
def introspected_field_types(self):
|
||||||
|
|
|
@ -5,7 +5,6 @@ import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import functools
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
|
||||||
import math
|
import math
|
||||||
import operator
|
import operator
|
||||||
import re
|
import re
|
||||||
|
@ -235,7 +234,6 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
create_deterministic_function('DEGREES', 1, none_guard(math.degrees))
|
create_deterministic_function('DEGREES', 1, none_guard(math.degrees))
|
||||||
create_deterministic_function('EXP', 1, none_guard(math.exp))
|
create_deterministic_function('EXP', 1, none_guard(math.exp))
|
||||||
create_deterministic_function('FLOOR', 1, none_guard(math.floor))
|
create_deterministic_function('FLOOR', 1, none_guard(math.floor))
|
||||||
create_deterministic_function('JSON_CONTAINS', 2, _sqlite_json_contains)
|
|
||||||
create_deterministic_function('LN', 1, none_guard(math.log))
|
create_deterministic_function('LN', 1, none_guard(math.log))
|
||||||
create_deterministic_function('LOG', 2, none_guard(lambda x, y: math.log(y, x)))
|
create_deterministic_function('LOG', 2, none_guard(lambda x, y: math.log(y, x)))
|
||||||
create_deterministic_function('LPAD', 3, _sqlite_lpad)
|
create_deterministic_function('LPAD', 3, _sqlite_lpad)
|
||||||
|
@ -601,11 +599,3 @@ def _sqlite_lpad(text, length, fill_text):
|
||||||
@none_guard
|
@none_guard
|
||||||
def _sqlite_rpad(text, length, fill_text):
|
def _sqlite_rpad(text, length, fill_text):
|
||||||
return (text + fill_text * length)[:length]
|
return (text + fill_text * length)[:length]
|
||||||
|
|
||||||
|
|
||||||
@none_guard
|
|
||||||
def _sqlite_json_contains(haystack, needle):
|
|
||||||
target, candidate = json.loads(haystack), json.loads(needle)
|
|
||||||
if isinstance(target, dict) and isinstance(candidate, dict):
|
|
||||||
return target.items() >= candidate.items()
|
|
||||||
return target == candidate
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
supports_aggregate_filter_clause = Database.sqlite_version_info >= (3, 30, 1)
|
supports_aggregate_filter_clause = Database.sqlite_version_info >= (3, 30, 1)
|
||||||
supports_order_by_nulls_modifier = Database.sqlite_version_info >= (3, 30, 0)
|
supports_order_by_nulls_modifier = Database.sqlite_version_info >= (3, 30, 0)
|
||||||
order_by_nulls_first = True
|
order_by_nulls_first = True
|
||||||
|
supports_json_field_contains = False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def supports_atomic_references_rename(self):
|
def supports_atomic_references_rename(self):
|
||||||
|
|
|
@ -140,28 +140,30 @@ class DataContains(PostgresOperatorLookup):
|
||||||
postgres_operator = '@>'
|
postgres_operator = '@>'
|
||||||
|
|
||||||
def as_sql(self, compiler, connection):
|
def as_sql(self, compiler, connection):
|
||||||
|
if not connection.features.supports_json_field_contains:
|
||||||
|
raise NotSupportedError(
|
||||||
|
'contains lookup is not supported on this database backend.'
|
||||||
|
)
|
||||||
lhs, lhs_params = self.process_lhs(compiler, connection)
|
lhs, lhs_params = self.process_lhs(compiler, connection)
|
||||||
rhs, rhs_params = self.process_rhs(compiler, connection)
|
rhs, rhs_params = self.process_rhs(compiler, connection)
|
||||||
params = tuple(lhs_params) + tuple(rhs_params)
|
params = tuple(lhs_params) + tuple(rhs_params)
|
||||||
return 'JSON_CONTAINS(%s, %s)' % (lhs, rhs), params
|
return 'JSON_CONTAINS(%s, %s)' % (lhs, rhs), params
|
||||||
|
|
||||||
def as_oracle(self, compiler, connection):
|
|
||||||
raise NotSupportedError('contains lookup is not supported on Oracle.')
|
|
||||||
|
|
||||||
|
|
||||||
class ContainedBy(PostgresOperatorLookup):
|
class ContainedBy(PostgresOperatorLookup):
|
||||||
lookup_name = 'contained_by'
|
lookup_name = 'contained_by'
|
||||||
postgres_operator = '<@'
|
postgres_operator = '<@'
|
||||||
|
|
||||||
def as_sql(self, compiler, connection):
|
def as_sql(self, compiler, connection):
|
||||||
|
if not connection.features.supports_json_field_contains:
|
||||||
|
raise NotSupportedError(
|
||||||
|
'contained_by lookup is not supported on this database backend.'
|
||||||
|
)
|
||||||
lhs, lhs_params = self.process_lhs(compiler, connection)
|
lhs, lhs_params = self.process_lhs(compiler, connection)
|
||||||
rhs, rhs_params = self.process_rhs(compiler, connection)
|
rhs, rhs_params = self.process_rhs(compiler, connection)
|
||||||
params = tuple(rhs_params) + tuple(lhs_params)
|
params = tuple(rhs_params) + tuple(lhs_params)
|
||||||
return 'JSON_CONTAINS(%s, %s)' % (rhs, lhs), params
|
return 'JSON_CONTAINS(%s, %s)' % (rhs, lhs), params
|
||||||
|
|
||||||
def as_oracle(self, compiler, connection):
|
|
||||||
raise NotSupportedError('contained_by lookup is not supported on Oracle.')
|
|
||||||
|
|
||||||
|
|
||||||
class HasKeyLookup(PostgresOperatorLookup):
|
class HasKeyLookup(PostgresOperatorLookup):
|
||||||
logical_operator = None
|
logical_operator = None
|
||||||
|
|
|
@ -533,7 +533,10 @@ backends.
|
||||||
``DatabaseFeatures.supports_json_field`` to ``False``. If storing primitives
|
``DatabaseFeatures.supports_json_field`` to ``False``. If storing primitives
|
||||||
is not supported, set ``DatabaseFeatures.supports_primitives_in_json_field``
|
is not supported, set ``DatabaseFeatures.supports_primitives_in_json_field``
|
||||||
to ``False``. If there is a true datatype for JSON, set
|
to ``False``. If there is a true datatype for JSON, set
|
||||||
``DatabaseFeatures.has_native_json_field`` to ``True``.
|
``DatabaseFeatures.has_native_json_field`` to ``True``. If
|
||||||
|
:lookup:`jsonfield.contains` and :lookup:`jsonfield.contained_by` are not
|
||||||
|
supported, set ``DatabaseFeatures.supports_json_field_contains`` to
|
||||||
|
``False``.
|
||||||
|
|
||||||
* Third party database backends must implement introspection for ``JSONField``
|
* Third party database backends must implement introspection for ``JSONField``
|
||||||
or set ``can_introspect_json_field`` to ``False``.
|
or set ``can_introspect_json_field`` to ``False``.
|
||||||
|
|
|
@ -960,9 +960,9 @@ contained in the top-level of the field. For example::
|
||||||
>>> Dog.objects.filter(data__contains={'breed': 'collie'})
|
>>> Dog.objects.filter(data__contains={'breed': 'collie'})
|
||||||
<QuerySet [<Dog: Meg>]>
|
<QuerySet [<Dog: Meg>]>
|
||||||
|
|
||||||
.. admonition:: Oracle
|
.. admonition:: Oracle and SQLite
|
||||||
|
|
||||||
``contains`` is not supported on Oracle.
|
``contains`` is not supported on Oracle and SQLite.
|
||||||
|
|
||||||
.. fieldlookup:: jsonfield.contained_by
|
.. fieldlookup:: jsonfield.contained_by
|
||||||
|
|
||||||
|
@ -984,9 +984,9 @@ subset of those in the value passed. For example::
|
||||||
>>> Dog.objects.filter(data__contained_by={'breed': 'collie'})
|
>>> Dog.objects.filter(data__contained_by={'breed': 'collie'})
|
||||||
<QuerySet [<Dog: Fred>]>
|
<QuerySet [<Dog: Fred>]>
|
||||||
|
|
||||||
.. admonition:: Oracle
|
.. admonition:: Oracle and SQLite
|
||||||
|
|
||||||
``contained_by`` is not supported on Oracle.
|
``contained_by`` is not supported on Oracle and SQLite.
|
||||||
|
|
||||||
.. fieldlookup:: jsonfield.has_key
|
.. fieldlookup:: jsonfield.has_key
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import operator
|
import operator
|
||||||
import uuid
|
import uuid
|
||||||
from unittest import mock, skipIf, skipUnless
|
from unittest import mock, skipIf
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
|
@ -441,17 +441,20 @@ class TestQuerying(TestCase):
|
||||||
[self.objs[3], self.objs[4], self.objs[6]],
|
[self.objs[3], self.objs[4], self.objs[6]],
|
||||||
)
|
)
|
||||||
|
|
||||||
@skipIf(
|
@skipUnlessDBFeature('supports_json_field_contains')
|
||||||
connection.vendor == 'oracle',
|
|
||||||
"Oracle doesn't support contains lookup.",
|
|
||||||
)
|
|
||||||
def test_contains(self):
|
def test_contains(self):
|
||||||
tests = [
|
tests = [
|
||||||
({}, self.objs[2:5] + self.objs[6:8]),
|
({}, self.objs[2:5] + self.objs[6:8]),
|
||||||
({'baz': {'a': 'b', 'c': 'd'}}, [self.objs[7]]),
|
({'baz': {'a': 'b', 'c': 'd'}}, [self.objs[7]]),
|
||||||
|
({'baz': {'a': 'b'}}, [self.objs[7]]),
|
||||||
|
({'baz': {'c': 'd'}}, [self.objs[7]]),
|
||||||
({'k': True, 'l': False}, [self.objs[6]]),
|
({'k': True, 'l': False}, [self.objs[6]]),
|
||||||
({'d': ['e', {'f': 'g'}]}, [self.objs[4]]),
|
({'d': ['e', {'f': 'g'}]}, [self.objs[4]]),
|
||||||
|
({'d': ['e']}, [self.objs[4]]),
|
||||||
|
({'d': [{'f': 'g'}]}, [self.objs[4]]),
|
||||||
([1, [2]], [self.objs[5]]),
|
([1, [2]], [self.objs[5]]),
|
||||||
|
([1], [self.objs[5]]),
|
||||||
|
([[2]], [self.objs[5]]),
|
||||||
({'n': [None]}, [self.objs[4]]),
|
({'n': [None]}, [self.objs[4]]),
|
||||||
({'j': None}, [self.objs[4]]),
|
({'j': None}, [self.objs[4]]),
|
||||||
]
|
]
|
||||||
|
@ -460,38 +463,32 @@ class TestQuerying(TestCase):
|
||||||
qs = NullableJSONModel.objects.filter(value__contains=value)
|
qs = NullableJSONModel.objects.filter(value__contains=value)
|
||||||
self.assertSequenceEqual(qs, expected)
|
self.assertSequenceEqual(qs, expected)
|
||||||
|
|
||||||
@skipUnless(
|
@skipIfDBFeature('supports_json_field_contains')
|
||||||
connection.vendor == 'oracle',
|
|
||||||
"Oracle doesn't support contains lookup.",
|
|
||||||
)
|
|
||||||
def test_contains_unsupported(self):
|
def test_contains_unsupported(self):
|
||||||
msg = 'contains lookup is not supported on Oracle.'
|
msg = 'contains lookup is not supported on this database backend.'
|
||||||
with self.assertRaisesMessage(NotSupportedError, msg):
|
with self.assertRaisesMessage(NotSupportedError, msg):
|
||||||
NullableJSONModel.objects.filter(
|
NullableJSONModel.objects.filter(
|
||||||
value__contains={'baz': {'a': 'b', 'c': 'd'}},
|
value__contains={'baz': {'a': 'b', 'c': 'd'}},
|
||||||
).get()
|
).get()
|
||||||
|
|
||||||
@skipUnlessDBFeature('supports_primitives_in_json_field')
|
@skipUnlessDBFeature(
|
||||||
|
'supports_primitives_in_json_field',
|
||||||
|
'supports_json_field_contains',
|
||||||
|
)
|
||||||
def test_contains_primitives(self):
|
def test_contains_primitives(self):
|
||||||
for value in self.primitives:
|
for value in self.primitives:
|
||||||
with self.subTest(value=value):
|
with self.subTest(value=value):
|
||||||
qs = NullableJSONModel.objects.filter(value__contains=value)
|
qs = NullableJSONModel.objects.filter(value__contains=value)
|
||||||
self.assertIs(qs.exists(), True)
|
self.assertIs(qs.exists(), True)
|
||||||
|
|
||||||
@skipIf(
|
@skipUnlessDBFeature('supports_json_field_contains')
|
||||||
connection.vendor == 'oracle',
|
|
||||||
"Oracle doesn't support contained_by lookup.",
|
|
||||||
)
|
|
||||||
def test_contained_by(self):
|
def test_contained_by(self):
|
||||||
qs = NullableJSONModel.objects.filter(value__contained_by={'a': 'b', 'c': 14, 'h': True})
|
qs = NullableJSONModel.objects.filter(value__contained_by={'a': 'b', 'c': 14, 'h': True})
|
||||||
self.assertSequenceEqual(qs, self.objs[2:4])
|
self.assertSequenceEqual(qs, self.objs[2:4])
|
||||||
|
|
||||||
@skipUnless(
|
@skipIfDBFeature('supports_json_field_contains')
|
||||||
connection.vendor == 'oracle',
|
|
||||||
"Oracle doesn't support contained_by lookup.",
|
|
||||||
)
|
|
||||||
def test_contained_by_unsupported(self):
|
def test_contained_by_unsupported(self):
|
||||||
msg = 'contained_by lookup is not supported on Oracle.'
|
msg = 'contained_by lookup is not supported on this database backend.'
|
||||||
with self.assertRaisesMessage(NotSupportedError, msg):
|
with self.assertRaisesMessage(NotSupportedError, msg):
|
||||||
NullableJSONModel.objects.filter(value__contained_by={'a': 'b'}).get()
|
NullableJSONModel.objects.filter(value__contained_by={'a': 'b'}).get()
|
||||||
|
|
||||||
|
@ -679,19 +676,25 @@ class TestQuerying(TestCase):
|
||||||
('value__baz__has_any_keys', ['a', 'x']),
|
('value__baz__has_any_keys', ['a', 'x']),
|
||||||
('value__has_key', KeyTextTransform('foo', 'value')),
|
('value__has_key', KeyTextTransform('foo', 'value')),
|
||||||
)
|
)
|
||||||
# contained_by and contains lookups are not supported on Oracle.
|
for lookup, value in tests:
|
||||||
if connection.vendor != 'oracle':
|
with self.subTest(lookup=lookup):
|
||||||
tests += (
|
self.assertIs(NullableJSONModel.objects.filter(
|
||||||
('value__contains', KeyTransform('bax', 'value')),
|
**{lookup: value},
|
||||||
('value__baz__contained_by', {'a': 'b', 'c': 'd', 'e': 'f'}),
|
).exists(), True)
|
||||||
(
|
|
||||||
'value__contained_by',
|
@skipUnlessDBFeature('supports_json_field_contains')
|
||||||
KeyTransform('x', RawSQL(
|
def test_contains_contained_by_with_key_transform(self):
|
||||||
self.raw_sql,
|
tests = [
|
||||||
['{"x": {"a": "b", "c": 1, "d": "e"}}'],
|
('value__contains', KeyTransform('bax', 'value')),
|
||||||
)),
|
('value__baz__contained_by', {'a': 'b', 'c': 'd', 'e': 'f'}),
|
||||||
),
|
(
|
||||||
)
|
'value__contained_by',
|
||||||
|
KeyTransform('x', RawSQL(
|
||||||
|
self.raw_sql,
|
||||||
|
['{"x": {"a": "b", "c": 1, "d": "e"}}'],
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
]
|
||||||
for lookup, value in tests:
|
for lookup, value in tests:
|
||||||
with self.subTest(lookup=lookup):
|
with self.subTest(lookup=lookup):
|
||||||
self.assertIs(NullableJSONModel.objects.filter(
|
self.assertIs(NullableJSONModel.objects.filter(
|
||||||
|
|
Loading…
Reference in New Issue