mirror of
https://github.com/django/django.git
synced 2025-07-04 01:39:20 +00:00
Fixed #36273 -- Moved Index system checks from Model to Index.check().
This commit is contained in:
parent
8620a3b0c7
commit
8638d8bf74
@ -2115,93 +2115,13 @@ class Model(AltersData, metaclass=ModelBase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_indexes(cls, databases):
|
def _check_indexes(cls, databases):
|
||||||
"""Check fields, names, and conditions of indexes."""
|
|
||||||
errors = []
|
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:
|
for db in databases:
|
||||||
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 not (
|
for index in cls._meta.indexes:
|
||||||
connection.features.supports_partial_indexes
|
errors.extend(index.check(cls, connection))
|
||||||
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"))
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from types import NoneType
|
from types import NoneType
|
||||||
|
|
||||||
|
from django.core import checks
|
||||||
from django.db.backends.utils import names_digest, split_identifier
|
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.expressions import Col, ExpressionList, F, Func, OrderBy
|
||||||
from django.db.models.functions import Collate
|
from django.db.models.functions import Collate
|
||||||
@ -82,6 +83,92 @@ class Index:
|
|||||||
def contains_expressions(self):
|
def contains_expressions(self):
|
||||||
return bool(self.expressions)
|
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):
|
def _get_condition_sql(self, model, schema_editor):
|
||||||
if self.condition is None:
|
if self.condition is None:
|
||||||
return None
|
return None
|
||||||
|
@ -175,7 +175,7 @@ class IndexesTests(TestCase):
|
|||||||
indexes = [models.Index(fields=["missing_field"], name="name")]
|
indexes = [models.Index(fields=["missing_field"], name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to the nonexistent field 'missing_field'.",
|
"'indexes' refers to the nonexistent field 'missing_field'.",
|
||||||
@ -193,7 +193,7 @@ class IndexesTests(TestCase):
|
|||||||
indexes = [models.Index(fields=["m2m"], name="name")]
|
indexes = [models.Index(fields=["m2m"], name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to a ManyToManyField 'm2m', but "
|
"'indexes' refers to a ManyToManyField 'm2m', but "
|
||||||
@ -215,7 +215,7 @@ class IndexesTests(TestCase):
|
|||||||
indexes = [models.Index(fields=["field2", "field1"], name="name")]
|
indexes = [models.Index(fields=["field2", "field1"], name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Bar.check(),
|
Bar.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to field 'field1' which is not local to "
|
"'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")
|
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):
|
def test_pointing_to_composite_primary_key(self):
|
||||||
class Model(models.Model):
|
class Model(models.Model):
|
||||||
@ -256,7 +256,7 @@ class IndexesTests(TestCase):
|
|||||||
indexes = [models.Index(fields=["pk", "name"], name="name")]
|
indexes = [models.Index(fields=["pk", "name"], name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to a CompositePrimaryKey 'pk', but "
|
"'indexes' refers to a CompositePrimaryKey 'pk', but "
|
||||||
@ -276,7 +276,7 @@ class IndexesTests(TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"The index name '%sindex_name' cannot start with an "
|
"The index name '%sindex_name' cannot start with an "
|
||||||
@ -296,7 +296,7 @@ class IndexesTests(TestCase):
|
|||||||
indexes = [models.Index(fields=["id"], name=index_name)]
|
indexes = [models.Index(fields=["id"], name=index_name)]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"The index name '%s' cannot be longer than 30 characters."
|
"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")]
|
indexes = [models.Index(fields=["name"], include=["pk"], name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to a CompositePrimaryKey 'pk', but "
|
"'indexes' refers to a CompositePrimaryKey 'pk', but "
|
||||||
@ -539,6 +539,7 @@ class IndexesTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(Model.check(databases=self.databases), [])
|
self.assertEqual(Model.check(databases=self.databases), [])
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_expression_indexes")
|
||||||
def test_func_index_complex_expression_custom_lookup(self):
|
def test_func_index_complex_expression_custom_lookup(self):
|
||||||
class Model(models.Model):
|
class Model(models.Model):
|
||||||
height = models.IntegerField()
|
height = models.IntegerField()
|
||||||
@ -554,15 +555,16 @@ class IndexesTests(TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
with register_lookup(models.IntegerField, Abs):
|
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):
|
def test_func_index_pointing_to_missing_field(self):
|
||||||
class Model(models.Model):
|
class Model(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
indexes = [models.Index(Lower("missing_field").desc(), name="name")]
|
indexes = [models.Index(Lower("missing_field").desc(), name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to the nonexistent field 'missing_field'.",
|
"'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):
|
def test_func_index_pointing_to_missing_field_nested(self):
|
||||||
class Model(models.Model):
|
class Model(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -580,7 +583,7 @@ class IndexesTests(TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to the nonexistent field 'missing_field'.",
|
"'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):
|
def test_func_index_pointing_to_m2m_field(self):
|
||||||
class Model(models.Model):
|
class Model(models.Model):
|
||||||
m2m = models.ManyToManyField("self")
|
m2m = models.ManyToManyField("self")
|
||||||
@ -598,7 +602,7 @@ class IndexesTests(TestCase):
|
|||||||
indexes = [models.Index(Lower("m2m"), name="name")]
|
indexes = [models.Index(Lower("m2m"), name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to a ManyToManyField 'm2m', but "
|
"'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):
|
def test_func_index_pointing_to_non_local_field(self):
|
||||||
class Foo(models.Model):
|
class Foo(models.Model):
|
||||||
field1 = models.CharField(max_length=15)
|
field1 = models.CharField(max_length=15)
|
||||||
@ -618,7 +623,7 @@ class IndexesTests(TestCase):
|
|||||||
indexes = [models.Index(Lower("field1"), name="name")]
|
indexes = [models.Index(Lower("field1"), name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Bar.check(),
|
Bar.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to field 'field1' which is not local to "
|
"'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):
|
def test_func_index_pointing_to_fk(self):
|
||||||
class Foo(models.Model):
|
class Foo(models.Model):
|
||||||
pass
|
pass
|
||||||
@ -643,8 +649,9 @@ class IndexesTests(TestCase):
|
|||||||
models.Index(Lower("foo_1_id"), Lower("foo_2"), name="index_name"),
|
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):
|
def test_func_index_pointing_to_composite_primary_key(self):
|
||||||
class Model(models.Model):
|
class Model(models.Model):
|
||||||
pk = models.CompositePrimaryKey("version", "name")
|
pk = models.CompositePrimaryKey("version", "name")
|
||||||
@ -655,7 +662,7 @@ class IndexesTests(TestCase):
|
|||||||
indexes = [models.Index(Abs("pk"), name="name")]
|
indexes = [models.Index(Abs("pk"), name="name")]
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Model.check(),
|
Model.check(databases=self.databases),
|
||||||
[
|
[
|
||||||
Error(
|
Error(
|
||||||
"'indexes' refers to a CompositePrimaryKey 'pk', but "
|
"'indexes' refers to a CompositePrimaryKey 'pk', but "
|
||||||
|
Loading…
x
Reference in New Issue
Block a user