mirror of
https://github.com/django/django.git
synced 2025-06-07 12:39:12 +00:00
Refs #36042 -- Raised ValueError when providing composite expressions to aggregates.
This commit is contained in:
parent
6eec703667
commit
470e5545e5
@ -166,6 +166,7 @@ class Count(Aggregate):
|
|||||||
output_field = IntegerField()
|
output_field = IntegerField()
|
||||||
allow_distinct = True
|
allow_distinct = True
|
||||||
empty_result_set_value = 0
|
empty_result_set_value = 0
|
||||||
|
allows_composite_expressions = True
|
||||||
|
|
||||||
def __init__(self, expression, filter=None, **extra):
|
def __init__(self, expression, filter=None, **extra):
|
||||||
if expression == "*":
|
if expression == "*":
|
||||||
|
@ -184,6 +184,8 @@ class BaseExpression:
|
|||||||
constraint_validation_compatible = True
|
constraint_validation_compatible = True
|
||||||
# Does the expression possibly return more than one row?
|
# Does the expression possibly return more than one row?
|
||||||
set_returning = False
|
set_returning = False
|
||||||
|
# Does the expression allow composite expressions?
|
||||||
|
allows_composite_expressions = False
|
||||||
|
|
||||||
def __init__(self, output_field=None):
|
def __init__(self, output_field=None):
|
||||||
if output_field is not None:
|
if output_field is not None:
|
||||||
@ -1077,6 +1079,12 @@ class Func(SQLiteNumericMixin, Expression):
|
|||||||
c.source_expressions[pos] = arg.resolve_expression(
|
c.source_expressions[pos] = arg.resolve_expression(
|
||||||
query, allow_joins, reuse, summarize, for_save
|
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
|
return c
|
||||||
|
|
||||||
def as_sql(
|
def as_sql(
|
||||||
@ -1827,6 +1835,7 @@ class OrderBy(Expression):
|
|||||||
template = "%(expression)s %(ordering)s"
|
template = "%(expression)s %(ordering)s"
|
||||||
conditional = False
|
conditional = False
|
||||||
constraint_validation_compatible = False
|
constraint_validation_compatible = False
|
||||||
|
allows_composite_expressions = True
|
||||||
|
|
||||||
def __init__(self, expression, descending=False, nulls_first=None, nulls_last=None):
|
def __init__(self, expression, descending=False, nulls_first=None, nulls_last=None):
|
||||||
if nulls_first and nulls_last:
|
if nulls_first and nulls_last:
|
||||||
|
@ -17,6 +17,7 @@ from django.db.models.sql.where import AND, OR, WhereNode
|
|||||||
|
|
||||||
|
|
||||||
class Tuple(Func):
|
class Tuple(Func):
|
||||||
|
allows_composite_expressions = True
|
||||||
function = ""
|
function = ""
|
||||||
output_field = Field()
|
output_field = Field()
|
||||||
|
|
||||||
@ -28,6 +29,8 @@ class Tuple(Func):
|
|||||||
|
|
||||||
|
|
||||||
class TupleLookupMixin:
|
class TupleLookupMixin:
|
||||||
|
allows_composite_expressions = True
|
||||||
|
|
||||||
def get_prep_lookup(self):
|
def get_prep_lookup(self):
|
||||||
self.check_rhs_is_tuple_or_list()
|
self.check_rhs_is_tuple_or_list()
|
||||||
self.check_rhs_length_equals_lhs_length()
|
self.check_rhs_length_equals_lhs_length()
|
||||||
|
@ -1105,6 +1105,14 @@ calling the appropriate methods on the wrapped expression.
|
|||||||
``UNNEST``, etc.) to skip optimization and be properly evaluated when
|
``UNNEST``, etc.) to skip optimization and be properly evaluated when
|
||||||
annotations spawn rows themselves. Defaults to ``False``.
|
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)
|
.. 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
|
Provides the chance to do any preprocessing or validation of
|
||||||
|
@ -330,6 +330,10 @@ Models
|
|||||||
accepts a list of field names or expressions and returns a JSON array
|
accepts a list of field names or expressions and returns a JSON array
|
||||||
containing those values.
|
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
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -131,6 +131,8 @@ database.
|
|||||||
``ForeignObject`` is an internal API. This means it is not covered by our
|
``ForeignObject`` is an internal API. This means it is not covered by our
|
||||||
:ref:`deprecation policy <internal-release-deprecation-policy>`.
|
:ref:`deprecation policy <internal-release-deprecation-policy>`.
|
||||||
|
|
||||||
|
.. _cpk-and-database-functions:
|
||||||
|
|
||||||
Composite primary keys 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("order_id") -- OK
|
||||||
MAX("product_id", "order_id") -- ERROR
|
MAX("product_id", "order_id") -- ERROR
|
||||||
|
|
||||||
As a consequence, they cannot be used with composite primary key references as
|
In these cases, providing a composite primary key reference raises a
|
||||||
they are composed of multiple column expressions.
|
``ValueError``, since it is composed of multiple column expressions. An
|
||||||
|
exception is made for ``Count``.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
Max("order_id") # OK
|
Max("order_id") # OK
|
||||||
Max("pk") # ERROR
|
Max("pk") # ValueError
|
||||||
|
Count("pk") # OK
|
||||||
|
|
||||||
Composite primary keys in forms
|
Composite primary keys in forms
|
||||||
===============================
|
===============================
|
||||||
|
@ -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 django.test import TestCase
|
||||||
|
|
||||||
from .models import Comment, Tenant, User
|
from .models import Comment, Tenant, User
|
||||||
@ -136,3 +136,8 @@ class CompositePKAggregateTests(TestCase):
|
|||||||
),
|
),
|
||||||
(self.user_3, self.user_1, self.user_2),
|
(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"))
|
||||||
|
@ -428,6 +428,6 @@ class CompositePKFilterTests(TestCase):
|
|||||||
self.assertSequenceEqual(queryset, (self.user_2,))
|
self.assertSequenceEqual(queryset, (self.user_2,))
|
||||||
|
|
||||||
def test_cannot_cast_pk(self):
|
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):
|
with self.assertRaisesMessage(ValueError, msg):
|
||||||
Comment.objects.filter(text__gt=Cast(F("pk"), TextField())).count()
|
Comment.objects.filter(text__gt=Cast(F("pk"), TextField())).count()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user