Skip to content

Commit

Permalink
[5.0.x] Fixed #35422 -- Fixed migrations crash when altering Generate…
Browse files Browse the repository at this point in the history
…dField referencing rename field.

Thanks Sarah Boyce for the report and Simon Charette for the
implementation idea.

Backport of 91a4b9a from main.
  • Loading branch information
felixxm authored and sarahboyce committed May 3, 2024
1 parent 24f54c3 commit c544f1a
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 6 deletions.
20 changes: 14 additions & 6 deletions django/db/backends/base/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime

from django.conf import settings
from django.core.exceptions import FieldError
from django.db.backends.ddl_references import (
Columns,
Expressions,
Expand Down Expand Up @@ -832,6 +833,7 @@ def alter_field(self, model, old_field, new_field, strict=False):
old_type = old_db_params["type"]
new_db_params = new_field.db_parameters(connection=self.connection)
new_type = new_db_params["type"]
modifying_generated_field = False
if (old_type is None and old_field.remote_field is None) or (
new_type is None and new_field.remote_field is None
):
Expand Down Expand Up @@ -870,13 +872,19 @@ def alter_field(self, model, old_field, new_field, strict=False):
"through= on M2M fields)" % (old_field, new_field)
)
elif old_field.generated != new_field.generated or (
new_field.generated
and (
old_field.db_persist != new_field.db_persist
or old_field.generated_sql(self.connection)
!= new_field.generated_sql(self.connection)
)
new_field.generated and old_field.db_persist != new_field.db_persist
):
modifying_generated_field = True
elif new_field.generated:
try:
old_field_sql = old_field.generated_sql(self.connection)
except FieldError:
# Field used in a generated field was renamed.
modifying_generated_field = True
else:
new_field_sql = new_field.generated_sql(self.connection)
modifying_generated_field = old_field_sql != new_field_sql
if modifying_generated_field:
raise ValueError(
f"Modifying GeneratedFields is not supported - the field {new_field} "
"must be removed and re-added with the new definition."
Expand Down
12 changes: 12 additions & 0 deletions django/db/backends/mysql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,18 @@ def django_test_skips(self):
},
}
)
if not self.connection.mysql_is_mariadb:
skips.update(
{
"MySQL doesn't allow renaming columns referenced by generated "
"columns": {
"migrations.test_operations.OperationTests."
"test_invalid_generated_field_changes_on_rename_stored",
"migrations.test_operations.OperationTests."
"test_invalid_generated_field_changes_on_rename_virtual",
},
}
)
return skips

@cached_property
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/5.0.5.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ Bugfixes
* Fixed a bug in Django 5.0 that caused a migration crash when a
``GeneratedField`` was added before any of the referenced fields from its
``expression`` definition (:ticket:`35359`).

* Fixed a bug in Django 5.0 that caused a migration crash when altering a
``GeneratedField`` referencing a rename field (:ticket:`35422`).
44 changes: 44 additions & 0 deletions tests/migrations/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5926,6 +5926,50 @@ def test_invalid_generated_field_changes_stored(self):
def test_invalid_generated_field_changes_virtual(self):
self._test_invalid_generated_field_changes(db_persist=False)

def _test_invalid_generated_field_changes_on_rename(self, db_persist):
app_label = "test_igfcor"
operation = migrations.AddField(
"Pony",
"modified_pink",
models.GeneratedField(
expression=F("pink") + F("pink"),
output_field=models.IntegerField(),
db_persist=db_persist,
),
)
project_state, new_state = self.make_test_state(app_label, operation)
# Add generated column.
with connection.schema_editor() as editor:
operation.database_forwards(app_label, editor, project_state, new_state)
# Rename field used in the generated field.
operations = [
migrations.RenameField("Pony", "pink", "renamed_pink"),
migrations.AlterField(
"Pony",
"modified_pink",
models.GeneratedField(
expression=F("renamed_pink"),
output_field=models.IntegerField(),
db_persist=db_persist,
),
),
]
msg = (
"Modifying GeneratedFields is not supported - the field "
f"{app_label}.Pony.modified_pink must be removed and re-added with the "
"new definition."
)
with self.assertRaisesMessage(ValueError, msg):
self.apply_operations(app_label, new_state, operations)

@skipUnlessDBFeature("supports_stored_generated_columns")
def test_invalid_generated_field_changes_on_rename_stored(self):
self._test_invalid_generated_field_changes_on_rename(db_persist=True)

@skipUnlessDBFeature("supports_virtual_generated_columns")
def test_invalid_generated_field_changes_on_rename_virtual(self):
self._test_invalid_generated_field_changes_on_rename(db_persist=False)

@skipUnlessDBFeature(
"supports_stored_generated_columns",
"supports_virtual_generated_columns",
Expand Down

0 comments on commit c544f1a

Please sign in to comment.