Browse Source

Fixed #31351 -- Added system checks for partial indexes and unique constraints support.

Ichlasul Affan 5 years ago
parent
commit
53d229ff63
3 changed files with 140 additions and 8 deletions
  1. 49 7
      django/db/models/base.py
  2. 3 0
      docs/ref/checks.txt
  3. 88 1
      tests/invalid_models_tests/test_models.py

+ 49 - 7
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
 
 

+ 3 - 0
docs/ref/checks.txt

@@ -349,6 +349,9 @@ Models
   ``<max_length>`` characters.
 * **models.W035**: ``db_table`` ``<db_table>`` is used by multiple models:
   ``<model list>``.
+* **models.W036**: ``<database>`` does not support unique constraints with
+  conditions.
+* **models.W037**: ``<database>`` does not support indexes with conditions.
 
 Security
 --------

+ 88 - 1
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), [])