Browse Source

Fixed #35992, Fixed #35997 -- Added system check for CompositePrimaryKeys in Meta.indexes/constraints/unique_together.

CompositePrimaryKeys are not supported in any of these options.
Mariusz Felisiak 3 months ago
parent
commit
2249370c86

+ 10 - 0
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(

+ 4 - 2
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.

+ 3 - 0
docs/ref/checks.txt

@@ -431,6 +431,9 @@ Models
   (``db_table_comment``).
 * **models.W047**: ``<database>`` does not support unique constraints with
   nulls distinct.
+* **models.E048**: ``constraints/indexes/unique_together`` refers to a
+  ``CompositePrimaryKey`` ``<field name>``, but ``CompositePrimaryKey``\s are
+  not supported for that option.
 
 Management Commands
 -------------------

+ 183 - 0
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",
+                ),
+            ],
+        )