From 8be0c0d6901669661fca578f474cd51cd284d35a Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Thu, 8 May 2025 14:02:35 -0400 Subject: [PATCH] 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. --- django/db/models/query.py | 6 +++++- docs/releases/5.2.2.txt | 3 ++- tests/composite_pk/models/tenant.py | 10 ++++++---- tests/composite_pk/tests.py | 8 ++++++++ 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 4ba96928d4..663f8bade4 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -2678,7 +2678,11 @@ class RelatedPopulator: ) self.model_cls = klass_info["model"] - self.pk_idx = self.init_list.index(self.model_cls._meta.pk.attname) + # A primary key must have all of its constituents not-NULL as + # NULL != NULL and thus NULL cannot be referenced through a foreign + # relationship. Therefore checking for a single member of the primary + # key is enough to determine if the referenced object exists or not. + self.pk_idx = self.init_list.index(self.model_cls._meta.pk_fields[0].attname) self.related_populators = get_related_populators(klass_info, select, self.db) self.local_setter = klass_info["local_setter"] self.remote_setter = klass_info["remote_setter"] diff --git a/docs/releases/5.2.2.txt b/docs/releases/5.2.2.txt index 6d5f3bb61d..1af581e60c 100644 --- a/docs/releases/5.2.2.txt +++ b/docs/releases/5.2.2.txt @@ -9,4 +9,5 @@ Django 5.2.2 fixes several bugs in 5.2.1. Bugfixes ======== -* ... +* Fixed a crash when using ``select_related`` against a ``ForeignObject`` + originating from a model with a ``CompositePrimaryKey`` (:ticket:`36373`). diff --git a/tests/composite_pk/models/tenant.py b/tests/composite_pk/models/tenant.py index c85869afa7..65eb0feae8 100644 --- a/tests/composite_pk/models/tenant.py +++ b/tests/composite_pk/models/tenant.py @@ -14,17 +14,18 @@ class Token(models.Model): secret = models.CharField(max_length=10, default="", blank=True) -class BaseModel(models.Model): +class AbstractUser(models.Model): pk = models.CompositePrimaryKey("tenant_id", "id") tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE) + email = models.EmailField(unique=True) id = models.SmallIntegerField(unique=True) class Meta: abstract = True -class User(BaseModel): - email = models.EmailField(unique=True) +class User(AbstractUser): + pass class Comment(models.Model): @@ -35,13 +36,14 @@ class Comment(models.Model): related_name="comments", ) id = models.SmallIntegerField(unique=True, db_column="comment_id") - user_id = models.SmallIntegerField() + user_id = models.SmallIntegerField(null=True) user = models.ForeignObject( User, on_delete=models.CASCADE, from_fields=("tenant_id", "user_id"), to_fields=("tenant_id", "id"), related_name="comments", + null=True, ) text = models.TextField(default="", blank=True) integer = models.IntegerField(default=0) diff --git a/tests/composite_pk/tests.py b/tests/composite_pk/tests.py index 5dea23c9f2..91cbee0635 100644 --- a/tests/composite_pk/tests.py +++ b/tests/composite_pk/tests.py @@ -184,6 +184,14 @@ class CompositePKTests(TestCase): with self.assertNumQueries(1): self.assertEqual(user.email, self.user.email) + def test_select_related(self): + Comment.objects.create(tenant=self.tenant, id=2) + with self.assertNumQueries(1): + comments = list(Comment.objects.select_related("user").order_by("pk")) + self.assertEqual(len(comments), 2) + self.assertEqual(comments[0].user, self.user) + self.assertIsNone(comments[1].user) + def test_model_forms(self): fields = ["tenant", "id", "user_id", "text", "integer"] self.assertEqual(list(CommentForm.base_fields), fields)