Browse Source

Fixed #32645 -- Fixed QuerySet.update() crash when ordered by joined fields on MySQL/MariaDB.

Thanks Matt Westcott for the report.

Regression in 779e615e362108862f1681f965ee9e4f1d0ae6d2.
Mariusz Felisiak 4 years ago
parent
commit
ca98729055

+ 10 - 1
django/db/backends/mysql/compiler.py

@@ -1,4 +1,5 @@
 from django.core.exceptions import FieldError
+from django.db.models.expressions import Col
 from django.db.models.sql import compiler
 
 
@@ -45,8 +46,16 @@ class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
         if self.query.order_by:
             order_by_sql = []
             order_by_params = []
+            db_table = self.query.get_meta().db_table
             try:
-                for _, (sql, params, _) in self.get_order_by():
+                for resolved, (sql, params, _) in self.get_order_by():
+                    if (
+                        isinstance(resolved.expression, Col) and
+                        resolved.expression.alias != db_table
+                    ):
+                        # Ignore ordering if it contains joined fields, because
+                        # they cannot be used in the ORDER BY clause.
+                        raise FieldError
                     order_by_sql.append(sql)
                     order_by_params.extend(params)
                 update_query += ' ORDER BY ' + ', '.join(order_by_sql)

+ 2 - 1
docs/ref/models/querysets.txt

@@ -2646,7 +2646,8 @@ unique field in the order that is specified without conflicts. For example::
 
 .. note::
 
-    If the ``order_by()`` clause contains annotations, it will be ignored.
+    ``order_by()`` clause will be ignored if it contains annotations, inherited
+    fields, or lookups spanning relations.
 
 ``delete()``
 ~~~~~~~~~~~~

+ 4 - 0
docs/releases/3.2.1.txt

@@ -36,3 +36,7 @@ Bugfixes
 
 * Fixed a regression in Django 3.2 that caused a crash when combining ``Q()``
   objects which contains boolean expressions (:ticket:`32548`).
+
+* Fixed a regression in Django 3.2 that caused a crash of ``QuerySet.update()``
+  on a queryset ordered by inherited or joined fields on MySQL and MariaDB
+  (:ticket:`32645`).

+ 4 - 0
tests/update/models.py

@@ -45,3 +45,7 @@ class Bar(models.Model):
 
 class UniqueNumber(models.Model):
     number = models.IntegerField(unique=True)
+
+
+class UniqueNumberChild(UniqueNumber):
+    pass

+ 27 - 1
tests/update/tests.py

@@ -7,7 +7,10 @@ from django.db.models.functions import Abs, Concat, Lower
 from django.test import TestCase
 from django.test.utils import register_lookup
 
-from .models import A, B, Bar, D, DataPoint, Foo, RelatedPoint, UniqueNumber
+from .models import (
+    A, B, Bar, D, DataPoint, Foo, RelatedPoint, UniqueNumber,
+    UniqueNumberChild,
+)
 
 
 class SimpleTest(TestCase):
@@ -249,3 +252,26 @@ class MySQLUpdateOrderByTest(TestCase):
             ).order_by('number_inverse').update(
                 number=F('number') + 1,
             )
+
+    def test_order_by_update_on_parent_unique_constraint(self):
+        # Ordering by inherited fields is omitted because joined fields cannot
+        # be used in the ORDER BY clause.
+        UniqueNumberChild.objects.create(number=3)
+        UniqueNumberChild.objects.create(number=4)
+        with self.assertRaises(IntegrityError):
+            UniqueNumberChild.objects.order_by('number').update(
+                number=F('number') + 1,
+            )
+
+    def test_order_by_update_on_related_field(self):
+        # Ordering by related fields is omitted because joined fields cannot be
+        # used in the ORDER BY clause.
+        data = DataPoint.objects.create(name='d0', value='apple')
+        related = RelatedPoint.objects.create(name='r0', data=data)
+        with self.assertNumQueries(1) as ctx:
+            updated = RelatedPoint.objects.order_by('data__name').update(name='new')
+        sql = ctx.captured_queries[0]['sql']
+        self.assertNotIn('ORDER BY', sql)
+        self.assertEqual(updated, 1)
+        related.refresh_from_db()
+        self.assertEqual(related.name, 'new')