summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Wobrock <david.wobrock@backmarket.com>2026-03-16 22:39:40 +0100
committerJacob Walls <jacobtylerwalls@gmail.com>2026-06-15 17:40:16 -0400
commitcd385e6b8c16b51f68c1f220ff09a4cfd679af0c (patch)
tree0fe9b5558c804d43de369a68eda774a42cd49809
parent63ff1142b2f0cba1981825c65b62425e9ff7734b (diff)
Fixed #31317 -- Avoided crash in CreateModel with unique_together and AlterUniqueTogether.
-rw-r--r--django/db/backends/base/schema.py22
-rw-r--r--tests/migrations/test_operations.py85
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)