From b322319f9d779b8726436421daae2862a380061d Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 1 Jan 2025 10:23:56 +0100 Subject: [PATCH] Fixed #36034 -- Added system check for ForeignKey/ForeignObject/ManyToManyField to CompositePrimaryKeys. --- django/db/models/fields/related.py | 47 +++++++ docs/ref/checks.txt | 2 + .../test_relative_fields.py | 129 ++++++++++++++++++ 3 files changed, 178 insertions(+) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 9ef2d29024..de8fe9c339 100644 --- a/django/db/models/fields/related.py +++ b/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 diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 17570a1923..edadc06e2e 100644 --- a/docs/ref/checks.txt +++ b/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 ```` which is not supported. Models ------ diff --git a/tests/invalid_models_tests/test_relative_fields.py b/tests/invalid_models_tests/test_relative_fields.py index 9b69ae4151..4167e0712a 100644 --- a/tests/invalid_models_tests/test_relative_fields.py +++ b/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