mirror of https://github.com/django/django.git
Refs #33308 -- Deprecated support for passing encoded JSON string literals to JSONField & co.
JSON should be provided as literal Python objects an not in their encoded string literal forms.
This commit is contained in:
parent
d3e746ace5
commit
0ff46591ac
|
@ -1,8 +1,9 @@
|
|||
import json
|
||||
import warnings
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import Aggregate, BooleanField, JSONField, TextField, Value
|
||||
from django.utils.deprecation import RemovedInDjango50Warning
|
||||
from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning
|
||||
|
||||
from .mixins import OrderableAggMixin
|
||||
|
||||
|
@ -31,6 +32,14 @@ class DeprecatedConvertValueMixin:
|
|||
self._default_provided = True
|
||||
super().__init__(*expressions, default=default, **extra)
|
||||
|
||||
def resolve_expression(self, *args, **kwargs):
|
||||
resolved = super().resolve_expression(*args, **kwargs)
|
||||
if not self._default_provided:
|
||||
resolved.empty_result_set_value = getattr(
|
||||
self, "deprecation_empty_result_set_value", self.deprecation_value
|
||||
)
|
||||
return resolved
|
||||
|
||||
def convert_value(self, value, expression, connection):
|
||||
if value is None and not self._default_provided:
|
||||
warnings.warn(self.deprecation_msg, category=RemovedInDjango50Warning)
|
||||
|
@ -48,8 +57,7 @@ class ArrayAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
|
|||
deprecation_msg = (
|
||||
"In Django 5.0, ArrayAgg() will return None instead of an empty list "
|
||||
"if there are no rows. Pass default=None to opt into the new behavior "
|
||||
"and silence this warning or default=Value([]) to keep the previous "
|
||||
"behavior."
|
||||
"and silence this warning or default=[] to keep the previous behavior."
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -87,13 +95,46 @@ class JSONBAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
|
|||
|
||||
# RemovedInDjango50Warning
|
||||
deprecation_value = "[]"
|
||||
deprecation_empty_result_set_value = property(lambda self: [])
|
||||
deprecation_msg = (
|
||||
"In Django 5.0, JSONBAgg() will return None instead of an empty list "
|
||||
"if there are no rows. Pass default=None to opt into the new behavior "
|
||||
"and silence this warning or default=Value('[]') to keep the previous "
|
||||
"and silence this warning or default=[] to keep the previous "
|
||||
"behavior."
|
||||
)
|
||||
|
||||
# RemovedInDjango51Warning: When the deprecation ends, remove __init__().
|
||||
#
|
||||
# RemovedInDjango50Warning: When the deprecation ends, replace with:
|
||||
# def __init__(self, *expressions, default=None, **extra):
|
||||
def __init__(self, *expressions, default=NOT_PROVIDED, **extra):
|
||||
super().__init__(*expressions, default=default, **extra)
|
||||
if (
|
||||
isinstance(default, Value)
|
||||
and isinstance(default.value, str)
|
||||
and not isinstance(default.output_field, JSONField)
|
||||
):
|
||||
value = default.value
|
||||
try:
|
||||
decoded = json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
warnings.warn(
|
||||
"Passing a Value() with an output_field that isn't a JSONField as "
|
||||
"JSONBAgg(default) is deprecated. Pass default="
|
||||
f"Value({value!r}, output_field=JSONField()) instead.",
|
||||
stacklevel=2,
|
||||
category=RemovedInDjango51Warning,
|
||||
)
|
||||
self.default.output_field = self.output_field
|
||||
else:
|
||||
self.default = Value(decoded, self.output_field)
|
||||
warnings.warn(
|
||||
"Passing an encoded JSON string as JSONBAgg(default) is "
|
||||
f"deprecated. Pass default={decoded!r} instead.",
|
||||
stacklevel=2,
|
||||
category=RemovedInDjango51Warning,
|
||||
)
|
||||
|
||||
|
||||
class StringAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
|
||||
function = "STRING_AGG"
|
||||
|
@ -106,8 +147,7 @@ class StringAgg(DeprecatedConvertValueMixin, OrderableAggMixin, Aggregate):
|
|||
deprecation_msg = (
|
||||
"In Django 5.0, StringAgg() will return None instead of an empty "
|
||||
"string if there are no rows. Pass default=None to opt into the new "
|
||||
"behavior and silence this warning or default=Value('') to keep the "
|
||||
"previous behavior."
|
||||
'behavior and silence this warning or default="" to keep the previous behavior.'
|
||||
)
|
||||
|
||||
def __init__(self, expression, delimiter, **extra):
|
||||
|
|
|
@ -85,6 +85,8 @@ class Aggregate(Func):
|
|||
return c
|
||||
if hasattr(default, "resolve_expression"):
|
||||
default = default.resolve_expression(query, allow_joins, reuse, summarize)
|
||||
if default._output_field_or_none is None:
|
||||
default.output_field = c._output_field_or_none
|
||||
else:
|
||||
default = Value(default, c._output_field_or_none)
|
||||
c.default = None # Reset the default argument before wrapping.
|
||||
|
|
|
@ -921,6 +921,8 @@ class Field(RegisterLookupMixin):
|
|||
|
||||
def get_db_prep_save(self, value, connection):
|
||||
"""Return field's value prepared for saving into a database."""
|
||||
if hasattr(value, "as_sql"):
|
||||
return value
|
||||
return self.get_db_prep_value(value, connection=connection, prepared=False)
|
||||
|
||||
def has_default(self):
|
||||
|
@ -1715,6 +1717,8 @@ class DecimalField(Field):
|
|||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if not prepared:
|
||||
value = self.get_prep_value(value)
|
||||
if hasattr(value, "as_sql"):
|
||||
return value
|
||||
return connection.ops.adapt_decimalfield_value(
|
||||
value, self.max_digits, self.decimal_places
|
||||
)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import json
|
||||
import warnings
|
||||
|
||||
from django import forms
|
||||
from django.core import checks, exceptions
|
||||
from django.db import NotSupportedError, connections, router
|
||||
from django.db.models import lookups
|
||||
from django.db.models import expressions, lookups
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.fields import TextField
|
||||
from django.db.models.lookups import (
|
||||
|
@ -11,6 +12,7 @@ from django.db.models.lookups import (
|
|||
PostgresOperatorLookup,
|
||||
Transform,
|
||||
)
|
||||
from django.utils.deprecation import RemovedInDjango51Warning
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import Field
|
||||
|
@ -97,7 +99,32 @@ class JSONField(CheckFieldDefaultMixin, Field):
|
|||
return "JSONField"
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if hasattr(value, "as_sql"):
|
||||
# RemovedInDjango51Warning: When the deprecation ends, replace with:
|
||||
# if (
|
||||
# isinstance(value, expressions.Value)
|
||||
# and isinstance(value.output_field, JSONField)
|
||||
# ):
|
||||
# value = value.value
|
||||
# elif hasattr(value, "as_sql"): ...
|
||||
if isinstance(value, expressions.Value):
|
||||
if isinstance(value.value, str) and not isinstance(
|
||||
value.output_field, JSONField
|
||||
):
|
||||
try:
|
||||
value = json.loads(value.value, cls=self.decoder)
|
||||
except json.JSONDecodeError:
|
||||
value = value.value
|
||||
else:
|
||||
warnings.warn(
|
||||
"Providing an encoded JSON string via Value() is deprecated. "
|
||||
f"Use Value({value!r}, output_field=JSONField()) instead.",
|
||||
category=RemovedInDjango51Warning,
|
||||
)
|
||||
elif isinstance(value.output_field, JSONField):
|
||||
value = value.value
|
||||
else:
|
||||
return value
|
||||
elif hasattr(value, "as_sql"):
|
||||
return value
|
||||
return connection.ops.adapt_json_value(value, self.encoder)
|
||||
|
||||
|
|
|
@ -1637,9 +1637,7 @@ class SQLInsertCompiler(SQLCompiler):
|
|||
"Window expressions are not allowed in this query (%s=%r)."
|
||||
% (field.name, value)
|
||||
)
|
||||
else:
|
||||
value = field.get_db_prep_save(value, connection=self.connection)
|
||||
return value
|
||||
return field.get_db_prep_save(value, connection=self.connection)
|
||||
|
||||
def pre_save_val(self, field, obj):
|
||||
"""
|
||||
|
@ -1893,18 +1891,14 @@ class SQLUpdateCompiler(SQLCompiler):
|
|||
)
|
||||
elif hasattr(val, "prepare_database_save"):
|
||||
if field.remote_field:
|
||||
val = field.get_db_prep_save(
|
||||
val.prepare_database_save(field),
|
||||
connection=self.connection,
|
||||
)
|
||||
val = val.prepare_database_save(field)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Tried to update field %s with a model instance, %r. "
|
||||
"Use a value compatible with %s."
|
||||
% (field, val, field.__class__.__name__)
|
||||
)
|
||||
else:
|
||||
val = field.get_db_prep_save(val, connection=self.connection)
|
||||
val = field.get_db_prep_save(val, connection=self.connection)
|
||||
|
||||
# Getting the placeholder for the field.
|
||||
if hasattr(field, "get_placeholder"):
|
||||
|
|
|
@ -522,9 +522,9 @@ class Query(BaseExpression):
|
|||
result = compiler.execute_sql(SINGLE)
|
||||
if result is None:
|
||||
result = empty_set_result
|
||||
|
||||
converters = compiler.get_converters(outer_query.annotation_select.values())
|
||||
result = next(compiler.apply_converters((result,), converters))
|
||||
else:
|
||||
converters = compiler.get_converters(outer_query.annotation_select.values())
|
||||
result = next(compiler.apply_converters((result,), converters))
|
||||
|
||||
return dict(zip(outer_query.annotation_select, result))
|
||||
|
||||
|
|
|
@ -39,6 +39,9 @@ details on these changes.
|
|||
|
||||
* The ``TransactionTestCase.assertQuerysetEqual()`` method will be removed.
|
||||
|
||||
* Support for passing encoded JSON string literals to ``JSONField`` and
|
||||
associated lookups and expressions will be removed.
|
||||
|
||||
.. _deprecation-removed-in-5.0:
|
||||
|
||||
5.0
|
||||
|
|
|
@ -444,6 +444,42 @@ but it should not be used for new migrations. Use
|
|||
:class:`~django.db.migrations.operations.AddIndex` and
|
||||
:class:`~django.db.migrations.operations.RemoveIndex` operations instead.
|
||||
|
||||
Passing encoded JSON string literals to ``JSONField`` is deprecated
|
||||
-------------------------------------------------------------------
|
||||
|
||||
``JSONField`` and its associated lookups and aggregates use to allow passing
|
||||
JSON encoded string literals which caused ambiguity on whether string literals
|
||||
were already encoded from database backend's perspective.
|
||||
|
||||
During the deprecation period string literals will be attempted to be JSON
|
||||
decoded and a warning will be emitted on success that points at passing
|
||||
non-encoded forms instead.
|
||||
|
||||
Code that use to pass JSON encoded string literals::
|
||||
|
||||
Document.objects.bulk_create(
|
||||
Document(data=Value("null")),
|
||||
Document(data=Value("[]")),
|
||||
Document(data=Value('"foo-bar"')),
|
||||
)
|
||||
Document.objects.annotate(
|
||||
JSONBAgg("field", default=Value('[]')),
|
||||
)
|
||||
|
||||
Should become::
|
||||
|
||||
Document.objects.bulk_create(
|
||||
Document(data=Value(None, JSONField())),
|
||||
Document(data=[]),
|
||||
Document(data="foo-bar"),
|
||||
)
|
||||
Document.objects.annotate(
|
||||
JSONBAgg("field", default=[]),
|
||||
)
|
||||
|
||||
From Django 5.1+ string literals will be implicitly interpreted as JSON string
|
||||
literals.
|
||||
|
||||
Miscellaneous
|
||||
-------------
|
||||
|
||||
|
|
|
@ -971,7 +971,7 @@ Storing and querying for ``None``
|
|||
|
||||
As with other fields, storing ``None`` as the field's value will store it as
|
||||
SQL ``NULL``. While not recommended, it is possible to store JSON scalar
|
||||
``null`` instead of SQL ``NULL`` by using :class:`Value('null')
|
||||
``null`` instead of SQL ``NULL`` by using :class:`Value(None, JSONField())
|
||||
<django.db.models.Value>`.
|
||||
|
||||
Whichever of the values is stored, when retrieved from the database, the Python
|
||||
|
@ -987,11 +987,13 @@ query for SQL ``NULL``, use :lookup:`isnull`::
|
|||
|
||||
>>> Dog.objects.create(name='Max', data=None) # SQL NULL.
|
||||
<Dog: Max>
|
||||
>>> Dog.objects.create(name='Archie', data=Value('null')) # JSON null.
|
||||
>>> Dog.objects.create(
|
||||
... name='Archie', data=Value(None, JSONField()) # JSON null.
|
||||
... )
|
||||
<Dog: Archie>
|
||||
>>> Dog.objects.filter(data=None)
|
||||
<QuerySet [<Dog: Archie>]>
|
||||
>>> Dog.objects.filter(data=Value('null'))
|
||||
>>> Dog.objects.filter(data=Value(None, JSONField())
|
||||
<QuerySet [<Dog: Archie>]>
|
||||
>>> Dog.objects.filter(data__isnull=True)
|
||||
<QuerySet [<Dog: Max>]>
|
||||
|
@ -1007,6 +1009,15 @@ Unless you are sure you wish to work with SQL ``NULL`` values, consider setting
|
|||
Storing JSON scalar ``null`` does not violate :attr:`null=False
|
||||
<django.db.models.Field.null>`.
|
||||
|
||||
.. versionchanged:: 4.2
|
||||
|
||||
Support for expressing JSON ``null`` using ``Value(None, JSONField())`` was
|
||||
added.
|
||||
|
||||
.. deprecated:: 4.2
|
||||
|
||||
Passing ``Value("null")`` to express JSON ``null`` is deprecated.
|
||||
|
||||
.. fieldlookup:: jsonfield.key
|
||||
|
||||
Key, index, and path transforms
|
||||
|
|
|
@ -19,6 +19,7 @@ from django.db.models import (
|
|||
ExpressionWrapper,
|
||||
F,
|
||||
IntegerField,
|
||||
JSONField,
|
||||
OuterRef,
|
||||
Q,
|
||||
Subquery,
|
||||
|
@ -36,6 +37,7 @@ from django.db.models.fields.json import (
|
|||
from django.db.models.functions import Cast
|
||||
from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.utils.deprecation import RemovedInDjango51Warning
|
||||
|
||||
from .models import CustomJSONDecoder, JSONModel, NullableJSONModel, RelatedJSONModel
|
||||
|
||||
|
@ -191,15 +193,40 @@ class TestSaveLoad(TestCase):
|
|||
obj.refresh_from_db()
|
||||
self.assertIsNone(obj.value)
|
||||
|
||||
def test_ambiguous_str_value_deprecation(self):
|
||||
msg = (
|
||||
"Providing an encoded JSON string via Value() is deprecated. Use Value([], "
|
||||
"output_field=JSONField()) instead."
|
||||
)
|
||||
with self.assertWarnsMessage(RemovedInDjango51Warning, msg):
|
||||
obj = NullableJSONModel.objects.create(value=Value("[]"))
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(obj.value, [])
|
||||
|
||||
@skipUnlessDBFeature("supports_primitives_in_json_field")
|
||||
def test_value_str_primitives_deprecation(self):
|
||||
msg = (
|
||||
"Providing an encoded JSON string via Value() is deprecated. Use "
|
||||
"Value(None, output_field=JSONField()) instead."
|
||||
)
|
||||
with self.assertWarnsMessage(RemovedInDjango51Warning, msg):
|
||||
obj = NullableJSONModel.objects.create(value=Value("null"))
|
||||
obj.refresh_from_db()
|
||||
self.assertIsNone(obj.value)
|
||||
obj = NullableJSONModel.objects.create(value=Value("invalid-json"))
|
||||
obj.refresh_from_db()
|
||||
self.assertEqual(obj.value, "invalid-json")
|
||||
|
||||
@skipUnlessDBFeature("supports_primitives_in_json_field")
|
||||
def test_json_null_different_from_sql_null(self):
|
||||
json_null = NullableJSONModel.objects.create(value=Value("null"))
|
||||
json_null = NullableJSONModel.objects.create(value=Value(None, JSONField()))
|
||||
NullableJSONModel.objects.update(value=Value(None, JSONField()))
|
||||
json_null.refresh_from_db()
|
||||
sql_null = NullableJSONModel.objects.create(value=None)
|
||||
sql_null.refresh_from_db()
|
||||
# 'null' is not equal to NULL in the database.
|
||||
self.assertSequenceEqual(
|
||||
NullableJSONModel.objects.filter(value=Value("null")),
|
||||
NullableJSONModel.objects.filter(value=Value(None, JSONField())),
|
||||
[json_null],
|
||||
)
|
||||
self.assertSequenceEqual(
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.db.models import (
|
|||
F,
|
||||
Func,
|
||||
IntegerField,
|
||||
JSONField,
|
||||
OuterRef,
|
||||
Q,
|
||||
Subquery,
|
||||
|
@ -15,7 +16,7 @@ from django.db.models.functions import Cast, Concat, Substr
|
|||
from django.test import skipUnlessDBFeature
|
||||
from django.test.utils import Approximate, ignore_warnings
|
||||
from django.utils import timezone
|
||||
from django.utils.deprecation import RemovedInDjango50Warning
|
||||
from django.utils.deprecation import RemovedInDjango50Warning, RemovedInDjango51Warning
|
||||
|
||||
from . import PostgreSQLTestCase
|
||||
from .models import AggregateTestModel, HotelReservation, Room, StatTestModel
|
||||
|
@ -124,7 +125,11 @@ class TestGeneralAggregate(PostgreSQLTestCase):
|
|||
(BitOr("integer_field", default=0), 0),
|
||||
(BoolAnd("boolean_field", default=False), False),
|
||||
(BoolOr("boolean_field", default=False), False),
|
||||
(JSONBAgg("integer_field", default=Value('["<empty>"]')), ["<empty>"]),
|
||||
(JSONBAgg("integer_field", default=["<empty>"]), ["<empty>"]),
|
||||
(
|
||||
JSONBAgg("integer_field", default=Value(["<empty>"], JSONField())),
|
||||
["<empty>"],
|
||||
),
|
||||
(StringAgg("char_field", delimiter=";", default="<empty>"), "<empty>"),
|
||||
(
|
||||
StringAgg("char_field", delimiter=";", default=Value("<empty>")),
|
||||
|
@ -189,9 +194,7 @@ class TestGeneralAggregate(PostgreSQLTestCase):
|
|||
{"aggregation": []},
|
||||
)
|
||||
self.assertEqual(
|
||||
queryset.aggregate(
|
||||
aggregation=JSONBAgg("integer_field", default=Value("[]"))
|
||||
),
|
||||
queryset.aggregate(aggregation=JSONBAgg("integer_field", default=[])),
|
||||
{"aggregation": []},
|
||||
)
|
||||
self.assertEqual(
|
||||
|
@ -201,6 +204,59 @@ class TestGeneralAggregate(PostgreSQLTestCase):
|
|||
{"aggregation": ""},
|
||||
)
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
||||
def test_jsonb_agg_default_str_value(self):
|
||||
AggregateTestModel.objects.all().delete()
|
||||
queryset = AggregateTestModel.objects.all()
|
||||
self.assertEqual(
|
||||
queryset.aggregate(
|
||||
aggregation=JSONBAgg("integer_field", default=Value("<empty>"))
|
||||
),
|
||||
{"aggregation": "<empty>"},
|
||||
)
|
||||
|
||||
def test_jsonb_agg_default_str_value_deprecation(self):
|
||||
queryset = AggregateTestModel.objects.all()
|
||||
msg = (
|
||||
"Passing a Value() with an output_field that isn't a JSONField as "
|
||||
"JSONBAgg(default) is deprecated. Pass default=Value('<empty>', "
|
||||
"output_field=JSONField()) instead."
|
||||
)
|
||||
with self.assertWarnsMessage(RemovedInDjango51Warning, msg):
|
||||
queryset.aggregate(
|
||||
aggregation=JSONBAgg("integer_field", default=Value("<empty>"))
|
||||
)
|
||||
with self.assertWarnsMessage(RemovedInDjango51Warning, msg):
|
||||
queryset.none().aggregate(
|
||||
aggregation=JSONBAgg("integer_field", default=Value("<empty>"))
|
||||
),
|
||||
|
||||
@ignore_warnings(category=RemovedInDjango51Warning)
|
||||
def test_jsonb_agg_default_encoded_json_string(self):
|
||||
AggregateTestModel.objects.all().delete()
|
||||
queryset = AggregateTestModel.objects.all()
|
||||
self.assertEqual(
|
||||
queryset.aggregate(
|
||||
aggregation=JSONBAgg("integer_field", default=Value("[]"))
|
||||
),
|
||||
{"aggregation": []},
|
||||
)
|
||||
|
||||
def test_jsonb_agg_default_encoded_json_string_deprecation(self):
|
||||
queryset = AggregateTestModel.objects.all()
|
||||
msg = (
|
||||
"Passing an encoded JSON string as JSONBAgg(default) is deprecated. Pass "
|
||||
"default=[] instead."
|
||||
)
|
||||
with self.assertWarnsMessage(RemovedInDjango51Warning, msg):
|
||||
queryset.aggregate(
|
||||
aggregation=JSONBAgg("integer_field", default=Value("[]"))
|
||||
)
|
||||
with self.assertWarnsMessage(RemovedInDjango51Warning, msg):
|
||||
queryset.none().aggregate(
|
||||
aggregation=JSONBAgg("integer_field", default=Value("[]"))
|
||||
)
|
||||
|
||||
def test_array_agg_charfield(self):
|
||||
values = AggregateTestModel.objects.aggregate(arrayagg=ArrayAgg("char_field"))
|
||||
self.assertEqual(values, {"arrayagg": ["Foo1", "Foo2", "Foo4", "Foo3"]})
|
||||
|
|
Loading…
Reference in New Issue