Skip to content

Commit ededff2

Browse files
committed
More checks for indexes
1 parent 4886ace commit ededff2

File tree

10 files changed

+138
-25
lines changed

10 files changed

+138
-25
lines changed

CHANGELOG.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,35 @@
22

33
### Unreleased
44

5+
### 0.7.0
6+
7+
- Check `field-foreign-key-index` now accepts `when: indexes` instead of `when: unique_toegether` because now it search for duplicate indexes in `Meta.indexes`, `Meta.unique_toegether` and `Meta.constraints`
8+
- Add checks:
9+
- `no-unique-together`
10+
- `no-index-together`
11+
512
### 0.6.0
613

714
- Add checks:
8-
- field-default-null
15+
- `field-default-null`
916

1017
### 0.5.0
1118

12-
- Fix ignore_checks
13-
- Skip models fields not inherited from fields.Field
14-
- Add ignore_types option
19+
- Fix `ignore_checks`
20+
- Skip models fields not inherited from `fields.Field`
21+
- Add `ignore_types` option
1522

1623
### 0.4.1
1724

18-
- Fix message for *field-verbose-name-gettext-case*
25+
- Fix message for `field-verbose-name-gettext-case`
1926

2027
### 0.4.0
2128

2229
- Add infra for rest framework serializers checks
2330
- Add checks:
24-
- drf-model-serializer-extra-kwargs
25-
- drf-model-serializer-meta-attribute
26-
- model-admin
31+
- `drf-model-serializer-extra-kwargs`
32+
- `drf-model-serializer-meta-attribute`
33+
- `model-admin`
2734

2835
### 0.3.0
2936

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ EXTRA_CHECKS = {
6464
- **extra-checks-config** - settings.EXTRA_CHECKS is valid config for django-extra-checks (always enabled).
6565
- **model-attribute** - Each Model in the project must have all attributes from `attrs` setting specified.
6666
- **model-meta-attribute** - Each Model.Meta in the project must have all attributes from `attrs` setting specified.
67+
- **no-unique-together** - Use UniqueConstraint with the constraints option instead.
68+
- **no-index-together** - Use the indexes option instead.
6769
- **model-admin** - Each model must be registered in admin.
6870
- **field-file-upload-to** - FileField/ImageField must have non empty `upload_to` argument.
6971
- **field-verbose-name** - All model's fields must have verbose name.
@@ -73,7 +75,7 @@ EXTRA_CHECKS = {
7375
- **field-text-null** - text fields shoudn't use `null=True`.
7476
- **field-boolean-null** - prefer using `BooleanField(null=True)` instead of `NullBooleanField`.
7577
- **field-null** - don't pass `null=False` to model fields (this is django default).
76-
- **field-foreign-key-index** - ForeignKey fields must specify `db_index` explicitly (to apply to unique together only: `when: unique_together`).
78+
- **field-foreign-key-index** - ForeignKey fields must specify `db_index` explicitly (to apply only to fields in indexes: `when: indexes`).
7779
- **field-default-null** - If field nullable (`null=True`), then
7880
`default=None` argument is redundant and should be removed.
7981
**WARNING** Be aware that setting is database dependent,

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = django-extra-checks
3-
version = 0.6.0
3+
version = 0.7.0
44
author = Konstantin Alekseev
55
author_email = [email protected]
66
description = Collection of useful checks for Django Checks Framework

src/extra_checks/check_id.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ class CheckId(str, enum.Enum):
66
X010 = "model-attribute"
77
X011 = "model-meta-attribute"
88
X012 = "model-admin"
9+
X013 = "no-unique-together"
10+
X014 = "no-index-together"
911
X050 = "field-verbose-name"
1012
X051 = "field-verbose-name-gettext"
1113
X052 = "field-verbose-name-gettext-case"

src/extra_checks/checks/model_checks.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,29 @@ def apply(
155155
) -> Iterator[django.core.checks.CheckMessage]:
156156
if model not in self.models_with_admin:
157157
yield self.message("The model is not registered in admin.", obj=model)
158+
159+
160+
@registry.register(django.core.checks.Tags.models)
161+
class CheckNoUniqueTogether(CheckModel):
162+
Id = CheckId.X013
163+
level = django.core.checks.ERROR
164+
165+
def apply(
166+
self, model: Type[models.Model], model_ast: ModelAST
167+
) -> Iterator[django.core.checks.CheckMessage]:
168+
if "unique_together" in model_ast.meta_vars:
169+
yield self.message(
170+
"Use UniqueConstraint with the constraints option instead.", obj=model,
171+
)
172+
173+
174+
@registry.register(django.core.checks.Tags.models)
175+
class CheckNoIndexTogether(CheckModel):
176+
Id = CheckId.X014
177+
level = django.core.checks.ERROR
178+
179+
def apply(
180+
self, model: Type[models.Model], model_ast: ModelAST
181+
) -> Iterator[django.core.checks.CheckMessage]:
182+
if "index_together" in model_ast.meta_vars:
183+
yield self.message("Use the indexes option instead", obj=model)

src/extra_checks/checks/model_field_checks.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,40 @@ class CheckFieldForeignKeyIndex(CheckModelField):
198198

199199
class CheckFieldForeignKeyIndexForm(BaseCheckForm):
200200
when = forms.ChoiceField(
201-
choices=[("unique_together", "unique_together"), ("always", "always")],
202-
required=False,
201+
choices=[("indexes", "indexes"), ("always", "always")], required=False,
203202
)
204203

205204
settings_form_class = CheckFieldForeignKeyIndexForm
206205

207206
def __init__(self, when: str, **kwargs: Any) -> None:
208-
self.when = when or "unique_together"
207+
self.when = when or "indexes"
209208
super().__init__(**kwargs)
210209

210+
@classmethod
211+
def get_index_values_in_meta(cls, model: Type[models.Model]) -> Iterator[str]:
212+
for entry in model._meta.unique_together:
213+
if isinstance(entry, str):
214+
yield entry
215+
else:
216+
yield from entry
217+
for entry in model._meta.index_together:
218+
if isinstance(entry, str):
219+
yield entry
220+
else:
221+
yield from entry
222+
for constraint in model._meta.constraints:
223+
if isinstance(constraint, models.UniqueConstraint):
224+
yield from constraint.fields
225+
for index in model._meta.indexes:
226+
yield from index.fields
227+
228+
@classmethod
229+
def get_fields_with_indexes_in_meta(
230+
cls, model: Type[models.Model]
231+
) -> Iterator[str]:
232+
for entry in cls.get_index_values_in_meta(model):
233+
yield entry.lstrip("-")
234+
211235
def apply(
212236
self,
213237
field: models.fields.Field,
@@ -216,10 +240,8 @@ def apply(
216240
) -> Iterator[django.core.checks.CheckMessage]:
217241
if isinstance(field, models.fields.related.RelatedField):
218242
if field.many_to_one and field_ast.kwargs.get("db_index") is None:
219-
if self.when == "unique_together":
220-
if any(
221-
field.name in index for index in model._meta.unique_together
222-
):
243+
if self.when == "indexes":
244+
if field.name in self.get_fields_with_indexes_in_meta(model):
223245
yield self.message(
224246
"ForeignKey must set `db_index` explicitly if it present in unique_together.",
225247
hint="Specify `db_index` field argument.",

tests/example/apps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.apps import AppConfig
22

33

4-
class ProductsConfig(AppConfig):
5-
name = "products"
4+
class ExampleConfig(AppConfig):
5+
name = "tests.example"

tests/example/models.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ class ModelFieldNullDefault(models.Model):
9898

9999

100100
class ModelFieldForeignKeyIndex(models.Model):
101-
article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="+")
101+
article = models.ForeignKey(
102+
Article, on_delete=models.CASCADE, related_name="+", db_index=True
103+
)
102104
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="+")
103105
another_article = models.ForeignKey(
104106
Article, on_delete=models.CASCADE, related_name="+"
@@ -107,14 +109,33 @@ class ModelFieldForeignKeyIndex(models.Model):
107109
Article, on_delete=models.CASCADE, related_name="+", db_index=True
108110
)
109111
field_one = models.ForeignKey(
110-
ModelFieldTextNull, on_delete=models.CASCADE, related_name="+", db_index=False
112+
ModelFieldTextNull, on_delete=models.CASCADE, related_name="+"
111113
)
112114
field_two = models.ForeignKey(
113115
ModelFieldNullFalse, on_delete=models.CASCADE, related_name="+", db_index=True
114116
)
117+
field_three = models.ForeignKey(
118+
ModelFieldNullFalse, on_delete=models.CASCADE, related_name="+"
119+
)
120+
field_in_indexes = models.ForeignKey(
121+
ModelFieldNullFalse, on_delete=models.CASCADE, related_name="+"
122+
)
123+
field_index_desc = models.ForeignKey(
124+
ModelFieldNullFalse, on_delete=models.CASCADE, related_name="+"
125+
)
115126

116127
class Meta:
117-
unique_together = (("article", "author"), ("field_one", "field_two"))
128+
unique_together = [("author", "article")]
129+
index_together = ("field_one", "field_two")
130+
constraints = [
131+
models.UniqueConstraint(
132+
fields=("author", "field_three"), name="fi_author_field_unique"
133+
)
134+
]
135+
indexes = [
136+
models.Index(fields=("field_in_indexes",)),
137+
models.Index(fields=("field_in_indexes", "-field_index_desc")),
138+
]
118139

119140

120141
class GenericKeyOne(models.Model):

tests/test_model_checks.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import pytest
22

33
from extra_checks.checks import model_checks
4-
from tests.example.models import Article, Author, ModelFieldTextNull
4+
from tests.example.models import (
5+
Article,
6+
Author,
7+
ModelFieldForeignKeyIndex,
8+
ModelFieldTextNull,
9+
)
510

611

712
@pytest.fixture
@@ -80,3 +85,25 @@ def test_admin_models(test_case):
8085
messages = test_case.models(ModelFieldTextNull).run()
8186
assert len(messages) == 1
8287
assert messages[0].id == model_checks.CheckModelAdmin.Id.name
88+
89+
90+
def test_no_unique_together(test_case):
91+
messages = (
92+
test_case.models(ModelFieldForeignKeyIndex)
93+
.settings({"checks": [model_checks.CheckNoUniqueTogether.Id.value]})
94+
.check(model_checks.CheckNoUniqueTogether)
95+
.run()
96+
)
97+
assert len(messages) == 1
98+
assert messages[0].id == model_checks.CheckNoUniqueTogether.Id.name
99+
100+
101+
def test_no_index_together(test_case):
102+
messages = (
103+
test_case.models(ModelFieldForeignKeyIndex)
104+
.settings({"checks": [model_checks.CheckNoIndexTogether.Id.value]})
105+
.check(model_checks.CheckNoIndexTogether)
106+
.run()
107+
)
108+
assert len(messages) == 1
109+
assert messages[0].id == model_checks.CheckNoIndexTogether.Id.name

tests/test_model_field_checks.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,11 @@ def test_check_field_foreign_key_index(test_case):
140140
.run()
141141
)
142142
assert {m.obj.name for m in messages} == {
143-
"article",
143+
"field_one",
144144
"author",
145+
"field_three",
146+
"field_in_indexes",
147+
"field_index_desc",
145148
}
146149

147150

@@ -161,9 +164,12 @@ def test_check_field_foreign_key_index_always(test_case):
161164
.run()
162165
)
163166
assert {m.obj.name for m in messages} == {
164-
"article",
167+
"field_one",
165168
"author",
166169
"another_article",
170+
"field_three",
171+
"field_in_indexes",
172+
"field_index_desc",
167173
}
168174

169175

0 commit comments

Comments
 (0)