From 2249370c8611d97f8bdb6003fb7b4d8fd3646202 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Wed, 11 Dec 2024 21:37:23 +0100 Subject: [PATCH] Fixed #35992, Fixed #35997 -- Added system check for CompositePrimaryKeys in Meta.indexes/constraints/unique_together. CompositePrimaryKeys are not supported in any of these options. --- django/db/models/base.py | 10 ++ django/db/models/constraints.py | 6 +- docs/ref/checks.txt | 3 + tests/invalid_models_tests/test_models.py | 183 ++++++++++++++++++++++ 4 files changed, 200 insertions(+), 2 deletions(-) diff --git a/django/db/models/base.py b/django/db/models/base.py index d948cd2a1c..a7a26b405c 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -2288,6 +2288,16 @@ class Model(AltersData, metaclass=ModelBase): id="models.E013", ) ) + elif isinstance(field, models.CompositePrimaryKey): + errors.append( + checks.Error( + f"{option!r} refers to a CompositePrimaryKey " + f"{field_name!r}, but CompositePrimaryKeys are not " + f"permitted in {option!r}.", + obj=cls, + id="models.E048", + ) + ) elif field not in cls._meta.local_fields: errors.append( checks.Error( diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index 00829aee28..1601fc9933 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -93,11 +93,13 @@ class BaseConstraint: return [] def _check_references(self, model, references): + from django.db.models.fields.composite import CompositePrimaryKey + errors = [] fields = set() for field_name, *lookups in references: - # pk is an alias that won't be found by opts.get_field. - if field_name != "pk": + # pk is an alias that won't be found by opts.get_field(). + if field_name != "pk" or isinstance(model._meta.pk, CompositePrimaryKey): fields.add(field_name) if not lookups: # If it has no lookups it cannot result in a JOIN. diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index b0a98bde28..17570a1923 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -431,6 +431,9 @@ Models (``db_table_comment``). * **models.W047**: ```` does not support unique constraints with nulls distinct. +* **models.E048**: ``constraints/indexes/unique_together`` refers to a + ``CompositePrimaryKey`` ````, but ``CompositePrimaryKey``\s are + not supported for that option. Management Commands ------------------- diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index 8b6d705acb..c44fe56f89 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -145,6 +145,27 @@ class UniqueTogetherTests(SimpleTestCase): self.assertEqual(Bar.check(), []) + def test_pointing_to_composite_primary_key(self): + class Model(models.Model): + pk = models.CompositePrimaryKey("version", "name") + version = models.IntegerField() + name = models.CharField(max_length=20) + + class Meta: + unique_together = [["pk"]] + + self.assertEqual( + Model.check(), + [ + Error( + "'unique_together' refers to a CompositePrimaryKey 'pk', but " + "CompositePrimaryKeys are not permitted in 'unique_together'.", + obj=Model, + id="models.E048", + ), + ], + ) + @isolate_apps("invalid_models_tests") class IndexesTests(TestCase): @@ -225,6 +246,27 @@ class IndexesTests(TestCase): self.assertEqual(Bar.check(), []) + def test_pointing_to_composite_primary_key(self): + class Model(models.Model): + pk = models.CompositePrimaryKey("version", "name") + version = models.IntegerField() + name = models.CharField(max_length=20) + + class Meta: + indexes = [models.Index(fields=["pk", "name"], name="name")] + + self.assertEqual( + Model.check(), + [ + Error( + "'indexes' refers to a CompositePrimaryKey 'pk', but " + "CompositePrimaryKeys are not permitted in 'indexes'.", + obj=Model, + id="models.E048", + ), + ], + ) + def test_name_constraints(self): class Model(models.Model): class Meta: @@ -446,6 +488,28 @@ class IndexesTests(TestCase): self.assertEqual(Model.check(databases=self.databases), []) + @skipUnlessDBFeature("supports_covering_indexes") + def test_index_include_pointing_to_composite_primary_key(self): + class Model(models.Model): + pk = models.CompositePrimaryKey("version", "name") + version = models.IntegerField() + name = models.CharField(max_length=20) + + class Meta: + indexes = [models.Index(fields=["name"], include=["pk"], name="name")] + + self.assertEqual( + Model.check(), + [ + Error( + "'indexes' refers to a CompositePrimaryKey 'pk', but " + "CompositePrimaryKeys are not permitted in 'indexes'.", + obj=Model, + id="models.E048", + ), + ], + ) + def test_func_index(self): class Model(models.Model): name = models.CharField(max_length=10) @@ -581,6 +645,27 @@ class IndexesTests(TestCase): self.assertEqual(Bar.check(), []) + def test_func_index_pointing_to_composite_primary_key(self): + class Model(models.Model): + pk = models.CompositePrimaryKey("version", "name") + version = models.IntegerField() + name = models.CharField(max_length=20) + + class Meta: + indexes = [models.Index(Abs("pk"), name="name")] + + self.assertEqual( + Model.check(), + [ + Error( + "'indexes' refers to a CompositePrimaryKey 'pk', but " + "CompositePrimaryKeys are not permitted in 'indexes'.", + obj=Model, + id="models.E048", + ), + ], + ) + @isolate_apps("invalid_models_tests") class FieldNamesTests(TestCase): @@ -2209,6 +2294,33 @@ class ConstraintsTests(TestCase): ) self.assertEqual(Model.check(databases=self.databases), expected_warnings) + @skipUnlessDBFeature("supports_table_check_constraints") + def test_check_constraint_pointing_to_composite_primary_key(self): + class Model(models.Model): + pk = models.CompositePrimaryKey("version", "name") + version = models.IntegerField() + name = models.CharField(max_length=20) + + class Meta: + constraints = [ + models.CheckConstraint( + name="name", + condition=models.Q(pk__gt=(7, "focal")), + ), + ] + + self.assertEqual( + Model.check(databases=self.databases), + [ + Error( + "'constraints' refers to a CompositePrimaryKey 'pk', but " + "CompositePrimaryKeys are not permitted in 'constraints'.", + obj=Model, + id="models.E048", + ), + ], + ) + def test_unique_constraint_with_condition(self): class Model(models.Model): age = models.IntegerField() @@ -2471,6 +2583,27 @@ class ConstraintsTests(TestCase): self.assertEqual(Model.check(databases=self.databases), []) + def test_unique_constraint_pointing_to_composite_primary_key(self): + class Model(models.Model): + pk = models.CompositePrimaryKey("version", "name") + version = models.IntegerField() + name = models.CharField(max_length=20) + + class Meta: + constraints = [models.UniqueConstraint(fields=["pk"], name="name")] + + self.assertEqual( + Model.check(databases=self.databases), + [ + Error( + "'constraints' refers to a CompositePrimaryKey 'pk', but " + "CompositePrimaryKeys are not permitted in 'constraints'.", + obj=Model, + id="models.E048", + ), + ], + ) + def test_unique_constraint_with_include(self): class Model(models.Model): age = models.IntegerField() @@ -2618,6 +2751,34 @@ class ConstraintsTests(TestCase): self.assertEqual(Model.check(databases=self.databases), []) + @skipUnlessDBFeature("supports_covering_indexes") + def test_unique_constraint_include_pointing_to_composite_primary_key(self): + class Model(models.Model): + pk = models.CompositePrimaryKey("version", "name") + version = models.IntegerField() + name = models.CharField(max_length=20) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["version"], + include=["pk"], + name="name", + ), + ] + + self.assertEqual( + Model.check(databases=self.databases), + [ + Error( + "'constraints' refers to a CompositePrimaryKey 'pk', but " + "CompositePrimaryKeys are not permitted in 'constraints'.", + obj=Model, + id="models.E048", + ), + ], + ) + def test_func_unique_constraint(self): class Model(models.Model): name = models.CharField(max_length=10) @@ -2815,3 +2976,25 @@ class ConstraintsTests(TestCase): ] self.assertEqual(Bar.check(databases=self.databases), []) + + @skipUnlessDBFeature("supports_expression_indexes") + def test_func_unique_constraint_pointing_composite_primary_key(self): + class Model(models.Model): + pk = models.CompositePrimaryKey("version", "name") + version = models.IntegerField() + name = models.CharField(max_length=20) + + class Meta: + constraints = [models.UniqueConstraint(Abs("pk"), name="name")] + + self.assertEqual( + Model.check(databases=self.databases), + [ + Error( + "'constraints' refers to a CompositePrimaryKey 'pk', but " + "CompositePrimaryKeys are not permitted in 'constraints'.", + obj=Model, + id="models.E048", + ), + ], + )