Skip to content

Commit

Permalink
Fixed #31573 -- Made QuerySet.update() respect ordering on MariaDB/My…
Browse files Browse the repository at this point in the history
…SQL.
  • Loading branch information
dchorpash-dod authored and felixxm committed Jul 8, 2020
1 parent 060576b commit 779e615
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 2 deletions.
19 changes: 18 additions & 1 deletion django/db/backends/mysql/compiler.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.core.exceptions import FieldError
from django.db.models.sql import compiler


Expand Down Expand Up @@ -36,7 +37,23 @@ def as_sql(self):


class SQLUpdateCompiler(compiler.SQLUpdateCompiler, SQLCompiler):
pass
def as_sql(self):
update_query, update_params = super().as_sql()
# MySQL and MariaDB support UPDATE ... ORDER BY syntax.
if self.query.order_by:
order_by_sql = []
order_by_params = []
try:
for _, (sql, params, _) in self.get_order_by():
order_by_sql.append(sql)
order_by_params.extend(params)
update_query += ' ORDER BY ' + ', '.join(order_by_sql)
update_params += tuple(order_by_params)
except FieldError:
# Ignore ordering if it contains annotations, because they're
# removed in .update() and cannot be resolved.
pass
return update_query, update_params


class SQLAggregateCompiler(compiler.SQLAggregateCompiler, SQLCompiler):
Expand Down
15 changes: 15 additions & 0 deletions docs/ref/models/querysets.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2530,6 +2530,21 @@ update a bunch of records for a model that has a custom
e.comments_on = False
e.save()

Ordered queryset
^^^^^^^^^^^^^^^^

.. versionadded:: 3.2

Chaining ``order_by()`` with ``update()`` is supported only on MariaDB and
MySQL, and is ignored for different databases. This is useful for updating a
unique field in the order that is specified without conflicts. For example::

Entry.objects.order_by('-number').update(number=F('number') + 1)

.. note::

If the ``order_by()`` clause contains annotations, it will be ignored.

``delete()``
~~~~~~~~~~~~

Expand Down
3 changes: 3 additions & 0 deletions docs/releases/3.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ Models
* The new :attr:`.UniqueConstraint.opclasses` attribute allows setting
PostgreSQL operator classes.

* The :meth:`.QuerySet.update` method now respects the ``order_by()`` clause on
MySQL and MariaDB.

Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 4 additions & 0 deletions tests/update/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ class Foo(models.Model):
class Bar(models.Model):
foo = models.ForeignKey(Foo, models.CASCADE, to_field='target')
m2m_foo = models.ManyToManyField(Foo, related_name='m2m_foo')


class UniqueNumber(models.Model):
number = models.IntegerField(unique=True)
40 changes: 39 additions & 1 deletion tests/update/tests.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import unittest

from django.core.exceptions import FieldError
from django.db import IntegrityError, connection, transaction
from django.db.models import Count, F, Max
from django.db.models.functions import Concat, Lower
from django.test import TestCase

from .models import A, B, Bar, D, DataPoint, Foo, RelatedPoint
from .models import A, B, Bar, D, DataPoint, Foo, RelatedPoint, UniqueNumber


class SimpleTest(TestCase):
Expand Down Expand Up @@ -199,3 +202,38 @@ def test_update_with_joined_field_annotation(self):
with self.subTest(annotation=annotation):
with self.assertRaisesMessage(FieldError, msg):
RelatedPoint.objects.annotate(new_name=annotation).update(name=F('new_name'))


@unittest.skipUnless(
connection.vendor == 'mysql',
'UPDATE...ORDER BY syntax is supported on MySQL/MariaDB',
)
class MySQLUpdateOrderByTest(TestCase):
"""Update field with a unique constraint using an ordered queryset."""
@classmethod
def setUpTestData(cls):
UniqueNumber.objects.create(number=1)
UniqueNumber.objects.create(number=2)

def test_order_by_update_on_unique_constraint(self):
tests = [
('-number', 'id'),
(F('number').desc(), 'id'),
(F('number') * -1, 'id'),
]
for ordering in tests:
with self.subTest(ordering=ordering), transaction.atomic():
updated = UniqueNumber.objects.order_by(*ordering).update(
number=F('number') + 1,
)
self.assertEqual(updated, 2)

def test_order_by_update_on_unique_constraint_annotation(self):
# Ordering by annotations is omitted because they cannot be resolved in
# .update().
with self.assertRaises(IntegrityError):
UniqueNumber.objects.annotate(
number_inverse=F('number').desc(),
).order_by('number_inverse').update(
number=F('number') + 1,
)

0 comments on commit 779e615

Please sign in to comment.