Browse Source

Fixed #36034 -- Added system check for ForeignKey/ForeignObject/ManyToManyField to CompositePrimaryKeys.

Mariusz Felisiak 2 months ago
parent
commit
b322319f9d

+ 47 - 0
django/db/models/fields/related.py

@@ -580,6 +580,7 @@ class ForeignObject(RelatedField):
         return [
             *super().check(**kwargs),
             *self._check_to_fields_exist(),
+            *self._check_to_fields_composite_pk(),
             *self._check_unique_target(),
         ]
 
@@ -605,6 +606,36 @@ class ForeignObject(RelatedField):
                     )
         return errors
 
+    def _check_to_fields_composite_pk(self):
+        from django.db.models.fields.composite import CompositePrimaryKey
+
+        # Skip nonexistent models.
+        if isinstance(self.remote_field.model, str):
+            return []
+
+        errors = []
+        for to_field in self.to_fields:
+            try:
+                field = (
+                    self.remote_field.model._meta.pk
+                    if to_field is None
+                    else self.remote_field.model._meta.get_field(to_field)
+                )
+            except exceptions.FieldDoesNotExist:
+                pass
+            else:
+                if isinstance(field, CompositePrimaryKey):
+                    errors.append(
+                        checks.Error(
+                            "Field defines a relation to the CompositePrimaryKey of "
+                            f"model {self.remote_field.model._meta.object_name!r} "
+                            "which is not supported.",
+                            obj=self,
+                            id="fields.E347",
+                        )
+                    )
+        return errors
+
     def _check_unique_target(self):
         rel_is_string = isinstance(self.remote_field.model, str)
         if rel_is_string or not self.requires_unique_target:
@@ -1470,6 +1501,8 @@ class ManyToManyField(RelatedField):
         return warnings
 
     def _check_relationship_model(self, from_model=None, **kwargs):
+        from django.db.models.fields.composite import CompositePrimaryKey
+
         if hasattr(self.remote_field.through, "_meta"):
             qualified_model_name = "%s.%s" % (
                 self.remote_field.through._meta.app_label,
@@ -1506,6 +1539,20 @@ class ManyToManyField(RelatedField):
                 to_model_name = to_model
             else:
                 to_model_name = to_model._meta.object_name
+            if (
+                self.remote_field.through_fields is None
+                and not isinstance(to_model, str)
+                and isinstance(to_model._meta.pk, CompositePrimaryKey)
+            ):
+                errors.append(
+                    checks.Error(
+                        "Field defines a relation to the CompositePrimaryKey of model "
+                        f"{self.remote_field.model._meta.object_name!r} which is not "
+                        "supported.",
+                        obj=self,
+                        id="fields.E347",
+                    )
+                )
             relationship_model_name = self.remote_field.through._meta.object_name
             self_referential = from_model == to_model
             # Count foreign keys in intermediate model

+ 2 - 0
docs/ref/checks.txt

@@ -338,6 +338,8 @@ Related fields
 * **fields.W345**: ``related_name`` has no effect on ``ManyToManyField`` with a
   symmetrical relationship, e.g. to "self".
 * **fields.W346**: ``db_comment`` has no effect on ``ManyToManyField``.
+* **fields.E347**: Field defines a relation to the ``CompositePrimaryKey`` of
+  model ``<model>`` which is not supported.
 
 Models
 ------

+ 129 - 0
tests/invalid_models_tests/test_relative_fields.py

@@ -440,6 +440,84 @@ class RelativeFieldTests(SimpleTestCase):
             ],
         )
 
+    def test_foreignkey_to_model_with_composite_primary_key(self):
+        class Parent(models.Model):
+            pk = models.CompositePrimaryKey("version", "name")
+            version = models.IntegerField()
+            name = models.CharField(max_length=20)
+
+        class Child(models.Model):
+            rel_class_parent = models.ForeignKey(
+                Parent, on_delete=models.CASCADE, related_name="child_class_set"
+            )
+            rel_string_parent = models.ForeignKey(
+                "Parent", on_delete=models.CASCADE, related_name="child_string_set"
+            )
+
+        field = Child._meta.get_field("rel_string_parent")
+        self.assertEqual(
+            field.check(),
+            [
+                Error(
+                    "Field defines a relation to the CompositePrimaryKey of model "
+                    "'Parent' which is not supported.",
+                    obj=field,
+                    id="fields.E347",
+                ),
+            ],
+        )
+        field = Child._meta.get_field("rel_class_parent")
+        self.assertEqual(
+            field.check(),
+            [
+                Error(
+                    "Field defines a relation to the CompositePrimaryKey of model "
+                    "'Parent' which is not supported.",
+                    obj=field,
+                    id="fields.E347",
+                ),
+            ],
+        )
+
+    def test_many_to_many_to_model_with_composite_primary_key(self):
+        class Parent(models.Model):
+            pk = models.CompositePrimaryKey("version", "name")
+            version = models.IntegerField()
+            name = models.CharField(max_length=20)
+
+        class Child(models.Model):
+            rel_class_parent = models.ManyToManyField(
+                Parent, related_name="child_class_set"
+            )
+            rel_string_parent = models.ManyToManyField(
+                "Parent", related_name="child_string_set"
+            )
+
+        field = Child._meta.get_field("rel_string_parent")
+        self.assertEqual(
+            field.check(from_model=Child),
+            [
+                Error(
+                    "Field defines a relation to the CompositePrimaryKey of model "
+                    "'Parent' which is not supported.",
+                    obj=field,
+                    id="fields.E347",
+                ),
+            ],
+        )
+        field = Child._meta.get_field("rel_class_parent")
+        self.assertEqual(
+            field.check(from_model=Child),
+            [
+                Error(
+                    "Field defines a relation to the CompositePrimaryKey of model "
+                    "'Parent' which is not supported.",
+                    obj=field,
+                    id="fields.E347",
+                ),
+            ],
+        )
+
     def test_foreign_key_to_non_unique_field(self):
         class Target(models.Model):
             bad = models.IntegerField()  # No unique=True
@@ -939,6 +1017,57 @@ class RelativeFieldTests(SimpleTestCase):
             ],
         )
 
+    def test_to_fields_with_composite_primary_key(self):
+        class Parent(models.Model):
+            pk = models.CompositePrimaryKey("version", "name")
+            version = models.IntegerField()
+            name = models.CharField(max_length=20)
+
+        class Child(models.Model):
+            a = models.IntegerField()
+            b = models.IntegerField()
+            parent = models.ForeignObject(
+                Parent,
+                on_delete=models.SET_NULL,
+                from_fields=("a", "b"),
+                to_fields=("pk", "version"),
+            )
+
+        field = Child._meta.get_field("parent")
+        self.assertEqual(
+            field.check(),
+            [
+                Error(
+                    "Field defines a relation to the CompositePrimaryKey of model "
+                    "'Parent' which is not supported.",
+                    obj=field,
+                    id="fields.E347",
+                ),
+            ],
+        )
+
+    def test_to_field_to_composite_primery_key(self):
+        class Parent(models.Model):
+            pk = models.CompositePrimaryKey("version", "name")
+            version = models.IntegerField()
+            name = models.CharField(max_length=20)
+
+        class Child(models.Model):
+            parent = models.ForeignKey(Parent, on_delete=models.CASCADE, to_field="pk")
+
+        field = Child._meta.get_field("parent")
+        self.assertEqual(
+            field.check(),
+            [
+                Error(
+                    "Field defines a relation to the CompositePrimaryKey of model "
+                    "'Parent' which is not supported.",
+                    obj=field,
+                    id="fields.E347",
+                ),
+            ],
+        )
+
     def test_invalid_related_query_name(self):
         class Target(models.Model):
             pass