1
0
mirror of https://github.com/django/django.git synced 2025-06-08 21:19:13 +00:00

Fixed #36273 -- Moved Index system checks from Model to Index.check().

This commit is contained in:
Tim Graham 2025-04-19 20:32:58 -04:00 committed by Sarah Boyce
parent 8620a3b0c7
commit 8638d8bf74
3 changed files with 111 additions and 97 deletions

View File

@ -2115,93 +2115,13 @@ class Model(AltersData, metaclass=ModelBase):
@classmethod
def _check_indexes(cls, databases):
"""Check fields, names, and conditions of indexes."""
errors = []
references = set()
for index in cls._meta.indexes:
# Index name can't start with an underscore or a number, restricted
# for cross-database compatibility with Oracle.
if index.name[0] == "_" or index.name[0].isdigit():
errors.append(
checks.Error(
"The index name '%s' cannot start with an underscore "
"or a number." % index.name,
obj=cls,
id="models.E033",
),
)
if len(index.name) > index.max_name_length:
errors.append(
checks.Error(
"The index name '%s' cannot be longer than %d "
"characters." % (index.name, index.max_name_length),
obj=cls,
id="models.E034",
),
)
if index.contains_expressions:
for expression in index.expressions:
references.update(
ref[0] for ref in cls._get_expr_references(expression)
)
for db in databases:
if not router.allow_migrate_model(db, cls):
continue
connection = connections[db]
if not (
connection.features.supports_partial_indexes
or "supports_partial_indexes" in cls._meta.required_db_features
) and 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",
)
)
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",
)
)
if not (
connection.features.supports_expression_indexes
or "supports_expression_indexes" in cls._meta.required_db_features
) and any(index.contains_expressions for index in cls._meta.indexes):
errors.append(
checks.Warning(
"%s does not support indexes on expressions."
% connection.display_name,
hint=(
"An index won't be created. Silence this warning "
"if you don't care about it."
),
obj=cls,
id="models.W043",
)
)
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 += references
errors.extend(cls._check_local_fields(fields, "indexes"))
for index in cls._meta.indexes:
errors.extend(index.check(cls, connection))
return errors
@classmethod

View File

@ -1,5 +1,6 @@
from types import NoneType
from django.core import checks
from django.db.backends.utils import names_digest, split_identifier
from django.db.models.expressions import Col, ExpressionList, F, Func, OrderBy
from django.db.models.functions import Collate
@ -82,6 +83,92 @@ class Index:
def contains_expressions(self):
return bool(self.expressions)
def check(self, model, connection):
"""Check fields, names, and conditions of indexes."""
errors = []
# Index name can't start with an underscore or a number (restricted
# for cross-database compatibility with Oracle)
if self.name[0] == "_" or self.name[0].isdigit():
errors.append(
checks.Error(
"The index name '%s' cannot start with an underscore "
"or a number." % self.name,
obj=model,
id="models.E033",
),
)
if len(self.name) > self.max_name_length:
errors.append(
checks.Error(
"The index name '%s' cannot be longer than %d "
"characters." % (self.name, self.max_name_length),
obj=model,
id="models.E034",
),
)
references = set()
if self.contains_expressions:
for expression in self.expressions:
references.update(
ref[0] for ref in model._get_expr_references(expression)
)
errors.extend(
model._check_local_fields(
{*self.fields, *self.include, *references}, "indexes"
)
)
# Database-feature checks:
required_db_features = model._meta.required_db_features
if not (
connection.features.supports_partial_indexes
or "supports_partial_indexes" in required_db_features
) and any(self.condition is not None for index in model._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=model,
id="models.W037",
)
)
if not (
connection.features.supports_covering_indexes
or "supports_covering_indexes" in required_db_features
) and any(index.include for index in model._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=model,
id="models.W040",
)
)
if not (
connection.features.supports_expression_indexes
or "supports_expression_indexes" in required_db_features
) and any(index.contains_expressions for index in model._meta.indexes):
errors.append(
checks.Warning(
"%s does not support indexes on expressions."
% connection.display_name,
hint=(
"An index won't be created. Silence this warning "
"if you don't care about it."
),
obj=model,
id="models.W043",
)
)
return errors
def _get_condition_sql(self, model, schema_editor):
if self.condition is None:
return None

View File

@ -175,7 +175,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(fields=["missing_field"], name="name")]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"'indexes' refers to the nonexistent field 'missing_field'.",
@ -193,7 +193,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(fields=["m2m"], name="name")]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"'indexes' refers to a ManyToManyField 'm2m', but "
@ -215,7 +215,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(fields=["field2", "field1"], name="name")]
self.assertEqual(
Bar.check(),
Bar.check(databases=self.databases),
[
Error(
"'indexes' refers to field 'field1' which is not local to "
@ -244,7 +244,7 @@ class IndexesTests(TestCase):
models.Index(fields=["foo_1_id", "foo_2"], name="index_name")
]
self.assertEqual(Bar.check(), [])
self.assertEqual(Bar.check(databases=self.databases), [])
def test_pointing_to_composite_primary_key(self):
class Model(models.Model):
@ -256,7 +256,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(fields=["pk", "name"], name="name")]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"'indexes' refers to a CompositePrimaryKey 'pk', but "
@ -276,7 +276,7 @@ class IndexesTests(TestCase):
]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"The index name '%sindex_name' cannot start with an "
@ -296,7 +296,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(fields=["id"], name=index_name)]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"The index name '%s' cannot be longer than 30 characters."
@ -499,7 +499,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(fields=["name"], include=["pk"], name="name")]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"'indexes' refers to a CompositePrimaryKey 'pk', but "
@ -539,6 +539,7 @@ class IndexesTests(TestCase):
self.assertEqual(Model.check(databases=self.databases), [])
@skipUnlessDBFeature("supports_expression_indexes")
def test_func_index_complex_expression_custom_lookup(self):
class Model(models.Model):
height = models.IntegerField()
@ -554,15 +555,16 @@ class IndexesTests(TestCase):
]
with register_lookup(models.IntegerField, Abs):
self.assertEqual(Model.check(), [])
self.assertEqual(Model.check(databases=self.databases), [])
@skipUnlessDBFeature("supports_expression_indexes")
def test_func_index_pointing_to_missing_field(self):
class Model(models.Model):
class Meta:
indexes = [models.Index(Lower("missing_field").desc(), name="name")]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"'indexes' refers to the nonexistent field 'missing_field'.",
@ -572,6 +574,7 @@ class IndexesTests(TestCase):
],
)
@skipUnlessDBFeature("supports_expression_indexes")
def test_func_index_pointing_to_missing_field_nested(self):
class Model(models.Model):
class Meta:
@ -580,7 +583,7 @@ class IndexesTests(TestCase):
]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"'indexes' refers to the nonexistent field 'missing_field'.",
@ -590,6 +593,7 @@ class IndexesTests(TestCase):
],
)
@skipUnlessDBFeature("supports_expression_indexes")
def test_func_index_pointing_to_m2m_field(self):
class Model(models.Model):
m2m = models.ManyToManyField("self")
@ -598,7 +602,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(Lower("m2m"), name="name")]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"'indexes' refers to a ManyToManyField 'm2m', but "
@ -609,6 +613,7 @@ class IndexesTests(TestCase):
],
)
@skipUnlessDBFeature("supports_expression_indexes")
def test_func_index_pointing_to_non_local_field(self):
class Foo(models.Model):
field1 = models.CharField(max_length=15)
@ -618,7 +623,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(Lower("field1"), name="name")]
self.assertEqual(
Bar.check(),
Bar.check(databases=self.databases),
[
Error(
"'indexes' refers to field 'field1' which is not local to "
@ -630,6 +635,7 @@ class IndexesTests(TestCase):
],
)
@skipUnlessDBFeature("supports_expression_indexes")
def test_func_index_pointing_to_fk(self):
class Foo(models.Model):
pass
@ -643,8 +649,9 @@ class IndexesTests(TestCase):
models.Index(Lower("foo_1_id"), Lower("foo_2"), name="index_name"),
]
self.assertEqual(Bar.check(), [])
self.assertEqual(Bar.check(databases=self.databases), [])
@skipUnlessDBFeature("supports_expression_indexes")
def test_func_index_pointing_to_composite_primary_key(self):
class Model(models.Model):
pk = models.CompositePrimaryKey("version", "name")
@ -655,7 +662,7 @@ class IndexesTests(TestCase):
indexes = [models.Index(Abs("pk"), name="name")]
self.assertEqual(
Model.check(),
Model.check(databases=self.databases),
[
Error(
"'indexes' refers to a CompositePrimaryKey 'pk', but "