Skip to content

Commit 83bae77

Browse files
committed
Merge remote-tracking branch 'upstream/main'
2 parents eea2154 + dca8284 commit 83bae77

File tree

16 files changed

+253
-28
lines changed

16 files changed

+253
-28
lines changed

django/contrib/gis/db/models/lookups.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def process_band_indices(self, only_lhs=False):
5555

5656
def get_db_prep_lookup(self, value, connection):
5757
# get_db_prep_lookup is called by process_rhs from super class
58-
return ("%s", [connection.ops.Adapter(value)])
58+
return ("%s", (connection.ops.Adapter(value),))
5959

6060
def process_rhs(self, compiler, connection):
6161
if isinstance(self.rhs, Query):
@@ -284,7 +284,7 @@ def process_rhs(self, compiler, connection):
284284
elif not isinstance(pattern, str) or not self.pattern_regex.match(pattern):
285285
raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
286286
sql, params = super().process_rhs(compiler, connection)
287-
return sql, [*params, pattern]
287+
return sql, (*params, pattern)
288288

289289

290290
@BaseSpatialField.register_lookup
@@ -352,7 +352,7 @@ def process_rhs(self, compiler, connection):
352352
dist_sql, dist_params = self.process_distance(compiler, connection)
353353
self.template_params["value"] = dist_sql
354354
rhs_sql, params = super().process_rhs(compiler, connection)
355-
return rhs_sql, params + dist_params
355+
return rhs_sql, (*params, *dist_params)
356356

357357

358358
class DistanceLookupFromFunction(DistanceLookupBase):
@@ -367,7 +367,7 @@ def as_sql(self, compiler, connection):
367367
dist_sql, dist_params = self.process_distance(compiler, connection)
368368
return (
369369
"%(func)s %(op)s %(dist)s" % {"func": sql, "op": self.op, "dist": dist_sql},
370-
params + dist_params,
370+
(*params, *dist_params),
371371
)
372372

373373

django/contrib/postgres/search.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def process_rhs(self, qn, connection):
2727
def as_sql(self, qn, connection):
2828
lhs, lhs_params = self.process_lhs(qn, connection)
2929
rhs, rhs_params = self.process_rhs(qn, connection)
30-
params = lhs_params + rhs_params
30+
params = (*lhs_params, *rhs_params)
3131
return "%s @@ %s" % (lhs, rhs), params
3232

3333

@@ -148,7 +148,7 @@ def as_sql(self, compiler, connection, function=None, template=None):
148148
weight_sql, extra_params = compiler.compile(clone.weight)
149149
sql = "setweight({}, {})".format(sql, weight_sql)
150150

151-
return sql, config_params + params + extra_params
151+
return sql, (*config_params, *params, *extra_params)
152152

153153

154154
class CombinedSearchVector(SearchVectorCombinable, CombinedExpression):
@@ -318,13 +318,13 @@ def __init__(
318318

319319
def as_sql(self, compiler, connection, function=None, template=None):
320320
options_sql = ""
321-
options_params = []
321+
options_params = ()
322322
if self.options:
323-
options_params.append(
323+
options_params = (
324324
", ".join(
325325
connection.ops.compose_sql(f"{option}=%s", [value])
326326
for option, value in self.options.items()
327-
)
327+
),
328328
)
329329
options_sql = ", %s"
330330
sql, params = super().as_sql(

django/db/backends/base/schema.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import operator
33
from datetime import datetime
4+
from itertools import chain
45

56
from django.conf import settings
67
from django.core.exceptions import FieldError
@@ -1160,7 +1161,7 @@ def _alter_field(
11601161
# Combine actions together if we can (e.g. postgres)
11611162
if self.connection.features.supports_combined_alters and actions:
11621163
sql, params = tuple(zip(*actions))
1163-
actions = [(", ".join(sql), sum(params, []))]
1164+
actions = [(", ".join(sql), tuple(chain(*params)))]
11641165
# Apply those actions
11651166
for sql, params in actions:
11661167
self.execute(

django/db/models/expressions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,7 +1127,7 @@ def as_sql(
11271127
template = template or data.get("template", self.template)
11281128
arg_joiner = arg_joiner or data.get("arg_joiner", self.arg_joiner)
11291129
data["expressions"] = data["field"] = arg_joiner.join(sql_parts)
1130-
return template % data, params
1130+
return template % data, tuple(params)
11311131

11321132
def copy(self):
11331133
copy = super().copy()
@@ -1323,7 +1323,7 @@ def as_sql(self, compiler, connection):
13231323
alias, column = self.alias, self.target.column
13241324
identifiers = (alias, column) if alias else (column,)
13251325
sql = ".".join(map(compiler.quote_name_unless_alias, identifiers))
1326-
return sql, []
1326+
return sql, ()
13271327

13281328
def relabeled_clone(self, relabels):
13291329
if self.alias is None:

django/db/models/functions/comparison.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def as_sqlite(self, compiler, connection, **extra_context):
2626
compiler, connection, template=template, **extra_context
2727
)
2828
format_string = "%H:%M:%f" if db_type == "time" else "%Y-%m-%d %H:%M:%f"
29-
params.insert(0, format_string)
29+
params = (format_string, *params)
3030
return sql, params
3131
elif db_type == "date":
3232
template = "date(%(expressions)s)"

django/db/models/lookups.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def get_prep_lhs(self):
101101
return Value(self.lhs)
102102

103103
def get_db_prep_lookup(self, value, connection):
104-
return ("%s", [value])
104+
return ("%s", (value,))
105105

106106
def process_lhs(self, compiler, connection, lhs=None):
107107
lhs = lhs or self.lhs
@@ -415,7 +415,7 @@ class IExact(BuiltinLookup):
415415
def process_rhs(self, qn, connection):
416416
rhs, params = super().process_rhs(qn, connection)
417417
if params:
418-
params[0] = connection.ops.prep_for_iexact_query(params[0])
418+
params = (connection.ops.prep_for_iexact_query(params[0]), *params[1:])
419419
return rhs, params
420420

421421

@@ -603,8 +603,9 @@ def get_rhs_op(self, connection, rhs):
603603
def process_rhs(self, qn, connection):
604604
rhs, params = super().process_rhs(qn, connection)
605605
if self.rhs_is_direct_value() and params and not self.bilateral_transforms:
606-
params[0] = self.param_pattern % connection.ops.prep_for_like_query(
607-
params[0]
606+
params = (
607+
self.param_pattern % connection.ops.prep_for_like_query(params[0]),
608+
*params[1:],
608609
)
609610
return rhs, params
610611

@@ -686,8 +687,9 @@ def as_sql(self, compiler, connection):
686687
else:
687688
lhs, lhs_params = self.process_lhs(compiler, connection)
688689
rhs, rhs_params = self.process_rhs(compiler, connection)
690+
params = (*lhs_params, *rhs_params)
689691
sql_template = connection.ops.regex_lookup(self.lookup_name)
690-
return sql_template % (lhs, rhs), lhs_params + rhs_params
692+
return sql_template % (lhs, rhs), params
691693

692694

693695
@Field.register_lookup

django/db/models/query_utils.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from contextlib import nullcontext
1414

1515
from django.core.exceptions import FieldError
16-
from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections, transaction
16+
from django.db import DEFAULT_DB_ALIAS, DatabaseError, connections, models, transaction
1717
from django.db.models.constants import LOOKUP_SEP
1818
from django.utils import tree
1919
from django.utils.functional import cached_property
@@ -99,6 +99,45 @@ def resolve_expression(
9999
query.promote_joins(joins)
100100
return clause
101101

102+
def replace_expressions(self, replacements):
103+
if not replacements:
104+
return self
105+
clone = self.create(connector=self.connector, negated=self.negated)
106+
for child in self.children:
107+
child_replacement = child
108+
if isinstance(child, tuple):
109+
lhs, rhs = child
110+
if LOOKUP_SEP in lhs:
111+
path, lookup = lhs.rsplit(LOOKUP_SEP, 1)
112+
else:
113+
path = lhs
114+
lookup = None
115+
field = models.F(path)
116+
if (
117+
field_replacement := field.replace_expressions(replacements)
118+
) is not field:
119+
# Handle the implicit __exact case by falling back to an
120+
# extra transform when get_lookup returns no match for the
121+
# last component of the path.
122+
if lookup is None:
123+
lookup = "exact"
124+
if (lookup_class := field_replacement.get_lookup(lookup)) is None:
125+
if (
126+
transform_class := field_replacement.get_transform(lookup)
127+
) is not None:
128+
field_replacement = transform_class(field_replacement)
129+
lookup = "exact"
130+
lookup_class = field_replacement.get_lookup(lookup)
131+
if rhs is None and lookup == "exact":
132+
lookup_class = field_replacement.get_lookup("isnull")
133+
rhs = True
134+
if lookup_class is not None:
135+
child_replacement = lookup_class(field_replacement, rhs)
136+
else:
137+
child_replacement = child.replace_expressions(replacements)
138+
clone.children.append(child_replacement)
139+
return clone
140+
102141
def flatten(self):
103142
"""
104143
Recursively yield this Q object and all subexpressions, in depth-first

docs/ref/models/expressions.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,8 @@ calling the appropriate methods on the wrapped expression.
12021202
:meth:`~django.db.models.query.QuerySet.reverse()` is called on a
12031203
queryset.
12041204

1205+
.. _writing-your-own-query-expressions:
1206+
12051207
Writing your own Query Expressions
12061208
----------------------------------
12071209

@@ -1262,7 +1264,7 @@ Next, we write the method responsible for generating the SQL::
12621264
sql_params.extend(params)
12631265
template = template or self.template
12641266
data = {"expressions": ",".join(sql_expressions)}
1265-
return template % data, sql_params
1267+
return template % data, tuple(sql_params)
12661268

12671269

12681270
def as_oracle(self, compiler, connection):

docs/releases/5.2.5.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ Bugfixes
1515

1616
* Fixed a crash in Django 5.2 when filtering against a composite primary key
1717
using a tuple containing expressions (:ticket:`36522`).
18+
19+
* Fixed a crash in Django 5.2 when validating a model that uses
20+
``GeneratedField`` or constraints composed of ``Q`` and ``Case`` lookups
21+
(:ticket:`36518`).

docs/releases/6.0.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,24 @@ Email
427427
significantly, closely examine any custom subclasses that rely on overriding
428428
undocumented, internal underscore methods.
429429

430+
Custom ORM expressions should return params as a tuple
431+
------------------------------------------------------
432+
433+
Prior to Django 6.0, :doc:`custom lookups </howto/custom-lookups>` and
434+
:ref:`custom expressions <writing-your-own-query-expressions>` implementing the
435+
``as_sql()`` method (and its supporting methods ``process_lhs()`` and
436+
``process_rhs()``) were allowed to return a sequence of params in either a list
437+
or a tuple. To address the interoperability problems that resulted, the second
438+
return element of the ``as_sql()`` method should now be a tuple::
439+
440+
def as_sql(self, compiler, connection) -> tuple[str, tuple]: ...
441+
442+
If your custom expressions support multiple versions of Django, you should
443+
adjust any pre-processing of parameters to be resilient against either tuples
444+
or lists. For instance, prefer unpacking like this::
445+
446+
params = (*lhs_params, *rhs_params)
447+
430448
Miscellaneous
431449
-------------
432450

0 commit comments

Comments
 (0)