diff options
| author | Simon Charette <charette.s@gmail.com> | 2022-11-02 22:03:05 -0400 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-12-01 19:14:00 +0100 |
| commit | 0ff46591ac3010841c73fd26e0fef93995fedd99 (patch) | |
| tree | 7373b7f028df958db09d353e603124bc52b37bed /django | |
| parent | d3e746ace5eeea07216da97d9c3801f2fdc43223 (diff) | |
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.
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/postgres/aggregates/general.py | 52 | ||||
| -rw-r--r-- | django/db/models/aggregates.py | 2 | ||||
| -rw-r--r-- | django/db/models/fields/__init__.py | 4 | ||||
| -rw-r--r-- | django/db/models/fields/json.py | 31 | ||||
| -rw-r--r-- | django/db/models/sql/compiler.py | 12 | ||||
| -rw-r--r-- | django/db/models/sql/query.py | 6 |
6 files changed, 87 insertions, 20 deletions
diff --git a/django/contrib/postgres/aggregates/general.py b/django/contrib/postgres/aggregates/general.py index f8b40fb709..3de59dfcfd 100644 --- a/django/contrib/postgres/aggregates/general.py +++ b/django/contrib/postgres/aggregates/general.py @@ -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): diff --git a/django/db/models/aggregates.py b/django/db/models/aggregates.py index e2e0ef762b..e672f0aeb0 100644 --- a/django/db/models/aggregates.py +++ b/django/db/models/aggregates.py @@ -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. diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 2a98396cad..cd995de80f 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -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 ) diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py index c0242bd7be..eb2d35f100 100644 --- a/django/db/models/fields/json.py +++ b/django/db/models/fields/json.py @@ -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) diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index caf36382b5..2b66ab12b4 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -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"): diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 521054f69e..9e70aadca1 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -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)) |
