diff --git a/django/db/models/base.py b/django/db/models/base.py index 919371540d..0f8a8c50f3 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -19,7 +19,7 @@ from django.db.models import ( NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, Value, ) from django.db.models.constants import LOOKUP_SEP -from django.db.models.constraints import CheckConstraint +from django.db.models.constraints import CheckConstraint, UniqueConstraint from django.db.models.deletion import CASCADE, Collector from django.db.models.fields.related import ( ForeignObjectRel, OneToOneField, lazy_related_operation, resolve_relation, @@ -1276,7 +1276,7 @@ class Model(metaclass=ModelBase): errors += [ *cls._check_index_together(), *cls._check_unique_together(), - *cls._check_indexes(), + *cls._check_indexes(databases), *cls._check_ordering(), *cls._check_constraints(databases), ] @@ -1585,8 +1585,8 @@ class Model(metaclass=ModelBase): return errors @classmethod - def _check_indexes(cls): - """Check the fields and names of indexes.""" + def _check_indexes(cls, databases): + """Check fields, names, and conditions of indexes.""" errors = [] for index in cls._meta.indexes: # Index name can't start with an underscore or a number, restricted @@ -1609,6 +1609,28 @@ class Model(metaclass=ModelBase): id='models.E034', ), ) + for db in databases: + if not router.allow_migrate_model(db, cls): + continue + connection = connections[db] + if ( + 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): + errors.append( + checks.Warning( + '%s does not support indexes with conditions.' + % connection.display_name, + hint=( + "Conditions will be ignored. Silence this warning " + "if you don't care about it." + ), + obj=cls, + id='models.W037', + ) + ) fields = [field for index in cls._meta.indexes for field, _ in index.fields_orders] errors.extend(cls._check_local_fields(fields, 'indexes')) return errors @@ -1845,12 +1867,13 @@ class Model(metaclass=ModelBase): if not router.allow_migrate_model(db, cls): continue connection = connections[db] - if ( + if not ( connection.features.supports_table_check_constraints or 'supports_table_check_constraints' in cls._meta.required_db_features + ) and any( + isinstance(constraint, CheckConstraint) + for constraint in cls._meta.constraints ): - continue - if any(isinstance(constraint, CheckConstraint) for constraint in cls._meta.constraints): errors.append( checks.Warning( '%s does not support check constraints.' % connection.display_name, @@ -1862,6 +1885,25 @@ class Model(metaclass=ModelBase): id='models.W027', ) ) + if not ( + connection.features.supports_partial_indexes or + 'supports_partial_indexes' in cls._meta.required_db_features + ) and any( + isinstance(constraint, UniqueConstraint) and constraint.condition is not None + for constraint in cls._meta.constraints + ): + errors.append( + checks.Warning( + '%s does not support unique constraints with ' + 'conditions.' % connection.display_name, + hint=( + "A constraint won't be created. Silence this " + "warning if you don't care about it." + ), + obj=cls, + id='models.W036', + ) + ) return errors diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index ec3735598a..d9bfdb168c 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -349,6 +349,9 @@ Models ```` characters. * **models.W035**: ``db_table`` ```` is used by multiple models: ````. +* **models.W036**: ```` does not support unique constraints with + conditions. +* **models.W037**: ```` does not support indexes with conditions. Security -------- diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index 6090724adb..f3f0bc8bd5 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -234,7 +234,7 @@ class UniqueTogetherTests(SimpleTestCase): @isolate_apps('invalid_models_tests') -class IndexesTests(SimpleTestCase): +class IndexesTests(TestCase): def test_pointing_to_missing_field(self): class Model(models.Model): @@ -331,6 +331,50 @@ class IndexesTests(SimpleTestCase): ), ]) + def test_index_with_condition(self): + class Model(models.Model): + age = models.IntegerField() + + class Meta: + indexes = [ + models.Index( + fields=['age'], + name='index_age_gte_10', + condition=models.Q(age__gte=10), + ), + ] + + errors = Model.check(databases=self.databases) + expected = [] if connection.features.supports_partial_indexes else [ + Warning( + '%s does not support indexes with conditions.' + % connection.display_name, + hint=( + "Conditions will be ignored. Silence this warning if you " + "don't care about it." + ), + obj=Model, + id='models.W037', + ) + ] + self.assertEqual(errors, expected) + + def test_index_with_condition_required_db_features(self): + class Model(models.Model): + age = models.IntegerField() + + class Meta: + required_db_features = {'supports_partial_indexes'} + indexes = [ + models.Index( + fields=['age'], + name='index_age_gte_10', + condition=models.Q(age__gte=10), + ), + ] + + self.assertEqual(Model.check(databases=self.databases), []) + @isolate_apps('invalid_models_tests') class FieldNamesTests(TestCase): @@ -1325,5 +1369,48 @@ class ConstraintsTests(TestCase): class Meta: required_db_features = {'supports_table_check_constraints'} constraints = [models.CheckConstraint(check=models.Q(age__gte=18), name='is_adult')] + self.assertEqual(Model.check(databases=self.databases), []) + + def test_unique_constraint_with_condition(self): + class Model(models.Model): + age = models.IntegerField() + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['age'], + name='unique_age_gte_100', + condition=models.Q(age__gte=100), + ), + ] + + errors = Model.check(databases=self.databases) + expected = [] if connection.features.supports_partial_indexes else [ + Warning( + '%s does not support unique constraints with conditions.' + % connection.display_name, + hint=( + "A constraint won't be created. Silence this warning if " + "you don't care about it." + ), + obj=Model, + id='models.W036', + ), + ] + self.assertEqual(errors, expected) + + def test_unique_constraint_with_condition_required_db_features(self): + class Model(models.Model): + age = models.IntegerField() + + class Meta: + required_db_features = {'supports_partial_indexes'} + constraints = [ + models.UniqueConstraint( + fields=['age'], + name='unique_age_gte_100', + condition=models.Q(age__gte=100), + ), + ] self.assertEqual(Model.check(databases=self.databases), [])