1
0
mirror of https://github.com/django/django.git synced 2025-08-21 09:19:12 +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:
Clifford Gama 2025-07-05 12:47:53 +02:00 committed by Sarah Boyce
parent 9ab1991689
commit 1a7fc0f65d
2 changed files with 122 additions and 4 deletions

View File

@ -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 = []

View File

@ -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])