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

This commit is contained in:
Ichlasul Affan 2020-03-12 02:01:32 +07:00 committed by Mariusz Felisiak
parent e8d3088925
commit 53d229ff63
3 changed files with 140 additions and 8 deletions

View File

@ -19,7 +19,7 @@ from django.db.models import (
NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, Value, NOT_PROVIDED, ExpressionWrapper, IntegerField, Max, Value,
) )
from django.db.models.constants import LOOKUP_SEP 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.deletion import CASCADE, Collector
from django.db.models.fields.related import ( from django.db.models.fields.related import (
ForeignObjectRel, OneToOneField, lazy_related_operation, resolve_relation, ForeignObjectRel, OneToOneField, lazy_related_operation, resolve_relation,
@ -1276,7 +1276,7 @@ class Model(metaclass=ModelBase):
errors += [ errors += [
*cls._check_index_together(), *cls._check_index_together(),
*cls._check_unique_together(), *cls._check_unique_together(),
*cls._check_indexes(), *cls._check_indexes(databases),
*cls._check_ordering(), *cls._check_ordering(),
*cls._check_constraints(databases), *cls._check_constraints(databases),
] ]
@ -1585,8 +1585,8 @@ class Model(metaclass=ModelBase):
return errors return errors
@classmethod @classmethod
def _check_indexes(cls): def _check_indexes(cls, databases):
"""Check the fields and names of indexes.""" """Check fields, names, and conditions of indexes."""
errors = [] errors = []
for index in cls._meta.indexes: for index in cls._meta.indexes:
# Index name can't start with an underscore or a number, restricted # Index name can't start with an underscore or a number, restricted
@ -1609,6 +1609,28 @@ class Model(metaclass=ModelBase):
id='models.E034', 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] fields = [field for index in cls._meta.indexes for field, _ in index.fields_orders]
errors.extend(cls._check_local_fields(fields, 'indexes')) errors.extend(cls._check_local_fields(fields, 'indexes'))
return errors return errors
@ -1845,12 +1867,13 @@ class Model(metaclass=ModelBase):
if not router.allow_migrate_model(db, cls): if not router.allow_migrate_model(db, cls):
continue continue
connection = connections[db] connection = connections[db]
if ( if not (
connection.features.supports_table_check_constraints or connection.features.supports_table_check_constraints or
'supports_table_check_constraints' in cls._meta.required_db_features '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( errors.append(
checks.Warning( checks.Warning(
'%s does not support check constraints.' % connection.display_name, '%s does not support check constraints.' % connection.display_name,
@ -1862,6 +1885,25 @@ class Model(metaclass=ModelBase):
id='models.W027', 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 return errors

View File

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

View File

@ -234,7 +234,7 @@ class UniqueTogetherTests(SimpleTestCase):
@isolate_apps('invalid_models_tests') @isolate_apps('invalid_models_tests')
class IndexesTests(SimpleTestCase): class IndexesTests(TestCase):
def test_pointing_to_missing_field(self): def test_pointing_to_missing_field(self):
class Model(models.Model): 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') @isolate_apps('invalid_models_tests')
class FieldNamesTests(TestCase): class FieldNamesTests(TestCase):
@ -1325,5 +1369,48 @@ class ConstraintsTests(TestCase):
class Meta: class Meta:
required_db_features = {'supports_table_check_constraints'} required_db_features = {'supports_table_check_constraints'}
constraints = [models.CheckConstraint(check=models.Q(age__gte=18), name='is_adult')] 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), []) self.assertEqual(Model.check(databases=self.databases), [])