diff --git a/django/db/models/base.py b/django/db/models/base.py index d082c35917e..fcda6e202fc 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1614,12 +1614,10 @@ class Model(metaclass=ModelBase): if not router.allow_migrate_model(db, cls): continue connection = connections[db] - if ( + if not ( connection.features.supports_partial_indexes or 'supports_partial_indexes' in cls._meta.required_db_features - ): - continue - if any(index.condition is not None for index in cls._meta.indexes): + ) and any(index.condition is not None for index in cls._meta.indexes): errors.append( checks.Warning( '%s does not support indexes with conditions.' @@ -1632,6 +1630,22 @@ class Model(metaclass=ModelBase): id='models.W037', ) ) + if not ( + connection.features.supports_covering_indexes or + 'supports_covering_indexes' in cls._meta.required_db_features + ) and any(index.include for index in cls._meta.indexes): + errors.append( + checks.Warning( + '%s does not support indexes with non-key columns.' + % connection.display_name, + hint=( + "Non-key columns will be ignored. Silence this " + "warning if you don't care about it." + ), + obj=cls, + id='models.W040', + ) + ) fields = [field for index in cls._meta.indexes for field, _ in index.fields_orders] fields += [include for index in cls._meta.indexes for include in index.include] errors.extend(cls._check_local_fields(fields, 'indexes')) @@ -1927,6 +1941,25 @@ class Model(metaclass=ModelBase): id='models.W038', ) ) + if not ( + connection.features.supports_covering_indexes or + 'supports_covering_indexes' in cls._meta.required_db_features + ) and any( + isinstance(constraint, UniqueConstraint) and constraint.include + for constraint in cls._meta.constraints + ): + errors.append( + checks.Warning( + '%s does not support unique constraints with non-key ' + 'columns.' % connection.display_name, + hint=( + "A constraint won't be created. Silence this " + "warning if you don't care about it." + ), + obj=cls, + id='models.W039', + ) + ) fields = chain.from_iterable( (*constraint.fields, *constraint.include) for constraint in cls._meta.constraints if isinstance(constraint, UniqueConstraint) diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index d5aeca2d8c5..792c3936f6f 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -360,6 +360,10 @@ Models * **models.W037**: ```` does not support indexes with conditions. * **models.W038**: ```` does not support deferrable unique constraints. +* **models.W039**: ```` does not support unique constraints with + non-key columns. +* **models.W040**: ```` does not support indexes with non-key + columns. Security -------- diff --git a/tests/constraints/models.py b/tests/constraints/models.py index 947d1b4ff19..1460673a180 100644 --- a/tests/constraints/models.py +++ b/tests/constraints/models.py @@ -88,6 +88,7 @@ class UniqueConstraintInclude(models.Model): class Meta: required_db_features = { 'supports_table_check_constraints', + 'supports_covering_indexes', } constraints = [ models.UniqueConstraint( diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index 374eff60287..78757058608 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -375,6 +375,51 @@ class IndexesTests(TestCase): self.assertEqual(Model.check(databases=self.databases), []) + def test_index_with_include(self): + class Model(models.Model): + age = models.IntegerField() + + class Meta: + indexes = [ + models.Index( + fields=['age'], + name='index_age_include_id', + include=['id'], + ), + ] + + errors = Model.check(databases=self.databases) + expected = [] if connection.features.supports_covering_indexes else [ + Warning( + '%s does not support indexes with non-key columns.' + % connection.display_name, + hint=( + "Non-key columns will be ignored. Silence this warning if " + "you don't care about it." + ), + obj=Model, + id='models.W040', + ) + ] + self.assertEqual(errors, expected) + + def test_index_with_include_required_db_features(self): + class Model(models.Model): + age = models.IntegerField() + + class Meta: + required_db_features = {'supports_covering_indexes'} + indexes = [ + models.Index( + fields=['age'], + name='index_age_include_id', + include=['id'], + ), + ] + + self.assertEqual(Model.check(databases=self.databases), []) + + @skipUnlessDBFeature('supports_covering_indexes') def test_index_include_pointing_to_missing_field(self): class Model(models.Model): class Meta: @@ -390,6 +435,7 @@ class IndexesTests(TestCase): ), ]) + @skipUnlessDBFeature('supports_covering_indexes') def test_index_include_pointing_to_m2m_field(self): class Model(models.Model): m2m = models.ManyToManyField('self') @@ -406,6 +452,7 @@ class IndexesTests(TestCase): ), ]) + @skipUnlessDBFeature('supports_covering_indexes') def test_index_include_pointing_to_non_local_field(self): class Parent(models.Model): field1 = models.IntegerField() @@ -428,6 +475,7 @@ class IndexesTests(TestCase): ), ]) + @skipUnlessDBFeature('supports_covering_indexes') def test_index_include_pointing_to_fk(self): class Target(models.Model): pass @@ -1641,6 +1689,51 @@ class ConstraintsTests(TestCase): self.assertEqual(Model.check(databases=self.databases), []) + def test_unique_constraint_with_include(self): + class Model(models.Model): + age = models.IntegerField() + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['age'], + name='unique_age_include_id', + include=['id'], + ), + ] + + errors = Model.check(databases=self.databases) + expected = [] if connection.features.supports_covering_indexes else [ + Warning( + '%s does not support unique constraints with non-key columns.' + % connection.display_name, + hint=( + "A constraint won't be created. Silence this warning if " + "you don't care about it." + ), + obj=Model, + id='models.W039', + ), + ] + self.assertEqual(errors, expected) + + def test_unique_constraint_with_include_required_db_features(self): + class Model(models.Model): + age = models.IntegerField() + + class Meta: + required_db_features = {'supports_covering_indexes'} + constraints = [ + models.UniqueConstraint( + fields=['age'], + name='unique_age_include_id', + include=['id'], + ), + ] + + self.assertEqual(Model.check(databases=self.databases), []) + + @skipUnlessDBFeature('supports_covering_indexes') def test_unique_constraint_include_pointing_to_missing_field(self): class Model(models.Model): class Meta: @@ -1661,6 +1754,7 @@ class ConstraintsTests(TestCase): ), ]) + @skipUnlessDBFeature('supports_covering_indexes') def test_unique_constraint_include_pointing_to_m2m_field(self): class Model(models.Model): m2m = models.ManyToManyField('self') @@ -1683,6 +1777,7 @@ class ConstraintsTests(TestCase): ), ]) + @skipUnlessDBFeature('supports_covering_indexes') def test_unique_constraint_include_pointing_to_non_local_field(self): class Parent(models.Model): field1 = models.IntegerField() @@ -1709,6 +1804,7 @@ class ConstraintsTests(TestCase): ), ]) + @skipUnlessDBFeature('supports_covering_indexes') def test_unique_constraint_include_pointing_to_fk(self): class Target(models.Model): pass