#31004: Using FilteredRelation on M2M relationship duplicates result rows.
-------------------------------------+-------------------------------------
               Reporter:             |          Owner:  nobody
  idemchenko-wrk                     |
                   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          |
-------------------------------------+-------------------------------------
 Using {{{FilteredRelation}}} on many to many relationship could
 unexpectedly duplicate result rows.

 Models:
 {{{
 class Discount(models.Model):
     name = models.CharField(max_length=255)
     active_till = models.DateTimeField()


 class Item(models.Model):
     name = models.CharField(max_length=255)
     discounts = models.ManyToManyField(Discount)
 }}}

 Query:
 {{{
 Item.objects
 .annotate(
     available_discounts=FilteredRelation(
         'discounts', condition=Q(discounts__active_till__gte=now)
     )
 ).annotate(
     name_with_discount=Concat(F('name'), F('available_discounts__name'))
 )
 }}}

 SQL:
 {{{
 SELECT
     "sales_item"."id",
     "sales_item"."name",
     CONCAT("sales_item"."name", available_discounts."name") AS
 "name_with_discount"
 FROM "sales_item"
     LEFT OUTER JOIN "sales_item_discounts"
         ON ("sales_item"."id" = "sales_item_discounts"."item_id")
     LEFT OUTER JOIN "sales_discount" available_discounts
         ON (
             "sales_item_discounts"."discount_id" =
 available_discounts."id"
             AND (available_discounts."active_till" >=
 '2019-11-17T21:32:42.501283+00:00'::timestamptz)
         );
 }}}

 The problem is that the intermediate table (sales_item_discounts) joins
 with the target table (sales_discount) using the left join.
 So even if rows of the target table are filtered out, rows of the
 intermediate table are still there, and multiply the resulting rows.

 Test:
 {{{
 class DuplicationTest(TestCase):

     def test_row_duplication(self):
         now = timezone.now()

         active_discounts = [
             Discount.objects.create(name='-5%', active_till=now +
 timedelta(days=1)),
             Discount.objects.create(name='-5$', active_till=now +
 timedelta(days=1)),
         ]

         old_discounts = [
             Discount.objects.create(name='Whatever', active_till=now -
 timedelta(days=1)),
             Discount.objects.create(name='Whatever', active_till=now -
 timedelta(days=1)),
         ]

         item_w_discounts = Item.objects.create(name='item1')
         item_w_discounts.discounts.set(active_discounts + old_discounts)

         item_w_old_discounts = Item.objects.create(name='item2')
         item_w_old_discounts.discounts.set(old_discounts)

         item_wo_discounts = Item.objects.create(name='item3')

         items = list(
             Item.objects
             .annotate(
                 available_discounts=FilteredRelation(
                     'discounts',
 condition=Q(discounts__active_till__gte=now)
                 )
             ).annotate(
                 name_with_discount=Concat(F('name'),
 F('available_discounts__name'))
             )
         )

         self.assertEqual(items.count(item_wo_discounts), 1)     # Passed
         # expected to see one item, since it does not have active
 discounts
         self.assertEqual(items.count(item_w_old_discounts), 1)  #
 AssertionError: 2 != 1
         # expected to see two items, one for each active discount
         self.assertEqual(items.count(item_w_discounts),
 len(active_discounts))  # AssertionError: 4 != 2
 }}}

-- 
Ticket URL: <https://code.djangoproject.com/ticket/31004>
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/057.4119519e7007a5dc6c2cba21c9a50027%40djangoproject.com.

Reply via email to