Skip to content

Commit 2ee6ca6

Browse files
systemallicasarahboyce
authored andcommitted
[5.1.x] Fixed #34856 -- Fixed references to index_together in historical migrations.
While AlterUniqueTogether has been documented to be still allowed in historical migrations for the foreseeable future it has been crashing since 2abf417 was merged because the latter removed support for Meta.index_together which the migration framework uses to render models to perform schema changes. CreateModel(options["unique_together"]) was also affected. Refs #27236. Co-authored-by: Simon Charette <[email protected]> Backport of b44efdf from main.
1 parent 85c3550 commit 2ee6ca6

File tree

4 files changed

+234
-11
lines changed

4 files changed

+234
-11
lines changed

django/db/migrations/operations/models.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ def database_forwards(self, app_label, schema_editor, from_state, to_state):
9595
model = to_state.apps.get_model(app_label, self.name)
9696
if self.allow_migrate_model(schema_editor.connection.alias, model):
9797
schema_editor.create_model(model)
98+
# While the `index_together` option has been deprecated some
99+
# historical migrations might still have references to them.
100+
# This can be moved to the schema editor once it's adapted to
101+
# from model states instead of rendered models (#29898).
102+
to_model_state = to_state.models[app_label, self.name_lower]
103+
if index_together := to_model_state.options.get("index_together"):
104+
schema_editor.alter_index_together(
105+
model,
106+
set(),
107+
index_together,
108+
)
98109

99110
def database_backwards(self, app_label, schema_editor, from_state, to_state):
100111
model = from_state.apps.get_model(app_label, self.name)
@@ -668,12 +679,13 @@ def state_forwards(self, app_label, state):
668679
def database_forwards(self, app_label, schema_editor, from_state, to_state):
669680
new_model = to_state.apps.get_model(app_label, self.name)
670681
if self.allow_migrate_model(schema_editor.connection.alias, new_model):
671-
old_model = from_state.apps.get_model(app_label, self.name)
682+
from_model_state = from_state.models[app_label, self.name_lower]
683+
to_model_state = to_state.models[app_label, self.name_lower]
672684
alter_together = getattr(schema_editor, "alter_%s" % self.option_name)
673685
alter_together(
674686
new_model,
675-
getattr(old_model._meta, self.option_name, set()),
676-
getattr(new_model._meta, self.option_name, set()),
687+
from_model_state.options.get(self.option_name) or set(),
688+
to_model_state.options.get(self.option_name) or set(),
677689
)
678690

679691
def database_backwards(self, app_label, schema_editor, from_state, to_state):

django/db/migrations/state.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,14 @@ def rename_field(self, app_label, model_name, old_name, new_name):
310310
for from_field_name in from_fields
311311
]
312312
)
313-
# Fix unique_together to refer to the new field.
313+
# Fix index/unique_together to refer to the new field.
314314
options = model_state.options
315-
if "unique_together" in options:
316-
options["unique_together"] = [
317-
[new_name if n == old_name else n for n in together]
318-
for together in options["unique_together"]
319-
]
315+
for option in ("index_together", "unique_together"):
316+
if option in options:
317+
options[option] = [
318+
[new_name if n == old_name else n for n in together]
319+
for together in options[option]
320+
]
320321
# Fix to_fields to refer to the new field.
321322
delay = True
322323
references = get_references(self, model_key, (old_name, found))
@@ -931,7 +932,11 @@ def clone(self):
931932
def render(self, apps):
932933
"""Create a Model object from our current state into the given apps."""
933934
# First, make a Meta object
934-
meta_contents = {"app_label": self.app_label, "apps": apps, **self.options}
935+
meta_options = {**self.options}
936+
# Prune index_together from options as it's no longer an allowed meta
937+
# attribute.
938+
meta_options.pop("index_together", None)
939+
meta_contents = {"app_label": self.app_label, "apps": apps, **meta_options}
935940
meta = type("Meta", (), meta_contents)
936941
# Then, work out our bases
937942
try:

docs/releases/5.1.5.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ Django 5.1.5 fixes several bugs in 5.1.4.
99
Bugfixes
1010
========
1111

12-
* ...
12+
* Fixed a crash when applying migrations with references to the removed
13+
``Meta.index_together`` option (:ticket:`34856`).

tests/migrations/test_operations.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3329,6 +3329,52 @@ def test_rename_field_unique_together(self):
33293329
self.assertColumnExists("test_rnflut_pony", "pink")
33303330
self.assertColumnNotExists("test_rnflut_pony", "blue")
33313331

3332+
def test_rename_field_index_together(self):
3333+
app_label = "test_rnflit"
3334+
operations = [
3335+
migrations.CreateModel(
3336+
"Pony",
3337+
fields=[
3338+
("id", models.AutoField(primary_key=True)),
3339+
("pink", models.IntegerField(default=3)),
3340+
("weight", models.FloatField()),
3341+
],
3342+
options={
3343+
"index_together": [("weight", "pink")],
3344+
},
3345+
),
3346+
]
3347+
project_state = self.apply_operations(app_label, ProjectState(), operations)
3348+
3349+
operation = migrations.RenameField("Pony", "pink", "blue")
3350+
new_state = project_state.clone()
3351+
operation.state_forwards("test_rnflit", new_state)
3352+
self.assertIn("blue", new_state.models["test_rnflit", "pony"].fields)
3353+
self.assertNotIn("pink", new_state.models["test_rnflit", "pony"].fields)
3354+
# index_together has the renamed column.
3355+
self.assertIn(
3356+
"blue", new_state.models["test_rnflit", "pony"].options["index_together"][0]
3357+
)
3358+
self.assertNotIn(
3359+
"pink", new_state.models["test_rnflit", "pony"].options["index_together"][0]
3360+
)
3361+
3362+
# Rename field.
3363+
self.assertColumnExists("test_rnflit_pony", "pink")
3364+
self.assertColumnNotExists("test_rnflit_pony", "blue")
3365+
with connection.schema_editor() as editor:
3366+
operation.database_forwards("test_rnflit", editor, project_state, new_state)
3367+
self.assertColumnExists("test_rnflit_pony", "blue")
3368+
self.assertColumnNotExists("test_rnflit_pony", "pink")
3369+
# The index constraint has been ported over.
3370+
self.assertIndexExists("test_rnflit_pony", ["weight", "blue"])
3371+
# Reversal.
3372+
with connection.schema_editor() as editor:
3373+
operation.database_backwards(
3374+
"test_rnflit", editor, new_state, project_state
3375+
)
3376+
self.assertIndexExists("test_rnflit_pony", ["weight", "pink"])
3377+
33323378
def test_rename_field_with_db_column(self):
33333379
project_state = self.apply_operations(
33343380
"test_rfwdbc",
@@ -3822,6 +3868,63 @@ def test_rename_index_arguments(self):
38223868
with self.assertRaisesMessage(ValueError, msg):
38233869
migrations.RenameIndex("Pony", new_name="new_idx_name")
38243870

3871+
def test_rename_index_unnamed_index(self):
3872+
app_label = "test_rninui"
3873+
operations = [
3874+
migrations.CreateModel(
3875+
"Pony",
3876+
fields=[
3877+
("id", models.AutoField(primary_key=True)),
3878+
("pink", models.IntegerField(default=3)),
3879+
("weight", models.FloatField()),
3880+
],
3881+
options={
3882+
"index_together": [("weight", "pink")],
3883+
},
3884+
),
3885+
]
3886+
project_state = self.apply_operations(app_label, ProjectState(), operations)
3887+
table_name = app_label + "_pony"
3888+
self.assertIndexNameNotExists(table_name, "new_pony_test_idx")
3889+
operation = migrations.RenameIndex(
3890+
"Pony", new_name="new_pony_test_idx", old_fields=("weight", "pink")
3891+
)
3892+
self.assertEqual(
3893+
operation.describe(),
3894+
"Rename unnamed index for ('weight', 'pink') on Pony to new_pony_test_idx",
3895+
)
3896+
self.assertEqual(
3897+
operation.migration_name_fragment,
3898+
"rename_pony_weight_pink_new_pony_test_idx",
3899+
)
3900+
new_state = project_state.clone()
3901+
operation.state_forwards(app_label, new_state)
3902+
# Rename index.
3903+
with connection.schema_editor() as editor:
3904+
operation.database_forwards(app_label, editor, project_state, new_state)
3905+
self.assertIndexNameExists(table_name, "new_pony_test_idx")
3906+
# Reverse is a no-op.
3907+
with connection.schema_editor() as editor, self.assertNumQueries(0):
3908+
operation.database_backwards(app_label, editor, new_state, project_state)
3909+
self.assertIndexNameExists(table_name, "new_pony_test_idx")
3910+
# Reapply, RenameIndex operation is a noop when the old and new name
3911+
# match.
3912+
with connection.schema_editor() as editor:
3913+
operation.database_forwards(app_label, editor, new_state, project_state)
3914+
self.assertIndexNameExists(table_name, "new_pony_test_idx")
3915+
# Deconstruction.
3916+
definition = operation.deconstruct()
3917+
self.assertEqual(definition[0], "RenameIndex")
3918+
self.assertEqual(definition[1], [])
3919+
self.assertEqual(
3920+
definition[2],
3921+
{
3922+
"model_name": "Pony",
3923+
"new_name": "new_pony_test_idx",
3924+
"old_fields": ("weight", "pink"),
3925+
},
3926+
)
3927+
38253928
def test_rename_index_unknown_unnamed_index(self):
38263929
app_label = "test_rninuui"
38273930
project_state = self.set_up_test_model(app_label)
@@ -3892,6 +3995,33 @@ def test_rename_index_state_forwards(self):
38923995
self.assertIsNot(old_model, new_model)
38933996
self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx")
38943997

3998+
def test_rename_index_state_forwards_unnamed_index(self):
3999+
app_label = "test_rnidsfui"
4000+
operations = [
4001+
migrations.CreateModel(
4002+
"Pony",
4003+
fields=[
4004+
("id", models.AutoField(primary_key=True)),
4005+
("pink", models.IntegerField(default=3)),
4006+
("weight", models.FloatField()),
4007+
],
4008+
options={
4009+
"index_together": [("weight", "pink")],
4010+
},
4011+
),
4012+
]
4013+
project_state = self.apply_operations(app_label, ProjectState(), operations)
4014+
old_model = project_state.apps.get_model(app_label, "Pony")
4015+
new_state = project_state.clone()
4016+
4017+
operation = migrations.RenameIndex(
4018+
"Pony", new_name="new_pony_pink_idx", old_fields=("weight", "pink")
4019+
)
4020+
operation.state_forwards(app_label, new_state)
4021+
new_model = new_state.apps.get_model(app_label, "Pony")
4022+
self.assertIsNot(old_model, new_model)
4023+
self.assertEqual(new_model._meta.indexes[0].name, "new_pony_pink_idx")
4024+
38954025
@skipUnlessDBFeature("supports_expression_indexes")
38964026
def test_add_func_index(self):
38974027
app_label = "test_addfuncin"
@@ -4011,6 +4141,58 @@ def test_alter_field_with_index(self):
40114141
# Ensure the index is still there
40124142
self.assertIndexExists("test_alflin_pony", ["pink"])
40134143

4144+
def test_alter_index_together(self):
4145+
"""
4146+
Tests the AlterIndexTogether operation.
4147+
"""
4148+
project_state = self.set_up_test_model("test_alinto")
4149+
# Test the state alteration
4150+
operation = migrations.AlterIndexTogether("Pony", [("pink", "weight")])
4151+
self.assertEqual(
4152+
operation.describe(), "Alter index_together for Pony (1 constraint(s))"
4153+
)
4154+
self.assertEqual(
4155+
operation.migration_name_fragment,
4156+
"alter_pony_index_together",
4157+
)
4158+
new_state = project_state.clone()
4159+
operation.state_forwards("test_alinto", new_state)
4160+
self.assertEqual(
4161+
len(
4162+
project_state.models["test_alinto", "pony"].options.get(
4163+
"index_together", set()
4164+
)
4165+
),
4166+
0,
4167+
)
4168+
self.assertEqual(
4169+
len(
4170+
new_state.models["test_alinto", "pony"].options.get(
4171+
"index_together", set()
4172+
)
4173+
),
4174+
1,
4175+
)
4176+
# Make sure there's no matching index
4177+
self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"])
4178+
# Test the database alteration
4179+
with connection.schema_editor() as editor:
4180+
operation.database_forwards("test_alinto", editor, project_state, new_state)
4181+
self.assertIndexExists("test_alinto_pony", ["pink", "weight"])
4182+
# And test reversal
4183+
with connection.schema_editor() as editor:
4184+
operation.database_backwards(
4185+
"test_alinto", editor, new_state, project_state
4186+
)
4187+
self.assertIndexNotExists("test_alinto_pony", ["pink", "weight"])
4188+
# And deconstruction
4189+
definition = operation.deconstruct()
4190+
self.assertEqual(definition[0], "AlterIndexTogether")
4191+
self.assertEqual(definition[1], [])
4192+
self.assertEqual(
4193+
definition[2], {"name": "Pony", "index_together": {("pink", "weight")}}
4194+
)
4195+
40144196
def test_alter_index_together_remove(self):
40154197
operation = migrations.AlterIndexTogether("Pony", None)
40164198
self.assertEqual(
@@ -4021,6 +4203,29 @@ def test_alter_index_together_remove(self):
40214203
"~ Alter index_together for Pony (0 constraint(s))",
40224204
)
40234205

4206+
@skipUnlessDBFeature("allows_multiple_constraints_on_same_fields")
4207+
def test_alter_index_together_remove_with_unique_together(self):
4208+
app_label = "test_alintoremove_wunto"
4209+
table_name = "%s_pony" % app_label
4210+
project_state = self.set_up_test_model(app_label, unique_together=True)
4211+
self.assertUniqueConstraintExists(table_name, ["pink", "weight"])
4212+
# Add index together.
4213+
new_state = project_state.clone()
4214+
operation = migrations.AlterIndexTogether("Pony", [("pink", "weight")])
4215+
operation.state_forwards(app_label, new_state)
4216+
with connection.schema_editor() as editor:
4217+
operation.database_forwards(app_label, editor, project_state, new_state)
4218+
self.assertIndexExists(table_name, ["pink", "weight"])
4219+
# Remove index together.
4220+
project_state = new_state
4221+
new_state = project_state.clone()
4222+
operation = migrations.AlterIndexTogether("Pony", set())
4223+
operation.state_forwards(app_label, new_state)
4224+
with connection.schema_editor() as editor:
4225+
operation.database_forwards(app_label, editor, project_state, new_state)
4226+
self.assertIndexNotExists(table_name, ["pink", "weight"])
4227+
self.assertUniqueConstraintExists(table_name, ["pink", "weight"])
4228+
40244229
def test_add_constraint(self):
40254230
project_state = self.set_up_test_model("test_addconstraint")
40264231
gt_check = models.Q(pink__gt=2)

0 commit comments

Comments
 (0)