mirror of
https://github.com/django/django.git
synced 2025-08-21 01:09:13 +00:00
Fixed #36438 -- Made MigrationAutodetector remove generated fields before their base fields.
Thanks to Colton Saska for the report and to Simon Charette for the review.
This commit is contained in:
parent
9ab1991689
commit
1a7fc0f65d
@ -1221,9 +1221,9 @@ class MigrationAutodetector:
|
|||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
name=field_name,
|
name=field_name,
|
||||||
),
|
),
|
||||||
# We might need to depend on the removal of an
|
# Include dependencies such as order_with_respect_to, constraints,
|
||||||
# order_with_respect_to or index/constraint/unique_together
|
# and any generated fields that may depend on this field. These
|
||||||
# operation; this is safely ignored if there isn't one
|
# are safely ignored if not present.
|
||||||
dependencies=[
|
dependencies=[
|
||||||
OperationDependency(
|
OperationDependency(
|
||||||
app_label,
|
app_label,
|
||||||
@ -1243,6 +1243,9 @@ class MigrationAutodetector:
|
|||||||
field_name,
|
field_name,
|
||||||
OperationDependency.Type.REMOVE_INDEX_OR_CONSTRAINT,
|
OperationDependency.Type.REMOVE_INDEX_OR_CONSTRAINT,
|
||||||
),
|
),
|
||||||
|
*self._get_generated_field_dependencies_for_removed_field(
|
||||||
|
app_label, model_name, field_name
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1698,6 +1701,27 @@ class MigrationAutodetector:
|
|||||||
)
|
)
|
||||||
return dependencies
|
return dependencies
|
||||||
|
|
||||||
|
def _get_generated_field_dependencies_for_removed_field(
|
||||||
|
self, app_label, model_name, field_name
|
||||||
|
):
|
||||||
|
dependencies = []
|
||||||
|
model_state = self.from_state.models[app_label, model_name]
|
||||||
|
generated_fields = (f for f in model_state.fields.values() if f.generated)
|
||||||
|
for field in generated_fields:
|
||||||
|
if any(
|
||||||
|
field_name == name
|
||||||
|
for name, *_ in models.Model._get_expr_references(field.expression)
|
||||||
|
):
|
||||||
|
dependencies.append(
|
||||||
|
OperationDependency(
|
||||||
|
app_label,
|
||||||
|
model_name,
|
||||||
|
field.name,
|
||||||
|
OperationDependency.Type.REMOVE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return dependencies
|
||||||
|
|
||||||
def _get_dependencies_for_model(self, app_label, model_name):
|
def _get_dependencies_for_model(self, app_label, model_name):
|
||||||
"""Return foreign key dependencies of the given model."""
|
"""Return foreign key dependencies of the given model."""
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
@ -13,7 +13,7 @@ from django.db.migrations.graph import MigrationGraph
|
|||||||
from django.db.migrations.loader import MigrationLoader
|
from django.db.migrations.loader import MigrationLoader
|
||||||
from django.db.migrations.questioner import MigrationQuestioner
|
from django.db.migrations.questioner import MigrationQuestioner
|
||||||
from django.db.migrations.state import ModelState, ProjectState
|
from django.db.migrations.state import ModelState, ProjectState
|
||||||
from django.db.models.functions import Concat, Lower
|
from django.db.models.functions import Concat, Lower, Upper
|
||||||
from django.test import SimpleTestCase, TestCase, override_settings
|
from django.test import SimpleTestCase, TestCase, override_settings
|
||||||
from django.test.utils import isolate_lru_cache
|
from django.test.utils import isolate_lru_cache
|
||||||
|
|
||||||
@ -1454,6 +1454,100 @@ class AutodetectorTests(BaseAutodetectorTests):
|
|||||||
self.assertOperationTypes(changes, "testapp", 0, ["RemoveField"])
|
self.assertOperationTypes(changes, "testapp", 0, ["RemoveField"])
|
||||||
self.assertOperationAttributes(changes, "testapp", 0, 0, name="name")
|
self.assertOperationAttributes(changes, "testapp", 0, 0, name="name")
|
||||||
|
|
||||||
|
def test_remove_generated_field_before_its_base_field(self):
|
||||||
|
initial_state = [
|
||||||
|
ModelState(
|
||||||
|
"testapp",
|
||||||
|
"Author",
|
||||||
|
[
|
||||||
|
("name", models.CharField(max_length=20)),
|
||||||
|
(
|
||||||
|
"upper_name",
|
||||||
|
models.GeneratedField(
|
||||||
|
expression=Upper("name"),
|
||||||
|
db_persist=True,
|
||||||
|
output_field=models.CharField(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
updated_state = [ModelState("testapp", "Author", [])]
|
||||||
|
changes = self.get_changes(initial_state, updated_state)
|
||||||
|
self.assertNumberMigrations(changes, "testapp", 1)
|
||||||
|
self.assertOperationTypes(changes, "testapp", 0, ["RemoveField", "RemoveField"])
|
||||||
|
self.assertOperationAttributes(changes, "testapp", 0, 0, name="upper_name")
|
||||||
|
self.assertOperationAttributes(changes, "testapp", 0, 1, name="name")
|
||||||
|
|
||||||
|
def test_remove_generated_field_before_multiple_base_fields(self):
|
||||||
|
initial_state = [
|
||||||
|
ModelState(
|
||||||
|
"testapp",
|
||||||
|
"Author",
|
||||||
|
[
|
||||||
|
("first_name", models.CharField(max_length=20)),
|
||||||
|
("last_name", models.CharField(max_length=20)),
|
||||||
|
(
|
||||||
|
"full_name",
|
||||||
|
models.GeneratedField(
|
||||||
|
expression=Concat("first_name", "last_name"),
|
||||||
|
db_persist=True,
|
||||||
|
output_field=models.CharField(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
updated_state = [ModelState("testapp", "Author", [])]
|
||||||
|
changes = self.get_changes(initial_state, updated_state)
|
||||||
|
self.assertNumberMigrations(changes, "testapp", 1)
|
||||||
|
self.assertOperationTypes(
|
||||||
|
changes, "testapp", 0, ["RemoveField", "RemoveField", "RemoveField"]
|
||||||
|
)
|
||||||
|
self.assertOperationAttributes(changes, "testapp", 0, 0, name="full_name")
|
||||||
|
self.assertOperationAttributes(changes, "testapp", 0, 1, name="first_name")
|
||||||
|
self.assertOperationAttributes(changes, "testapp", 0, 2, name="last_name")
|
||||||
|
|
||||||
|
def test_remove_generated_field_and_one_of_multiple_base_fields(self):
|
||||||
|
initial_state = [
|
||||||
|
ModelState(
|
||||||
|
"testapp",
|
||||||
|
"Author",
|
||||||
|
[
|
||||||
|
("first_name", models.CharField(max_length=20)),
|
||||||
|
("last_name", models.CharField(max_length=20)),
|
||||||
|
(
|
||||||
|
"full_name",
|
||||||
|
models.GeneratedField(
|
||||||
|
expression=Concat("first_name", "last_name"),
|
||||||
|
db_persist=True,
|
||||||
|
output_field=models.CharField(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
# Only remove full_name and first_name.
|
||||||
|
updated_state = [
|
||||||
|
ModelState(
|
||||||
|
"testapp",
|
||||||
|
"Author",
|
||||||
|
[
|
||||||
|
("last_name", models.CharField(max_length=20)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
changes = self.get_changes(initial_state, updated_state)
|
||||||
|
self.assertNumberMigrations(changes, "testapp", 1)
|
||||||
|
self.assertOperationTypes(
|
||||||
|
changes,
|
||||||
|
"testapp",
|
||||||
|
0,
|
||||||
|
["RemoveField", "RemoveField"],
|
||||||
|
)
|
||||||
|
self.assertOperationAttributes(changes, "testapp", 0, 0, name="full_name")
|
||||||
|
self.assertOperationAttributes(changes, "testapp", 0, 1, name="first_name")
|
||||||
|
|
||||||
def test_alter_field(self):
|
def test_alter_field(self):
|
||||||
"""Tests autodetection of new fields."""
|
"""Tests autodetection of new fields."""
|
||||||
changes = self.get_changes([self.author_name], [self.author_name_longer])
|
changes = self.get_changes([self.author_name], [self.author_name_longer])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user