summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2025-06-08 16:56:46 -0400
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2025-06-23 14:06:14 +0200
commitb2407e4d7d25e4a5839bb512cb2fdf71e9e30a21 (patch)
tree17f342173f8d492389d05f1ca6b75ab3ed960ce5
parent54402a75296909b1b00df41ff4dbd95e3d8a87e6 (diff)
Fixed #35305 -- Avoided recreating constraints on fields renamed via db_column.
-rw-r--r--django/db/migrations/autodetector.py23
-rw-r--r--tests/migrations/test_autodetector.py180
2 files changed, 203 insertions, 0 deletions
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],