summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/contrib/gis/db/backends/base/operations.py32
-rw-r--r--django/contrib/gis/db/backends/oracle/operations.py6
-rw-r--r--django/contrib/gis/db/backends/postgis/operations.py18
-rw-r--r--django/contrib/gis/db/models/fields.py4
-rw-r--r--django/contrib/gis/db/models/lookups.py10
-rw-r--r--django/contrib/postgres/fields/array.py8
-rw-r--r--django/contrib/postgres/fields/ranges.py8
-rw-r--r--django/db/backends/base/operations.py6
-rw-r--r--django/db/backends/mysql/operations.py10
-rw-r--r--django/db/backends/postgresql/compiler.py4
-rw-r--r--django/db/models/expressions.py8
-rw-r--r--django/db/models/fields/__init__.py31
-rw-r--r--django/db/models/sql/compiler.py46
-rw-r--r--docs/internals/deprecation.txt3
-rw-r--r--docs/releases/6.1.txt17
-rw-r--r--tests/model_fields/tests.py39
-rw-r--r--tests/postgres_tests/fields.py8
17 files changed, 181 insertions, 77 deletions
diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py
index e468c6f610..3ff13dfc86 100644
--- a/django/contrib/gis/db/backends/base/operations.py
+++ b/django/contrib/gis/db/backends/base/operations.py
@@ -115,7 +115,7 @@ class BaseSpatialOperations:
"Distance operations not available on this spatial backend."
)
- def get_geom_placeholder(self, f, value, compiler):
+ def get_geom_placeholder_sql(self, f, value, compiler):
"""
Return the placeholder for the given geometry field with the given
value. Depending on the spatial backend, the placeholder may contain a
@@ -123,28 +123,26 @@ class BaseSpatialOperations:
backend.
"""
- def transform_value(value, field):
+ def must_transform_value(value, field):
return value is not None and value.srid != field.srid
if hasattr(value, "as_sql"):
- return (
- "%s(%%s, %s)" % (self.spatial_function_name("Transform"), f.srid)
- if transform_value(value.output_field, f)
- else "%s"
- )
- if transform_value(value, f):
- # Add Transform() to the SQL placeholder.
- return "%s(%s(%%s,%s), %s)" % (
- self.spatial_function_name("Transform"),
- self.from_text,
- value.srid,
- f.srid,
- )
+ sql, params = compiler.compile(value)
+ if must_transform_value(value.output_field, f):
+ transform_func = self.spatial_function_name("Transform")
+ sql = f"{transform_func}({sql}, %s)"
+ params = (*params, f.srid)
+ return sql, params
+ elif must_transform_value(value, f):
+ transform_func = self.spatial_function_name("Transform")
+ sql = f"{transform_func}({self.from_text}(%s, %s), %s)"
+ params = (value, value.srid, f.srid)
+ return sql, params
elif self.connection.features.has_spatialrefsys_table:
- return "%s(%%s,%s)" % (self.from_text, f.srid)
+ return f"{self.from_text}(%s, %s)", (value, f.srid)
else:
# For backwards compatibility on MySQL (#27464).
- return "%s(%%s)" % self.from_text
+ return f"{self.from_text}(%s)", (value,)
def check_expression_support(self, expression):
if isinstance(expression, self.disallowed_aggregates):
diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py
index 1c09281809..918ab7ce64 100644
--- a/django/contrib/gis/db/backends/oracle/operations.py
+++ b/django/contrib/gis/db/backends/oracle/operations.py
@@ -204,10 +204,10 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
return [dist_param]
- def get_geom_placeholder(self, f, value, compiler):
+ def get_geom_placeholder_sql(self, f, value, compiler):
if value is None:
- return "NULL"
- return super().get_geom_placeholder(f, value, compiler)
+ return "NULL", ()
+ return super().get_geom_placeholder_sql(f, value, compiler)
def spatial_aggregate_name(self, agg_name):
"""
diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
index f4e70c9204..3c5ea7687f 100644
--- a/django/contrib/gis/db/backends/postgis/operations.py
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -304,7 +304,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
return [dist_param]
- def get_geom_placeholder(self, f, value, compiler):
+ def get_geom_placeholder_sql(self, f, value, compiler):
"""
Provide a proper substitution value for Geometries or rasters that are
not in the SRID of the field. Specifically, this routine will
@@ -312,11 +312,11 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
"""
transform_func = self.spatial_function_name("Transform")
if hasattr(value, "as_sql"):
- if value.field.srid == f.srid:
- placeholder = "%s"
- else:
- placeholder = "%s(%%s, %s)" % (transform_func, f.srid)
- return placeholder
+ sql, params = compiler.compile(value)
+ if value.field.srid != f.srid:
+ sql = f"{transform_func}({sql}, %s)"
+ params = (*params, f.srid)
+ return sql, params
# Get the srid for this object
if value is None:
@@ -327,11 +327,9 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
# Adding Transform() to the SQL placeholder if the value srid
# is not equal to the field srid.
if value_srid is None or value_srid == f.srid:
- placeholder = "%s"
+ return "%s", (value,)
else:
- placeholder = "%s(%%s, %s)" % (transform_func, f.srid)
-
- return placeholder
+ return f"{transform_func}(%s, %s)", (value, f.srid)
def _get_postgis_func(self, func):
"""
diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py
index d1c1a5937e..14076d305b 100644
--- a/django/contrib/gis/db/models/fields.py
+++ b/django/contrib/gis/db/models/fields.py
@@ -135,12 +135,12 @@ class BaseSpatialField(Field):
"""
return get_srid_info(self.srid, connection).geodetic
- def get_placeholder(self, value, compiler, connection):
+ def get_placeholder_sql(self, value, compiler, connection):
"""
Return the placeholder for the spatial column for the
given value.
"""
- return connection.ops.get_geom_placeholder(self, value, compiler)
+ return connection.ops.get_geom_placeholder_sql(self, value, compiler)
def get_srid(self, obj):
"""
diff --git a/django/contrib/gis/db/models/lookups.py b/django/contrib/gis/db/models/lookups.py
index c0cb80ea70..3c2f4c3be4 100644
--- a/django/contrib/gis/db/models/lookups.py
+++ b/django/contrib/gis/db/models/lookups.py
@@ -62,12 +62,12 @@ class GISLookup(Lookup):
# If rhs is some Query, don't touch it.
return super().process_rhs(compiler, connection)
if isinstance(self.rhs, Expression):
- self.rhs = self.rhs.resolve_expression(compiler.query)
- rhs, rhs_params = super().process_rhs(compiler, connection)
- placeholder = connection.ops.get_geom_placeholder(
- self.lhs.output_field, self.rhs, compiler
+ rhs = self.rhs.resolve_expression(compiler.query)
+ else:
+ rhs = connection.ops.Adapter(self.rhs)
+ return connection.ops.get_geom_placeholder_sql(
+ self.lhs.output_field, rhs, compiler
)
- return placeholder % rhs, rhs_params
def get_rhs_op(self, connection, rhs):
# Unlike BuiltinLookup, the GIS get_rhs_op() implementation should
diff --git a/django/contrib/postgres/fields/array.py b/django/contrib/postgres/fields/array.py
index f867571215..58341e02ab 100644
--- a/django/contrib/postgres/fields/array.py
+++ b/django/contrib/postgres/fields/array.py
@@ -124,8 +124,12 @@ class ArrayField(CheckPostgresInstalledMixin, CheckFieldDefaultMixin, Field):
db_params["collation"] = self.db_collation
return db_params
- def get_placeholder(self, value, compiler, connection):
- return "%s::{}".format(self.db_type(connection))
+ def get_placeholder_sql(self, value, compiler, connection):
+ db_type = self.db_type(connection)
+ if hasattr(value, "as_sql"):
+ sql, params = compiler.compile(value)
+ return f"{sql}::{db_type}", params
+ return f"%s::{db_type}", (value,)
def get_db_prep_value(self, value, connection, prepared=False):
if isinstance(value, (list, tuple)):
diff --git a/django/contrib/postgres/fields/ranges.py b/django/contrib/postgres/fields/ranges.py
index 33c6611f86..22a56a3633 100644
--- a/django/contrib/postgres/fields/ranges.py
+++ b/django/contrib/postgres/fields/ranges.py
@@ -84,8 +84,12 @@ class RangeField(CheckPostgresInstalledMixin, models.Field):
def _choices_is_value(cls, value):
return isinstance(value, (list, tuple)) or super()._choices_is_value(value)
- def get_placeholder(self, value, compiler, connection):
- return "%s::{}".format(self.db_type(connection))
+ def get_placeholder_sql(self, value, compiler, connection):
+ db_type = self.db_type(connection)
+ if hasattr(value, "as_sql"):
+ sql, params = compiler.compile(value)
+ return f"{sql}::{db_type}", params
+ return f"%s::{db_type}", (value,)
def get_prep_value(self, value):
if value is None:
diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py
index bc1752df6f..29ef7d93d1 100644
--- a/django/db/backends/base/operations.py
+++ b/django/db/backends/base/operations.py
@@ -734,12 +734,14 @@ class BaseDatabaseOperations:
def combine_duration_expression(self, connector, sub_expressions):
return self.combine_expression(connector, sub_expressions)
- def binary_placeholder_sql(self, value):
+ def binary_placeholder_sql(self, value, compiler):
"""
Some backends require special syntax to insert binary content (MySQL
for example uses '_binary %s').
"""
- return "%s"
+ if hasattr(value, "as_sql"):
+ return compiler.compile(value)
+ return "%s", (value,)
def modify_insert_params(self, placeholder, params):
"""
diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py
index 74ba72f316..61fc9da3f4 100644
--- a/django/db/backends/mysql/operations.py
+++ b/django/db/backends/mysql/operations.py
@@ -306,10 +306,12 @@ class DatabaseOperations(BaseDatabaseOperations):
value = uuid.UUID(value)
return value
- def binary_placeholder_sql(self, value):
- return (
- "_binary %s" if value is not None and not hasattr(value, "as_sql") else "%s"
- )
+ def binary_placeholder_sql(self, value, compiler):
+ if value is None:
+ return "%s", (None,)
+ elif hasattr(value, "as_sql"):
+ return compiler.compile(value)
+ return "_binary %s", (value,)
def subtract_temporals(self, internal_type, lhs, rhs):
lhs_sql, lhs_params = lhs
diff --git a/django/db/backends/postgresql/compiler.py b/django/db/backends/postgresql/compiler.py
index 9d383b52e8..ad8359bfaf 100644
--- a/django/db/backends/postgresql/compiler.py
+++ b/django/db/backends/postgresql/compiler.py
@@ -36,11 +36,11 @@ class SQLInsertCompiler(BaseSQLInsertCompiler):
# Lack of fields denote the usage of the DEFAULT keyword
# for the insertion of empty rows.
or any(field is None for field in fields)
- # Field.get_placeholder takes value as an argument, so the
+ # Field.get_placeholder_sql takes value as an argument, so the
# resulting placeholder might be dependent on the value.
# in UNNEST requires a single placeholder to "fit all values" in
# the array.
- or any(hasattr(field, "get_placeholder") for field in fields)
+ or any(hasattr(field, "get_placeholder_sql") for field in fields)
# Fields that don't use standard internal types might not be
# unnest'able (e.g. array and geometry types are known to be
# problematic).
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 9c734c60a1..c6ba5c89a9 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -1177,8 +1177,12 @@ class Value(Expression):
val = output_field.get_db_prep_save(val, connection=connection)
else:
val = output_field.get_db_prep_value(val, connection=connection)
- if hasattr(output_field, "get_placeholder"):
- return output_field.get_placeholder(val, compiler, connection), [val]
+ try:
+ get_placeholder_sql = output_field.get_placeholder_sql
+ except AttributeError:
+ pass
+ else:
+ return get_placeholder_sql(val, compiler, connection)
if val is None:
# oracledb does not always convert None to the appropriate
# NULL type (like in case expressions using numbers), so we
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index feb0441bab..d98319ef00 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -30,6 +30,7 @@ from django.utils.dateparse import (
parse_duration,
parse_time,
)
+from django.utils.deprecation import RemovedInDjango70Warning, django_file_prefixes
from django.utils.duration import duration_string
from django.utils.functional import Promise, cached_property
from django.utils.ipv6 import MAX_IPV6_ADDRESS_LENGTH, clean_ipv6_address
@@ -181,6 +182,32 @@ class Field(RegisterLookupMixin):
description = property(_description)
+ def __init_subclass__(cls, **kwargs):
+ # RemovedInDjango70Warning: When the deprecation ends, remove
+ # completely.
+ # Allow for both `get_placeholder` and `get_placeholder_sql` to
+ # be declared to ease the deprecation process for third-party apps.
+ if (
+ get_placeholder := cls.__dict__.get("get_placeholder")
+ ) is not None and "get_placeholder_sql" not in cls.__dict__:
+ warnings.warn(
+ "Field.get_placeholder is deprecated in favor of get_placeholder_sql. "
+ f"Define {cls.__module__}.{cls.__qualname__}.get_placeholder_sql "
+ "to return both SQL and parameters instead.",
+ category=RemovedInDjango70Warning,
+ skip_file_prefixes=django_file_prefixes(),
+ )
+
+ def get_placeholder_sql(self, value, compiler, connection):
+ placeholder = get_placeholder(self, value, compiler, connection)
+ if hasattr(value, "as_sql"):
+ sql, params = compiler.compile(value)
+ return placeholder % sql, params
+ return placeholder, (value,)
+
+ setattr(cls, "get_placeholder_sql", get_placeholder_sql)
+ return super().__init_subclass__(**kwargs)
+
def __init__(
self,
verbose_name=None,
@@ -2735,8 +2762,8 @@ class BinaryField(Field):
def get_internal_type(self):
return "BinaryField"
- def get_placeholder(self, value, compiler, connection):
- return connection.ops.binary_placeholder_sql(value)
+ def get_placeholder_sql(self, value, compiler, connection):
+ return connection.ops.binary_placeholder_sql(value, compiler)
def get_default(self):
if self.has_default() and not callable(self.default):
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index 8742de00d6..6c758fb526 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -1690,11 +1690,12 @@ class SQLInsertCompiler(SQLCompiler):
returning_fields = None
returning_params = ()
- def field_as_sql(self, field, get_placeholder, val):
+ def field_as_sql(self, field, get_placeholder_sql, val):
"""
Take a field and a value intended to be saved on that field, and
return placeholder SQL and accompanying params. Check for raw values,
- expressions, and fields with get_placeholder() defined in that order.
+ fields with get_placeholder_sql(), and compilable defined in that
+ order.
When field is None, consider the value raw and use it as the
placeholder, with no corresponding parameters returned.
@@ -1702,13 +1703,13 @@ class SQLInsertCompiler(SQLCompiler):
if field is None:
# A field value of None means the value is raw.
sql, params = val, []
+ elif get_placeholder_sql is not None:
+ # Some fields (e.g. geo fields) need special munging before
+ # they can be inserted.
+ sql, params = get_placeholder_sql(val, self, self.connection)
elif hasattr(val, "as_sql"):
# This is an expression, let's compile it.
sql, params = self.compile(val)
- elif get_placeholder is not None:
- # Some fields (e.g. geo fields) need special munging before
- # they can be inserted.
- sql, params = get_placeholder(val, self, self.connection), [val]
else:
# Return the common case for the placeholder
sql, params = "%s", [val]
@@ -1777,11 +1778,15 @@ class SQLInsertCompiler(SQLCompiler):
# list of (sql, [params]) tuples for each object to be saved
# Shape: [n_objs][n_fields][2]
- get_placeholders = [getattr(field, "get_placeholder", None) for field in fields]
+ get_placeholder_sqls = [
+ getattr(field, "get_placeholder_sql", None) for field in fields
+ ]
rows_of_fields_as_sql = (
(
- self.field_as_sql(field, get_placeholder, value)
- for field, get_placeholder, value in zip(fields, get_placeholders, row)
+ self.field_as_sql(field, get_placeholder_sql, value)
+ for field, get_placeholder_sql, value in zip(
+ fields, get_placeholder_sqls, row
+ )
)
for row in value_rows
)
@@ -2078,21 +2083,20 @@ class SQLUpdateCompiler(SQLCompiler):
)
val = field.get_db_prep_save(val, connection=self.connection)
- # Getting the placeholder for the field.
- if hasattr(field, "get_placeholder"):
- placeholder = field.get_placeholder(val, self, self.connection)
- else:
- placeholder = "%s"
- name = field.column
- if hasattr(val, "as_sql"):
+ quoted_name = qn(field.column)
+ if (
+ get_placeholder_sql := getattr(field, "get_placeholder_sql", None)
+ ) is not None:
+ sql, params = get_placeholder_sql(val, self, self.connection)
+ values.append(f"{quoted_name} = {sql}")
+ update_params.extend(params)
+ elif hasattr(val, "as_sql"):
sql, params = self.compile(val)
- values.append("%s = %s" % (qn(name), placeholder % sql))
+ values.append(f"{quoted_name} = {sql}")
update_params.extend(params)
- elif val is not None:
- values.append("%s = %s" % (qn(name), placeholder))
- update_params.append(val)
else:
- values.append("%s = NULL" % qn(name))
+ values.append(f"{quoted_name} = %s")
+ update_params.append(val)
table = self.query.base_table
result = [
"UPDATE %s SET" % qn(table),
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 280187b368..8962cbde62 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -64,6 +64,9 @@ details on these changes.
:class:`~django.db.models.JSONNull` to query for a JSON ``null`` value
instead.
+* The ``Field.get_placeholder_sql`` shim over the deprecated
+ ``get_placeholder`` method will be removed.
+
.. _deprecation-removed-in-6.1:
6.1
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 2539e599c7..67891c5bf7 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -396,6 +396,15 @@ backends.
* Set the new ``DatabaseFeatures.supports_inspectdb`` attribute to ``False``
if the management command isn't supported.
+* The ``DatabaseOperations.binary_placeholder_sql()`` method now expects a
+ query compiler as an extra positional argument and should return a
+ two-elements tuple composed of an SQL format string and a tuple of associated
+ parameters.
+
+* The ``BaseSpatialOperations.get_geom_placeholder()`` method is renamed to
+ ``get_geom_placeholder_sql`` and is expected to return a two-elements tuple
+ composed of an SQL format string and a tuple of associated parameters.
+
:mod:`django.contrib.admin`
---------------------------
@@ -481,6 +490,14 @@ Miscellaneous
used as the top-level value. :lookup:`Key and index lookups <jsonfield.key>`
are unaffected by this deprecation.
+* The undocumented ``get_placeholder`` method of
+ :class:`~django.db.models.Field` is deprecated in favor of the newly
+ introduced ``get_placeholder_sql`` method, which has the same input signature
+ but is expected to return a two-elements tuple composed of an SQL format
+ string and a tuple of associated parameters. This method should now expect
+ to be provided expressions meant to be compiled via the provided ``compiler``
+ argument.
+
Features removed in 6.1
=======================
diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py
index 3d856d36c5..b27c07a92f 100644
--- a/tests/model_fields/tests.py
+++ b/tests/model_fields/tests.py
@@ -1,10 +1,12 @@
import pickle
+import warnings
from django import forms
from django.core.exceptions import ValidationError
-from django.db import models
+from django.db import connection, models
from django.test import SimpleTestCase, TestCase
from django.utils.choices import CallableChoiceIterator
+from django.utils.deprecation import RemovedInDjango70Warning
from django.utils.functional import lazy
from .models import (
@@ -147,6 +149,41 @@ class BasicFieldTests(SimpleTestCase):
self.assertEqual(field_hash, hash(field))
+ def test_get_placeholder_deprecation(self):
+ # RemovedInDjango70Warning: When the deprecation ends, remove
+ # completely.
+ msg = (
+ "Field.get_placeholder is deprecated in favor of get_placeholder_sql. "
+ "Define model_fields.tests.BasicFieldTests."
+ "test_get_placeholder_deprecation.<locals>.SomeField.get_placeholder_sql "
+ "to return both SQL and parameters instead."
+ )
+ with self.assertWarnsMessage(RemovedInDjango70Warning, msg):
+
+ class SomeField(models.Field):
+ def get_placeholder(self, value, compiler, connection):
+ return "%s"
+
+ def test_get_placeholder_sql_shim(self):
+ # RemovedInDjango70Warning: When the deprecation ends, remove
+ # completely.
+ with warnings.catch_warnings(record=True):
+ warnings.simplefilter("always")
+
+ class SomeField(models.Field):
+ def get_placeholder(self, value, compiler, connection):
+ return "(1 + %s)"
+
+ compiler = Bar.objects.all().query.get_compiler(connection=connection)
+ self.assertEqual(
+ SomeField().get_placeholder_sql(2, compiler, connection),
+ ("(1 + %s)", (2,)),
+ )
+ self.assertEqual(
+ SomeField().get_placeholder_sql(models.Value(2), compiler, connection),
+ ("(1 + %s)", (2,)),
+ )
+
class ChoicesTests(SimpleTestCase):
@classmethod
diff --git a/tests/postgres_tests/fields.py b/tests/postgres_tests/fields.py
index d099effdd5..77370611ea 100644
--- a/tests/postgres_tests/fields.py
+++ b/tests/postgres_tests/fields.py
@@ -60,5 +60,9 @@ class EnumField(models.CharField):
class OffByOneField(models.IntegerField):
- def get_placeholder(self, value, compiler, connection):
- return "(%s + 1)"
+ def get_placeholder_sql(self, value, compiler, connection):
+ if hasattr(value, "as_sql"):
+ sql, params = compiler.compile(value)
+ else:
+ sql, params = "%s", (value,)
+ return f"({sql} + %s)", (*params, 1)