From ca9872905559026af82000e46cde6f7dedc897b6 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 14 Apr 2021 21:11:17 +0200 Subject: [PATCH] Fixed #32645 -- Fixed QuerySet.update() crash when ordered by joined fields on MySQL/MariaDB. Thanks Matt Westcott for the report. Regression in 779e615e362108862f1681f965ee9e4f1d0ae6d2. --- django/db/backends/mysql/compiler.py | 11 ++++++++++- docs/ref/models/querysets.txt | 3 ++- docs/releases/3.2.1.txt | 4 ++++ tests/update/models.py | 4 ++++ tests/update/tests.py | 28 +++++++++++++++++++++++++++- 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/django/db/backends/mysql/compiler.py b/django/db/backends/mysql/compiler.py index da02c1fd73..49b47961a1 100644 --- a/django/db/backends/mysql/compiler.py +++ b/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) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index dbca894715..5dc7a6b5bc 100644 --- a/docs/ref/models/querysets.txt +++ b/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()`` ~~~~~~~~~~~~ diff --git a/docs/releases/3.2.1.txt b/docs/releases/3.2.1.txt index df8d192327..3de4b385c9 100644 --- a/docs/releases/3.2.1.txt +++ b/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`). diff --git a/tests/update/models.py b/tests/update/models.py index 98f40a8603..131c5b327e 100644 --- a/tests/update/models.py +++ b/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 diff --git a/tests/update/tests.py b/tests/update/tests.py index 0ba6fe43c3..a90f0f8323 100644 --- a/tests/update/tests.py +++ b/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')