diff options
| author | David Wobrock <david.wobrock@backmarket.com> | 2026-03-16 22:39:40 +0100 |
|---|---|---|
| committer | Jacob Walls <jacobtylerwalls@gmail.com> | 2026-06-15 17:40:16 -0400 |
| commit | cd385e6b8c16b51f68c1f220ff09a4cfd679af0c (patch) | |
| tree | 0fe9b5558c804d43de369a68eda774a42cd49809 | |
| parent | 63ff1142b2f0cba1981825c65b62425e9ff7734b (diff) | |
Fixed #31317 -- Avoided crash in CreateModel with unique_together and AlterUniqueTogether.
| -rw-r--r-- | django/db/backends/base/schema.py | 22 | ||||
| -rw-r--r-- | tests/migrations/test_operations.py | 85 |
2 files changed, 106 insertions, 1 deletions
diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index a99218da92..9857eea571 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -661,6 +661,26 @@ class BaseDatabaseSchemaEditor: } meta_index_names = {constraint.name for constraint in model._meta.indexes} columns = [model._meta.get_field(field).column for field in fields] + + # Check if the constraint is still in deferred_sql. This happens when + # CreateModel with unique_together is followed by AlterUniqueTogether + # in the same migration. index_together is not affected because its + # indexes are created immediately in CreateModel.database_forwards. + is_unique_constraint = constraint_kwargs.get("unique") is True + table = model._meta.db_table + if is_unique_constraint: + for deferred in list(self.deferred_sql): + if ( + isinstance(deferred, Statement) + and deferred.references_table(table) + and all( + deferred.references_column(table, column) for column in columns + ) + and deferred.parts["columns"].columns == columns + ): + self.deferred_sql.remove(deferred) + return + constraint_names = self._constraint_names( model, columns, @@ -668,7 +688,7 @@ class BaseDatabaseSchemaEditor: **constraint_kwargs, ) if ( - constraint_kwargs.get("unique") is True + is_unique_constraint and constraint_names and self.connection.features.allows_multiple_constraints_on_same_fields ): diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 7189cef400..b3d5481267 100644 --- a/tests/migrations/test_operations.py +++ b/tests/migrations/test_operations.py @@ -3861,6 +3861,62 @@ class OperationTests(OperationTestBase): operation.describe(), "Alter unique_together for Pony (0 constraint(s))" ) + def test_alter_unique_together_deferred(self): + """ + AlterUniqueTogether handles deferred SQL constraints from previous + operations. Regression test for #31317. + """ + app_label = "test_aluntod" + self.apply_operations( + app_label, + ProjectState(), + operations=[ + migrations.CreateModel( + "Pony", + fields=[ + ("id", models.AutoField(primary_key=True)), + ("pink", models.IntegerField(default=3)), + ("weight", models.FloatField()), + ], + options={"unique_together": {("pink",)}}, + ), + migrations.AlterUniqueTogether( + name="Pony", + unique_together={("pink", "weight")}, + ), + ], + ) + + table_name = f"{app_label}_pony" + self.assertUniqueConstraintExists(table_name, ("pink", "weight"), value=True) + self.assertUniqueConstraintExists(table_name, ("pink",), value=False) + + def test_alter_unique_together_deferred_overlapping_columns(self): + app_label = "test_aluntodoc" + self.apply_operations( + app_label, + ProjectState(), + operations=[ + migrations.CreateModel( + "Pony", + fields=[ + ("id", models.AutoField(primary_key=True)), + ("pink", models.IntegerField(default=3)), + ("weight", models.FloatField()), + ], + options={"unique_together": [("pink", "weight"), ("pink",)]}, + ), + migrations.AlterUniqueTogether( + name="Pony", + unique_together={("pink", "weight")}, + ), + ], + ) + + table_name = f"{app_label}_pony" + self.assertUniqueConstraintExists(table_name, ("pink", "weight"), value=True) + self.assertUniqueConstraintExists(table_name, ("pink",), value=False) + @skipUnlessDBFeature("allows_multiple_constraints_on_same_fields") def test_remove_unique_together_on_pk_field(self): app_label = "test_rutopkf" @@ -4452,6 +4508,35 @@ class OperationTests(OperationTestBase): self.assertIndexNotExists(table_name, ["pink", "weight"]) self.assertUniqueConstraintExists(table_name, ["pink", "weight"]) + def test_alter_index_together_deferred_overlapping_columns(self): + app_label = "test_alintodoc" + self.apply_operations( + app_label, + ProjectState(), + operations=[ + migrations.CreateModel( + "Pony", + fields=[ + ("id", models.AutoField(primary_key=True)), + ("pink", models.IntegerField(default=3)), + ("weight", models.FloatField()), + ], + options={ + "unique_together": [("pink",)], + "index_together": [("pink",)], + }, + ), + migrations.AlterIndexTogether( + name="Pony", + index_together=set(), + ), + ], + ) + + table_name = f"{app_label}_pony" + self.assertIndexNotExists(table_name, ["pink"]) + self.assertUniqueConstraintExists(table_name, ("pink",), value=True) + def test_add_constraint(self): project_state = self.set_up_test_model("test_addconstraint") gt_check = models.Q(pink__gt=2) |
