#30667: Running last() on a related object with a F() ordering breaks all()
ordering (global side effect)
-------------------------------------+-------------------------------------
               Reporter:             |          Owner:  nobody
  NyanKiyoshi                        |
                   Type:  Bug        |         Status:  new
              Component:  Database   |        Version:  2.2
  layer (models, ORM)                |
               Severity:  Normal     |       Keywords:
           Triage Stage:             |      Has patch:  0
  Unreviewed                         |
    Needs documentation:  0          |    Needs tests:  0
Patch needs improvement:  0          |  Easy pickings:  0
                  UI/UX:  0          |
-------------------------------------+-------------------------------------
 Hi,

 I have ran into a weird issue when working with a O2M relation, where one
 would use the meta field ordering with `F(...)`.

 We can take this following code to illustrate as an example:

 {{{#!python
 from django.db import models
 from django.db.models import F


 class Item(models.Model):
     pass


 class ItemValue(models.Model):
     sort_order = models.IntegerField(unique=True, null=True)
     item = models.ForeignKey(Item, on_delete=models.CASCADE,
 related_name="items")

     class Meta:
         ordering = (F("sort_order").asc(nulls_last=True), "id")
 }}}


 In `Item` we have a relation to the `items`. The most important part here,
 is the ordering with a `F(...)`.

 Now, if another part of the code (a view, a test, ...) runs `last()` on
 the items, it will trigger some kind of switch in the ORM, which will make
 it always run `F(...)` in `DESC`.

 Here is an example:
 {{{#!python

 item = Item.objects.create()
 ItemValue.objects.bulk_create([
     ItemValue(item=self.item, sort_order=0),
     ItemValue(item=self.item),
     ItemValue(item=self.item),
 ])

 sort_order = item.items.all()[0].sort_order  # is 0

 item.items.last()

 sort_order = item.items.all()[0].sort_order # is None

 item.items.last()

 sort_order = item.items.all()[0].sort_order # is 0
 }}}

 The biggest issue with this, is if this is ran into a view or a test, the
 ordering will always be reversed until we restart the django worker, or if
 we run `item.items.last()` again to flip the switch.

 Here is a full example:
 {{{#!python
 # models
 from django.db import models
 from django.db.models import F


 class Item(models.Model):
     pass


 class ItemValue(models.Model):
     sort_order = models.IntegerField(unique=True, null=True)
     item = models.ForeignKey(Item, on_delete=models.CASCADE,
 related_name="items")

     class Meta:
         ordering = (F("sort_order").asc(nulls_last=True), "id")


 # test
 class TestItemValue(TestCase):
     def setUp(self) -> None:
         self.item = Item.objects.create()
         ItemValue.objects.bulk_create([
             ItemValue(item=self.item, sort_order=0),
             ItemValue(item=self.item),
             ItemValue(item=self.item),
         ])

     def test_valid(self):
         self.assertEqual(self.item.items.all()[0].sort_order, 0)

     def test_switch(self):
         self.item.items.last()

         with CaptureQueriesContext(connection) as ctx:
             sort_order = self.item.items.all()[0].sort_order
             sql = ctx[0]['sql']
             order_by = sql[sql.find("ORDER BY"):]
             self.assertEqual(sort_order, 0, order_by)
 }}}

 This is an example of the switch in action during a `.all()`:
 [[Image(https://i.imgur.com/lVH1pOs.gif)]]

-- 
Ticket URL: <https://code.djangoproject.com/ticket/30667>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/054.b0b3f212d1d678735402ba33dc5ed056%40djangoproject.com.

Reply via email to