1
0
mirror of https://github.com/django/django.git synced 2025-04-05 22:16:41 +00:00

Refs #36042 -- Raised ValueError when providing composite expressions to aggregates.

This commit is contained in:
Jacob Walls 2025-01-05 10:09:46 -05:00 committed by Sarah Boyce
parent 6eec703667
commit 470e5545e5
8 changed files with 39 additions and 5 deletions

View File

@ -166,6 +166,7 @@ class Count(Aggregate):
output_field = IntegerField()
allow_distinct = True
empty_result_set_value = 0
allows_composite_expressions = True
def __init__(self, expression, filter=None, **extra):
if expression == "*":

View File

@ -184,6 +184,8 @@ class BaseExpression:
constraint_validation_compatible = True
# Does the expression possibly return more than one row?
set_returning = False
# Does the expression allow composite expressions?
allows_composite_expressions = False
def __init__(self, output_field=None):
if output_field is not None:
@ -1077,6 +1079,12 @@ class Func(SQLiteNumericMixin, Expression):
c.source_expressions[pos] = arg.resolve_expression(
query, allow_joins, reuse, summarize, for_save
)
if not self.allows_composite_expressions and any(
isinstance(expr, ColPairs) for expr in c.get_source_expressions()
):
raise ValueError(
f"{self.__class__.__name__} does not support composite primary keys."
)
return c
def as_sql(
@ -1827,6 +1835,7 @@ class OrderBy(Expression):
template = "%(expression)s %(ordering)s"
conditional = False
constraint_validation_compatible = False
allows_composite_expressions = True
def __init__(self, expression, descending=False, nulls_first=None, nulls_last=None):
if nulls_first and nulls_last:

View File

@ -17,6 +17,7 @@ from django.db.models.sql.where import AND, OR, WhereNode
class Tuple(Func):
allows_composite_expressions = True
function = ""
output_field = Field()
@ -28,6 +29,8 @@ class Tuple(Func):
class TupleLookupMixin:
allows_composite_expressions = True
def get_prep_lookup(self):
self.check_rhs_is_tuple_or_list()
self.check_rhs_length_equals_lhs_length()

View File

@ -1105,6 +1105,14 @@ calling the appropriate methods on the wrapped expression.
``UNNEST``, etc.) to skip optimization and be properly evaluated when
annotations spawn rows themselves. Defaults to ``False``.
.. attribute:: allows_composite_expressions
.. versionadded:: 5.2
Tells Django that this expression allows composite expressions, for
example, to support :ref:`composite primary keys
<cpk-and-database-functions>`. Defaults to ``False``.
.. method:: resolve_expression(query=None, allow_joins=True, reuse=None, summarize=False, for_save=False)
Provides the chance to do any preprocessing or validation of

View File

@ -330,6 +330,10 @@ Models
accepts a list of field names or expressions and returns a JSON array
containing those values.
* The new :attr:`.Expression.allows_composite_expressions` attribute specifies
that the expression allows composite expressions, for example, to support
:ref:`composite primary keys <cpk-and-database-functions>`.
Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~

View File

@ -131,6 +131,8 @@ database.
``ForeignObject`` is an internal API. This means it is not covered by our
:ref:`deprecation policy <internal-release-deprecation-policy>`.
.. _cpk-and-database-functions:
Composite primary keys and database functions
=============================================
@ -141,13 +143,15 @@ Many database functions only accept a single expression.
MAX("order_id") -- OK
MAX("product_id", "order_id") -- ERROR
As a consequence, they cannot be used with composite primary key references as
they are composed of multiple column expressions.
In these cases, providing a composite primary key reference raises a
``ValueError``, since it is composed of multiple column expressions. An
exception is made for ``Count``.
.. code-block:: python
Max("order_id") # OK
Max("pk") # ERROR
Max("pk") # ValueError
Count("pk") # OK
Composite primary keys in forms
===============================

View File

@ -1,4 +1,4 @@
from django.db.models import Count, Q
from django.db.models import Count, Max, Q
from django.test import TestCase
from .models import Comment, Tenant, User
@ -136,3 +136,8 @@ class CompositePKAggregateTests(TestCase):
),
(self.user_3, self.user_1, self.user_2),
)
def test_max_pk(self):
msg = "Max does not support composite primary keys."
with self.assertRaisesMessage(ValueError, msg):
Comment.objects.aggregate(Max("pk"))

View File

@ -428,6 +428,6 @@ class CompositePKFilterTests(TestCase):
self.assertSequenceEqual(queryset, (self.user_2,))
def test_cannot_cast_pk(self):
msg = "Casting CompositePrimaryKey is not supported."
msg = "Cast does not support composite primary keys."
with self.assertRaisesMessage(ValueError, msg):
Comment.objects.filter(text__gt=Cast(F("pk"), TextField())).count()