diff --git a/django/db/models/base.py b/django/db/models/base.py index eaedd48748..72ba0471a4 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -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 diff --git a/django/db/models/indexes.py b/django/db/models/indexes.py index 7bea0a663a..176f719fe5 100644 --- a/django/db/models/indexes.py +++ b/django/db/models/indexes.py @@ -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 diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py index c44fe56f89..faa0db9da7 100644 --- a/tests/invalid_models_tests/test_models.py +++ b/tests/invalid_models_tests/test_models.py @@ -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 "