summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2022-11-02 22:03:05 -0400
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-12-01 19:14:00 +0100
commit0ff46591ac3010841c73fd26e0fef93995fedd99 (patch)
tree7373b7f028df958db09d353e603124bc52b37bed /django
parentd3e746ace5eeea07216da97d9c3801f2fdc43223 (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.py52
-rw-r--r--django/db/models/aggregates.py2
-rw-r--r--django/db/models/fields/__init__.py4
-rw-r--r--django/db/models/fields/json.py31
-rw-r--r--django/db/models/sql/compiler.py12
-rw-r--r--django/db/models/sql/query.py6
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))