Opened 9 days ago

Closed 5 days ago

Last modified 5 days ago

#36373 closed Bug (fixed)

select_related() doesn't work when targeting composite primary keys

Reported by: Jacob Walls Owned by: Simon Charette
Component: Database layer (models, ORM) Version: 5.2
Severity: Release blocker Keywords:
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The suggested workaround until #35956 implements foreign keys to models with composite primary keys is to use ForeignObject, but it causes failures in select_related(). (Of note: prefetch_related() works fine.)

  • tests/composite_pk/tests.py

    diff --git a/tests/composite_pk/tests.py b/tests/composite_pk/tests.py
    index 5dea23c9f2..068030bd18 100644
    a b class CompositePKTests(TestCase):  
    184184        with self.assertNumQueries(1):
    185185            self.assertEqual(user.email, self.user.email)
    186186
     187    def test_select_related(self):
     188        with self.assertNumQueries(1):
     189            for comment in Comment.objects.select_related():
     190                comment.user
     191
    187192    def test_model_forms(self):
    188193        fields = ["tenant", "id", "user_id", "text", "integer"]
    189194        self.assertEqual(list(CommentForm.base_fields), fields)
======================================================================
ERROR: test_select_related (composite_pk.tests.CompositePKTests.test_select_related)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/.../django/tests/composite_pk/tests.py", line 189, in test_select_related
    for comment in Comment.objects.select_related():
                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/Users/.../django/django/db/models/query.py", line 403, in __iter__
    self._fetch_all()
    ~~~~~~~~~~~~~~~^^
  File "/Users/.../django/django/db/models/query.py", line 1966, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
                         ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/.../django/django/db/models/query.py", line 107, in __iter__
    related_populators = get_related_populators(klass_info, select, db, fetch_mode)
  File "/Users/.../django/django/db/models/query.py", line 2742, in get_related_populators
    rel_cls = RelatedPopulator(rel_klass_info, select, db, fetch_mode)
  File "/Users/.../django/django/db/models/query.py", line 2710, in __init__
    self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
                  ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: 'pk' is not in list

Change History (9)

comment:1 by Simon Charette, 9 days ago

Severity: NormalRelease blocker
Triage Stage: UnreviewedAccepted

Thanks for the report, it's worth pointing out that the issue is not specific to calling select_related without a subset of relationship the following test fails as well and is a more common usage of select_related

  • tests/composite_pk/tests.py

    diff --git a/tests/composite_pk/tests.py b/tests/composite_pk/tests.py
    index 5dea23c9f2..6983e13945 100644
    a b def test_only(self):  
    184184        with self.assertNumQueries(1):
    185185            self.assertEqual(user.email, self.user.email)
    186186
     187    def test_select_related(self):
     188        with self.assertNumQueries(1):
     189            for comment in Comment.objects.select_related("user"):
     190                comment.user
     191
    187192    def test_model_forms(self):
    188193        fields = ["tenant", "id", "user_id", "text", "integer"]
    189194        self.assertEqual(list(CommentForm.base_fields), fields)

comment:2 by Simon Charette, 9 days ago

Looks like something like the following should do

  • django/db/models/query.py

    diff --git a/django/db/models/query.py b/django/db/models/query.py
    index 4f4aad91ef..0472e99144 100644
    a b def __init__(self, klass_info, select, db):  
    26802680            )
    26812681
    26822682        self.model_cls = klass_info["model"]
    2683         self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname)
     2683        pk_fields = self.model_cls._meta.pk_fields
     2684        pk_idx = self.init_list.index(pk_fields[0].attname)
     2685        if (pk_fields_len := len(pk_fields)) > 1:
     2686            self.pk_idx = slice(pk_idx, pk_fields_len)
     2687        else:
     2688            self.pk_idx = pk_idx
    26842689        self.related_populators = get_related_populators(klass_info, select, self.db)
    26852690        self.local_setter = klass_info["local_setter"]
    26862691        self.remote_setter = klass_info["remote_setter"]

It might be worth adding a test with a model where pk fields are not defined next to each other or just alter the existing models to cover for that

  • tests/composite_pk/models/tenant.py

    diff --git a/tests/composite_pk/models/tenant.py b/tests/composite_pk/models/tenant.py
    index c85869afa7..a8dfd790f6 100644
    a b class Token(models.Model):  
    1717class BaseModel(models.Model):
    1818    pk = models.CompositePrimaryKey("tenant_id", "id")
    1919    tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE)
     20    between = models.IntegerField(null=True)
    2021    id = models.SmallIntegerField(unique=True)
    2122
    2223    class Meta:
    class Comment(models.Model):  
    3435        on_delete=models.CASCADE,
    3536        related_name="comments",
    3637    )
    37     id = models.SmallIntegerField(unique=True, db_column="comment_id")
    3838    user_id = models.SmallIntegerField()
     39    id = models.SmallIntegerField(unique=True, db_column="comment_id")
    3940    user = models.ForeignObject(
    4041        User,
    4142        on_delete=models.CASCADE,
Last edited 9 days ago by Simon Charette (previous) (diff)

comment:3 by Simon Charette, 8 days ago

Owner: set to Simon Charette
Status: newassigned

comment:4 by Simon Charette, 8 days ago

Has patch: set

comment:5 by Sarah Boyce, 8 days ago

Patch needs improvement: set

comment:6 by Sarah Boyce, 7 days ago

Patch needs improvement: unset

comment:7 by Sarah Boyce, 5 days ago

Triage Stage: AcceptedReady for checkin

comment:8 by Sarah Boyce <42296566+sarahboyce@…>, 5 days ago

Resolution: fixed
Status: assignedclosed

In 8be0c0d:

Fixed #36373 -- Fixed select_related() crash on foreign object for a composite pk.

Thanks Jacob Walls for the report and Sarah for the in-depth review.

comment:9 by Sarah Boyce <42296566+sarahboyce@…>, 5 days ago

In e23dd728:

[5.2.x] Fixed #36373 -- Fixed select_related() crash on foreign object for a composite pk.

Thanks Jacob Walls for the report and Sarah for the in-depth review.

Backport of 8be0c0d6901669661fca578f474cd51cd284d35a from main.

Note: See TracTickets for help on using tickets.
Back to Top