Refs #30913 -- Added system checks for covering indexes and unique constraints support.

This commit is contained in:
Mariusz Felisiak 2020-06-03 06:44:05 +02:00
parent 8c7992f658
commit f83b44075d
4 changed files with 138 additions and 4 deletions

View File

@ -1614,12 +1614,10 @@ 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_partial_indexes or connection.features.supports_partial_indexes or
'supports_partial_indexes' in cls._meta.required_db_features 'supports_partial_indexes' in cls._meta.required_db_features
): ) and any(index.condition is not None for index in cls._meta.indexes):
continue
if any(index.condition is not None for index in cls._meta.indexes):
errors.append( errors.append(
checks.Warning( checks.Warning(
'%s does not support indexes with conditions.' '%s does not support indexes with conditions.'
@ -1632,6 +1630,22 @@ class Model(metaclass=ModelBase):
id='models.W037', 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 = [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] fields += [include for index in cls._meta.indexes for include in index.include]
errors.extend(cls._check_local_fields(fields, 'indexes')) errors.extend(cls._check_local_fields(fields, 'indexes'))
@ -1927,6 +1941,25 @@ class Model(metaclass=ModelBase):
id='models.W038', 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( fields = chain.from_iterable(
(*constraint.fields, *constraint.include) (*constraint.fields, *constraint.include)
for constraint in cls._meta.constraints if isinstance(constraint, UniqueConstraint) for constraint in cls._meta.constraints if isinstance(constraint, UniqueConstraint)

View File

@ -360,6 +360,10 @@ Models
* **models.W037**: ``<database>`` does not support indexes with conditions. * **models.W037**: ``<database>`` does not support indexes with conditions.
* **models.W038**: ``<database>`` does not support deferrable unique * **models.W038**: ``<database>`` does not support deferrable unique
constraints. constraints.
* **models.W039**: ``<database>`` does not support unique constraints with
non-key columns.
* **models.W040**: ``<database>`` does not support indexes with non-key
columns.
Security Security
-------- --------

View File

@ -88,6 +88,7 @@ class UniqueConstraintInclude(models.Model):
class Meta: class Meta:
required_db_features = { required_db_features = {
'supports_table_check_constraints', 'supports_table_check_constraints',
'supports_covering_indexes',
} }
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(

View File

@ -375,6 +375,51 @@ class IndexesTests(TestCase):
self.assertEqual(Model.check(databases=self.databases), []) 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): def test_index_include_pointing_to_missing_field(self):
class Model(models.Model): class Model(models.Model):
class Meta: class Meta:
@ -390,6 +435,7 @@ class IndexesTests(TestCase):
), ),
]) ])
@skipUnlessDBFeature('supports_covering_indexes')
def test_index_include_pointing_to_m2m_field(self): def test_index_include_pointing_to_m2m_field(self):
class Model(models.Model): class Model(models.Model):
m2m = models.ManyToManyField('self') 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): def test_index_include_pointing_to_non_local_field(self):
class Parent(models.Model): class Parent(models.Model):
field1 = models.IntegerField() field1 = models.IntegerField()
@ -428,6 +475,7 @@ class IndexesTests(TestCase):
), ),
]) ])
@skipUnlessDBFeature('supports_covering_indexes')
def test_index_include_pointing_to_fk(self): def test_index_include_pointing_to_fk(self):
class Target(models.Model): class Target(models.Model):
pass pass
@ -1641,6 +1689,51 @@ class ConstraintsTests(TestCase):
self.assertEqual(Model.check(databases=self.databases), []) 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): def test_unique_constraint_include_pointing_to_missing_field(self):
class Model(models.Model): class Model(models.Model):
class Meta: class Meta:
@ -1661,6 +1754,7 @@ class ConstraintsTests(TestCase):
), ),
]) ])
@skipUnlessDBFeature('supports_covering_indexes')
def test_unique_constraint_include_pointing_to_m2m_field(self): def test_unique_constraint_include_pointing_to_m2m_field(self):
class Model(models.Model): class Model(models.Model):
m2m = models.ManyToManyField('self') 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): def test_unique_constraint_include_pointing_to_non_local_field(self):
class Parent(models.Model): class Parent(models.Model):
field1 = models.IntegerField() field1 = models.IntegerField()
@ -1709,6 +1804,7 @@ class ConstraintsTests(TestCase):
), ),
]) ])
@skipUnlessDBFeature('supports_covering_indexes')
def test_unique_constraint_include_pointing_to_fk(self): def test_unique_constraint_include_pointing_to_fk(self):
class Target(models.Model): class Target(models.Model):
pass pass