From b2407e4d7d25e4a5839bb512cb2fdf71e9e30a21 Mon Sep 17 00:00:00 2001 From: Jacob Walls Date: Sun, 8 Jun 2025 16:56:46 -0400 Subject: [PATCH] Fixed #35305 -- Avoided recreating constraints on fields renamed via db_column. --- django/db/migrations/autodetector.py | 23 ++++ tests/migrations/test_autodetector.py | 180 ++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py index 13fe8f01a4..1d25101219 100644 --- a/django/db/migrations/autodetector.py +++ b/django/db/migrations/autodetector.py @@ -1460,6 +1460,29 @@ class MigrationAutodetector: for attr in new_constraint.non_db_attrs: new_kwargs.pop(attr, None) + # Replace renamed fields if the db_column is preserved. + for ( + _, + _, + rem_db_column, + rem_field_name, + _, + _, + field, + field_name, + ) in self.renamed_operations: + if field.db_column and rem_db_column == field.db_column: + new_fields = new_kwargs["fields"] + try: + new_field_idx = new_fields.index(field_name) + except ValueError: + continue + new_kwargs["fields"] = tuple( + new_fields[:new_field_idx] + + (rem_field_name,) + + new_fields[new_field_idx + 1 :] + ) + return (old_path, old_args, old_kwargs) != (new_path, new_args, new_kwargs) def create_altered_constraints(self): diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index ac725d317e..11f7d1e801 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -2078,6 +2078,186 @@ class AutodetectorTests(BaseAutodetectorTests): new_name="renamed_foo", ) + def test_rename_field_preserve_db_column_preserve_constraint(self): + """ + Renaming a field that already had a db_column attribute and a constraint + generates two no-op operations: RenameField and AlterConstraint. + """ + before = [ + ModelState( + "app", + "Foo", + [ + ("id", models.AutoField(primary_key=True)), + ("field", models.IntegerField(db_column="full_field1_name")), + ("field2", models.IntegerField()), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=["field", "field2"], + name="unique_field", + ), + ], + }, + ), + ] + after = [ + ModelState( + "app", + "Foo", + [ + ("id", models.AutoField(primary_key=True)), + ( + "full_field1_name", + models.IntegerField(db_column="full_field1_name"), + ), + ( + "field2", + models.IntegerField(), + ), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=["full_field1_name", "field2"], + name="unique_field", + ), + ], + }, + ), + ] + changes = self.get_changes( + before, after, MigrationQuestioner({"ask_rename": True}) + ) + self.assertNumberMigrations(changes, "app", 1) + self.assertOperationTypes(changes, "app", 0, ["RenameField", "AlterConstraint"]) + self.assertOperationAttributes( + changes, + "app", + 0, + 1, + model_name="foo", + name="unique_field", + ) + self.assertEqual( + changes["app"][0].operations[1].deconstruct(), + ( + "AlterConstraint", + [], + { + "constraint": after[0].options["constraints"][0], + "model_name": "foo", + "name": "unique_field", + }, + ), + ) + + def test_rename_field_without_db_column_recreate_constraint(self): + """Renaming a field without given db_column recreates a constraint.""" + before = [ + ModelState( + "app", + "Foo", + [ + ("id", models.AutoField(primary_key=True)), + ("field", models.IntegerField()), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=["field"], + name="unique_field", + ), + ], + }, + ), + ] + after = [ + ModelState( + "app", + "Foo", + [ + ("id", models.AutoField(primary_key=True)), + ( + "full_field1_name", + models.IntegerField(), + ), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=["full_field1_name"], + name="unique_field", + ), + ], + }, + ), + ] + changes = self.get_changes( + before, after, MigrationQuestioner({"ask_rename": True}) + ) + self.assertNumberMigrations(changes, "app", 1) + self.assertOperationTypes( + changes, "app", 0, ["RemoveConstraint", "RenameField", "AddConstraint"] + ) + + def test_rename_field_preserve_db_column_recreate_constraint(self): + """Removing a field from the constraint triggers recreation.""" + before = [ + ModelState( + "app", + "Foo", + [ + ("id", models.AutoField(primary_key=True)), + ("field1", models.IntegerField(db_column="field1")), + ("field2", models.IntegerField(db_column="field2")), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=["field1", "field2"], + name="unique_fields", + ), + ], + }, + ), + ] + after = [ + ModelState( + "app", + "Foo", + [ + ("id", models.AutoField(primary_key=True)), + ("renamed_field1", models.IntegerField(db_column="field1")), + ("renamed_field2", models.IntegerField(db_column="field2")), + ], + options={ + "constraints": [ + models.UniqueConstraint( + fields=["renamed_field1"], + name="unique_fields", + ), + ], + }, + ), + ] + changes = self.get_changes( + before, after, MigrationQuestioner({"ask_rename": True}) + ) + self.assertNumberMigrations(changes, "app", 1) + self.assertOperationTypes( + changes, + "app", + 0, + [ + "RemoveConstraint", + "RenameField", + "RenameField", + "AddConstraint", + ], + ) + def test_rename_field_with_renamed_model(self): changes = self.get_changes( [self.author_name],