Skip to content

Commit 8638d8b

Browse files
timgrahamsarahboyce
authored andcommitted
Fixed #36273 -- Moved Index system checks from Model to Index.check().
1 parent 8620a3b commit 8638d8b

File tree

3 files changed

+111
-97
lines changed

3 files changed

+111
-97
lines changed

django/db/models/base.py

Lines changed: 2 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,93 +2115,13 @@ def _check_unique_together(cls):
21152115

21162116
@classmethod
21172117
def _check_indexes(cls, databases):
2118-
"""Check fields, names, and conditions of indexes."""
21192118
errors = []
2120-
references = set()
2121-
for index in cls._meta.indexes:
2122-
# Index name can't start with an underscore or a number, restricted
2123-
# for cross-database compatibility with Oracle.
2124-
if index.name[0] == "_" or index.name[0].isdigit():
2125-
errors.append(
2126-
checks.Error(
2127-
"The index name '%s' cannot start with an underscore "
2128-
"or a number." % index.name,
2129-
obj=cls,
2130-
id="models.E033",
2131-
),
2132-
)
2133-
if len(index.name) > index.max_name_length:
2134-
errors.append(
2135-
checks.Error(
2136-
"The index name '%s' cannot be longer than %d "
2137-
"characters." % (index.name, index.max_name_length),
2138-
obj=cls,
2139-
id="models.E034",
2140-
),
2141-
)
2142-
if index.contains_expressions:
2143-
for expression in index.expressions:
2144-
references.update(
2145-
ref[0] for ref in cls._get_expr_references(expression)
2146-
)
21472119
for db in databases:
21482120
if not router.allow_migrate_model(db, cls):
21492121
continue
21502122
connection = connections[db]
2151-
if not (
2152-
connection.features.supports_partial_indexes
2153-
or "supports_partial_indexes" in cls._meta.required_db_features
2154-
) and any(index.condition is not None for index in cls._meta.indexes):
2155-
errors.append(
2156-
checks.Warning(
2157-
"%s does not support indexes with conditions."
2158-
% connection.display_name,
2159-
hint=(
2160-
"Conditions will be ignored. Silence this warning "
2161-
"if you don't care about it."
2162-
),
2163-
obj=cls,
2164-
id="models.W037",
2165-
)
2166-
)
2167-
if not (
2168-
connection.features.supports_covering_indexes
2169-
or "supports_covering_indexes" in cls._meta.required_db_features
2170-
) and any(index.include for index in cls._meta.indexes):
2171-
errors.append(
2172-
checks.Warning(
2173-
"%s does not support indexes with non-key columns."
2174-
% connection.display_name,
2175-
hint=(
2176-
"Non-key columns will be ignored. Silence this "
2177-
"warning if you don't care about it."
2178-
),
2179-
obj=cls,
2180-
id="models.W040",
2181-
)
2182-
)
2183-
if not (
2184-
connection.features.supports_expression_indexes
2185-
or "supports_expression_indexes" in cls._meta.required_db_features
2186-
) and any(index.contains_expressions for index in cls._meta.indexes):
2187-
errors.append(
2188-
checks.Warning(
2189-
"%s does not support indexes on expressions."
2190-
% connection.display_name,
2191-
hint=(
2192-
"An index won't be created. Silence this warning "
2193-
"if you don't care about it."
2194-
),
2195-
obj=cls,
2196-
id="models.W043",
2197-
)
2198-
)
2199-
fields = [
2200-
field for index in cls._meta.indexes for field, _ in index.fields_orders
2201-
]
2202-
fields += [include for index in cls._meta.indexes for include in index.include]
2203-
fields += references
2204-
errors.extend(cls._check_local_fields(fields, "indexes"))
2123+
for index in cls._meta.indexes:
2124+
errors.extend(index.check(cls, connection))
22052125
return errors
22062126

22072127
@classmethod

django/db/models/indexes.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from types import NoneType
22

3+
from django.core import checks
34
from django.db.backends.utils import names_digest, split_identifier
45
from django.db.models.expressions import Col, ExpressionList, F, Func, OrderBy
56
from django.db.models.functions import Collate
@@ -82,6 +83,92 @@ def __init__(
8283
def contains_expressions(self):
8384
return bool(self.expressions)
8485

86+
def check(self, model, connection):
87+
"""Check fields, names, and conditions of indexes."""
88+
errors = []
89+
# Index name can't start with an underscore or a number (restricted
90+
# for cross-database compatibility with Oracle)
91+
if self.name[0] == "_" or self.name[0].isdigit():
92+
errors.append(
93+
checks.Error(
94+
"The index name '%s' cannot start with an underscore "
95+
"or a number." % self.name,
96+
obj=model,
97+
id="models.E033",
98+
),
99+
)
100+
if len(self.name) > self.max_name_length:
101+
errors.append(
102+
checks.Error(
103+
"The index name '%s' cannot be longer than %d "
104+
"characters." % (self.name, self.max_name_length),
105+
obj=model,
106+
id="models.E034",
107+
),
108+
)
109+
references = set()
110+
if self.contains_expressions:
111+
for expression in self.expressions:
112+
references.update(
113+
ref[0] for ref in model._get_expr_references(expression)
114+
)
115+
errors.extend(
116+
model._check_local_fields(
117+
{*self.fields, *self.include, *references}, "indexes"
118+
)
119+
)
120+
# Database-feature checks:
121+
required_db_features = model._meta.required_db_features
122+
if not (
123+
connection.features.supports_partial_indexes
124+
or "supports_partial_indexes" in required_db_features
125+
) and any(self.condition is not None for index in model._meta.indexes):
126+
errors.append(
127+
checks.Warning(
128+
"%s does not support indexes with conditions."
129+
% connection.display_name,
130+
hint=(
131+
"Conditions will be ignored. Silence this warning "
132+
"if you don't care about it."
133+
),
134+
obj=model,
135+
id="models.W037",
136+
)
137+
)
138+
if not (
139+
connection.features.supports_covering_indexes
140+
or "supports_covering_indexes" in required_db_features
141+
) and any(index.include for index in model._meta.indexes):
142+
errors.append(
143+
checks.Warning(
144+
"%s does not support indexes with non-key columns."
145+
% connection.display_name,
146+
hint=(
147+
"Non-key columns will be ignored. Silence this "
148+
"warning if you don't care about it."
149+
),
150+
obj=model,
151+
id="models.W040",
152+
)
153+
)
154+
if not (
155+
connection.features.supports_expression_indexes
156+
or "supports_expression_indexes" in required_db_features
157+
) and any(index.contains_expressions for index in model._meta.indexes):
158+
errors.append(
159+
checks.Warning(
160+
"%s does not support indexes on expressions."
161+
% connection.display_name,
162+
hint=(
163+
"An index won't be created. Silence this warning "
164+
"if you don't care about it."
165+
),
166+
obj=model,
167+
id="models.W043",
168+
)
169+
)
170+
return errors
171+
85172
def _get_condition_sql(self, model, schema_editor):
86173
if self.condition is None:
87174
return None

tests/invalid_models_tests/test_models.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ class Meta:
175175
indexes = [models.Index(fields=["missing_field"], name="name")]
176176

177177
self.assertEqual(
178-
Model.check(),
178+
Model.check(databases=self.databases),
179179
[
180180
Error(
181181
"'indexes' refers to the nonexistent field 'missing_field'.",
@@ -193,7 +193,7 @@ class Meta:
193193
indexes = [models.Index(fields=["m2m"], name="name")]
194194

195195
self.assertEqual(
196-
Model.check(),
196+
Model.check(databases=self.databases),
197197
[
198198
Error(
199199
"'indexes' refers to a ManyToManyField 'm2m', but "
@@ -215,7 +215,7 @@ class Meta:
215215
indexes = [models.Index(fields=["field2", "field1"], name="name")]
216216

217217
self.assertEqual(
218-
Bar.check(),
218+
Bar.check(databases=self.databases),
219219
[
220220
Error(
221221
"'indexes' refers to field 'field1' which is not local to "
@@ -244,7 +244,7 @@ class Meta:
244244
models.Index(fields=["foo_1_id", "foo_2"], name="index_name")
245245
]
246246

247-
self.assertEqual(Bar.check(), [])
247+
self.assertEqual(Bar.check(databases=self.databases), [])
248248

249249
def test_pointing_to_composite_primary_key(self):
250250
class Model(models.Model):
@@ -256,7 +256,7 @@ class Meta:
256256
indexes = [models.Index(fields=["pk", "name"], name="name")]
257257

258258
self.assertEqual(
259-
Model.check(),
259+
Model.check(databases=self.databases),
260260
[
261261
Error(
262262
"'indexes' refers to a CompositePrimaryKey 'pk', but "
@@ -276,7 +276,7 @@ class Meta:
276276
]
277277

278278
self.assertEqual(
279-
Model.check(),
279+
Model.check(databases=self.databases),
280280
[
281281
Error(
282282
"The index name '%sindex_name' cannot start with an "
@@ -296,7 +296,7 @@ class Meta:
296296
indexes = [models.Index(fields=["id"], name=index_name)]
297297

298298
self.assertEqual(
299-
Model.check(),
299+
Model.check(databases=self.databases),
300300
[
301301
Error(
302302
"The index name '%s' cannot be longer than 30 characters."
@@ -499,7 +499,7 @@ class Meta:
499499
indexes = [models.Index(fields=["name"], include=["pk"], name="name")]
500500

501501
self.assertEqual(
502-
Model.check(),
502+
Model.check(databases=self.databases),
503503
[
504504
Error(
505505
"'indexes' refers to a CompositePrimaryKey 'pk', but "
@@ -539,6 +539,7 @@ class Meta:
539539

540540
self.assertEqual(Model.check(databases=self.databases), [])
541541

542+
@skipUnlessDBFeature("supports_expression_indexes")
542543
def test_func_index_complex_expression_custom_lookup(self):
543544
class Model(models.Model):
544545
height = models.IntegerField()
@@ -554,15 +555,16 @@ class Meta:
554555
]
555556

556557
with register_lookup(models.IntegerField, Abs):
557-
self.assertEqual(Model.check(), [])
558+
self.assertEqual(Model.check(databases=self.databases), [])
558559

560+
@skipUnlessDBFeature("supports_expression_indexes")
559561
def test_func_index_pointing_to_missing_field(self):
560562
class Model(models.Model):
561563
class Meta:
562564
indexes = [models.Index(Lower("missing_field").desc(), name="name")]
563565

564566
self.assertEqual(
565-
Model.check(),
567+
Model.check(databases=self.databases),
566568
[
567569
Error(
568570
"'indexes' refers to the nonexistent field 'missing_field'.",
@@ -572,6 +574,7 @@ class Meta:
572574
],
573575
)
574576

577+
@skipUnlessDBFeature("supports_expression_indexes")
575578
def test_func_index_pointing_to_missing_field_nested(self):
576579
class Model(models.Model):
577580
class Meta:
@@ -580,7 +583,7 @@ class Meta:
580583
]
581584

582585
self.assertEqual(
583-
Model.check(),
586+
Model.check(databases=self.databases),
584587
[
585588
Error(
586589
"'indexes' refers to the nonexistent field 'missing_field'.",
@@ -590,6 +593,7 @@ class Meta:
590593
],
591594
)
592595

596+
@skipUnlessDBFeature("supports_expression_indexes")
593597
def test_func_index_pointing_to_m2m_field(self):
594598
class Model(models.Model):
595599
m2m = models.ManyToManyField("self")
@@ -598,7 +602,7 @@ class Meta:
598602
indexes = [models.Index(Lower("m2m"), name="name")]
599603

600604
self.assertEqual(
601-
Model.check(),
605+
Model.check(databases=self.databases),
602606
[
603607
Error(
604608
"'indexes' refers to a ManyToManyField 'm2m', but "
@@ -609,6 +613,7 @@ class Meta:
609613
],
610614
)
611615

616+
@skipUnlessDBFeature("supports_expression_indexes")
612617
def test_func_index_pointing_to_non_local_field(self):
613618
class Foo(models.Model):
614619
field1 = models.CharField(max_length=15)
@@ -618,7 +623,7 @@ class Meta:
618623
indexes = [models.Index(Lower("field1"), name="name")]
619624

620625
self.assertEqual(
621-
Bar.check(),
626+
Bar.check(databases=self.databases),
622627
[
623628
Error(
624629
"'indexes' refers to field 'field1' which is not local to "
@@ -630,6 +635,7 @@ class Meta:
630635
],
631636
)
632637

638+
@skipUnlessDBFeature("supports_expression_indexes")
633639
def test_func_index_pointing_to_fk(self):
634640
class Foo(models.Model):
635641
pass
@@ -643,8 +649,9 @@ class Meta:
643649
models.Index(Lower("foo_1_id"), Lower("foo_2"), name="index_name"),
644650
]
645651

646-
self.assertEqual(Bar.check(), [])
652+
self.assertEqual(Bar.check(databases=self.databases), [])
647653

654+
@skipUnlessDBFeature("supports_expression_indexes")
648655
def test_func_index_pointing_to_composite_primary_key(self):
649656
class Model(models.Model):
650657
pk = models.CompositePrimaryKey("version", "name")
@@ -655,7 +662,7 @@ class Meta:
655662
indexes = [models.Index(Abs("pk"), name="name")]
656663

657664
self.assertEqual(
658-
Model.check(),
665+
Model.check(databases=self.databases),
659666
[
660667
Error(
661668
"'indexes' refers to a CompositePrimaryKey 'pk', but "

0 commit comments

Comments
 (0)