Browse Source

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

Jacob Walls 2 months ago
parent
commit
470e5545e5

+ 1 - 0
django/db/models/aggregates.py

@@ -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 == "*":

+ 9 - 0
django/db/models/expressions.py

@@ -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:

+ 3 - 0
django/db/models/fields/tuple_lookups.py

@@ -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()

+ 8 - 0
docs/ref/models/expressions.txt

@@ -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

+ 4 - 0
docs/releases/5.2.txt

@@ -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
 ~~~~~~~~~~~~~~~~~~~~~~
 

+ 7 - 3
docs/topics/composite-primary-key.txt

@@ -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
 ===============================

+ 6 - 1
tests/composite_pk/test_aggregate.py

@@ -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"))

+ 1 - 1
tests/composite_pk/test_filter.py

@@ -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()