summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Graham <timograham@gmail.com>2025-04-19 20:32:58 -0400
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-05-14 16:00:53 +0200
commit8638d8bf74c1a58302c97d4436ad2eb08438145b (patch)
treecaf3ceeb75c7ac1becf9563ca01844f537f99486
parent8620a3b0c79e4e8098b88f1176ed26fad0bf6c5c (diff)
Fixed #36273 -- Moved Index system checks from Model to Index.check().
-rw-r--r--django/db/models/base.py84
-rw-r--r--django/db/models/indexes.py87
-rw-r--r--tests/invalid_models_tests/test_models.py37
3 files changed, 111 insertions, 97 deletions
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 "