Fixed #34529, Refs #34525 -- Reduced index operations with Meta.indexes/index_together when optimizing migrations.

This makes squashing migrations an available path for changing
Meta.index_together, which is deprecated, to Meta.indexes.

Follow up to f810325721.
This commit is contained in:
Mariusz Felisiak 2023-05-03 13:06:19 +02:00 committed by GitHub
parent 83339d2103
commit 8e2460d599
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 326 additions and 68 deletions

View File

@ -303,6 +303,71 @@ class CreateModel(ModelOperation):
managers=self.managers, managers=self.managers,
), ),
] ]
elif (
isinstance(operation, IndexOperation)
and self.name_lower == operation.model_name_lower
):
if isinstance(operation, AddIndex):
return [
CreateModel(
self.name,
fields=self.fields,
options={
**self.options,
"indexes": [
*self.options.get("indexes", []),
operation.index,
],
},
bases=self.bases,
managers=self.managers,
),
]
elif isinstance(operation, RemoveIndex):
options_indexes = [
index
for index in self.options.get("indexes", [])
if index.name != operation.name
]
return [
CreateModel(
self.name,
fields=self.fields,
options={
**self.options,
"indexes": options_indexes,
},
bases=self.bases,
managers=self.managers,
),
]
elif isinstance(operation, RenameIndex) and operation.old_fields:
options_index_together = {
fields
for fields in self.options.get("index_together", [])
if fields != operation.old_fields
}
if options_index_together:
self.options["index_together"] = options_index_together
else:
self.options.pop("index_together", None)
return [
CreateModel(
self.name,
fields=self.fields,
options={
**self.options,
"indexes": [
*self.options.get("indexes", []),
models.Index(
fields=operation.old_fields, name=operation.new_name
),
],
},
bases=self.bases,
managers=self.managers,
),
]
return super().reduce(operation, app_label) return super().reduce(operation, app_label)

View File

@ -53,3 +53,8 @@ Bugfixes
* Fixed a regression in Django 4.2 where breadcrumbs didn't appear on admin * Fixed a regression in Django 4.2 where breadcrumbs didn't appear on admin
site app index views (:ticket:`34512`). site app index views (:ticket:`34512`).
* Made squashing migrations reduce ``AddIndex``, ``RemoveIndex``,
``RenameIndex``, and ``CreateModel`` operations which allows removing a
deprecated ``Meta.index_together`` option from historical migrations and use
``Meta.indexes`` instead (:ticket:`34525`).

View File

@ -502,7 +502,8 @@ Should become::
Running the :djadmin:`makemigrations` command will generate a migration Running the :djadmin:`makemigrations` command will generate a migration
containing a :class:`~django.db.migrations.operations.RenameIndex` operation containing a :class:`~django.db.migrations.operations.RenameIndex` operation
which will rename the existing index. which will rename the existing index. Next, consider squashing migrations to
remove ``index_together`` from historical migrations.
The ``AlterIndexTogether`` migration operation is now officially supported only The ``AlterIndexTogether`` migration operation is now officially supported only
for pre-Django 4.2 migration files. For backward compatibility reasons, it's for pre-Django 4.2 migration files. For backward compatibility reasons, it's

View File

@ -2266,10 +2266,9 @@ class AutodetectorTests(BaseAutodetectorTests):
changes, changes,
"eggs", "eggs",
0, 0,
["CreateModel", "CreateModel", "AddIndex", "AlterUniqueTogether"], ["CreateModel", "CreateModel"],
) )
self.assertNotIn("unique_together", changes["eggs"][0].operations[0].options) self.assertNotIn("unique_together", changes["eggs"][0].operations[0].options)
self.assertNotIn("unique_together", changes["eggs"][0].operations[1].options)
self.assertMigrationDependencies(changes, "eggs", 0, []) self.assertMigrationDependencies(changes, "eggs", 0, [])
def test_alter_db_table_add(self): def test_alter_db_table_add(self):
@ -2565,6 +2564,9 @@ class AutodetectorTests(BaseAutodetectorTests):
def test_create_model_with_indexes(self): def test_create_model_with_indexes(self):
"""Test creation of new model with indexes already defined.""" """Test creation of new model with indexes already defined."""
added_index = models.Index(
fields=["name"], name="create_model_with_indexes_idx"
)
author = ModelState( author = ModelState(
"otherapp", "otherapp",
"Author", "Author",
@ -2573,25 +2575,25 @@ class AutodetectorTests(BaseAutodetectorTests):
("name", models.CharField(max_length=200)), ("name", models.CharField(max_length=200)),
], ],
{ {
"indexes": [ "indexes": [added_index],
models.Index(fields=["name"], name="create_model_with_indexes_idx")
]
}, },
) )
changes = self.get_changes([], [author]) changes = self.get_changes([], [author])
added_index = models.Index(
fields=["name"], name="create_model_with_indexes_idx"
)
# Right number of migrations? # Right number of migrations?
self.assertEqual(len(changes["otherapp"]), 1) self.assertEqual(len(changes["otherapp"]), 1)
# Right number of actions? # Right number of actions?
migration = changes["otherapp"][0] migration = changes["otherapp"][0]
self.assertEqual(len(migration.operations), 2) self.assertEqual(len(migration.operations), 1)
# Right actions order? # Right actions order?
self.assertOperationTypes(changes, "otherapp", 0, ["CreateModel", "AddIndex"]) self.assertOperationTypes(changes, "otherapp", 0, ["CreateModel"])
self.assertOperationAttributes(changes, "otherapp", 0, 0, name="Author") self.assertOperationAttributes(changes, "otherapp", 0, 0, name="Author")
self.assertOperationAttributes( self.assertOperationAttributes(
changes, "otherapp", 0, 1, model_name="author", index=added_index changes,
"otherapp",
0,
0,
name="Author",
options={"indexes": [added_index]},
) )
def test_add_indexes(self): def test_add_indexes(self):
@ -4043,62 +4045,69 @@ class AutodetectorTests(BaseAutodetectorTests):
}, },
) )
def test_add_model_order_with_respect_to_index_constraint(self): def test_add_model_order_with_respect_to_constraint(self):
tests = [ after = ModelState(
( "testapp",
"AddIndex", "Author",
{ [
"indexes": [ ("id", models.AutoField(primary_key=True)),
models.Index(fields=["_order"], name="book_order_idx"), ("name", models.CharField(max_length=200)),
] ("book", models.ForeignKey("otherapp.Book", models.CASCADE)),
}, ],
), options={
( "order_with_respect_to": "book",
"AddConstraint", "constraints": [
{ models.CheckConstraint(
"constraints": [ check=models.Q(_order__gt=1), name="book_order_gt_1"
models.CheckConstraint( ),
check=models.Q(_order__gt=1), ],
name="book_order_gt_1", },
), )
] changes = self.get_changes([], [self.book, after])
}, self.assertNumberMigrations(changes, "testapp", 1)
), self.assertOperationTypes(
] changes,
for operation, extra_option in tests: "testapp",
with self.subTest(operation=operation): 0,
after = ModelState( ["CreateModel", "AddConstraint"],
"testapp", )
"Author", self.assertOperationAttributes(
[ changes,
("id", models.AutoField(primary_key=True)), "testapp",
("name", models.CharField(max_length=200)), 0,
("book", models.ForeignKey("otherapp.Book", models.CASCADE)), 0,
], name="Author",
options={ options={"order_with_respect_to": "book"},
"order_with_respect_to": "book", )
**extra_option,
}, def test_add_model_order_with_respect_to_index(self):
) after = ModelState(
changes = self.get_changes([], [self.book, after]) "testapp",
self.assertNumberMigrations(changes, "testapp", 1) "Author",
self.assertOperationTypes( [
changes, ("id", models.AutoField(primary_key=True)),
"testapp", ("name", models.CharField(max_length=200)),
0, ("book", models.ForeignKey("otherapp.Book", models.CASCADE)),
[ ],
"CreateModel", options={
operation, "order_with_respect_to": "book",
], "indexes": [models.Index(fields=["_order"], name="book_order_idx")],
) },
self.assertOperationAttributes( )
changes, changes = self.get_changes([], [self.book, after])
"testapp", self.assertNumberMigrations(changes, "testapp", 1)
0, self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"])
0, self.assertOperationAttributes(
name="Author", changes,
options={"order_with_respect_to": "book"}, "testapp",
) 0,
0,
name="Author",
options={
"order_with_respect_to": "book",
"indexes": [models.Index(fields=["_order"], name="book_order_idx")],
},
)
def test_set_alter_order_with_respect_to_index_constraint_unique_together(self): def test_set_alter_order_with_respect_to_index_constraint_unique_together(self):
tests = [ tests = [

View File

@ -1172,3 +1172,181 @@ class OptimizerTests(SimpleTestCase):
], ],
[], [],
) )
def test_create_model_add_index(self):
self.assertOptimizesTo(
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"indexes": [models.Index(fields=["age"], name="idx_pony_age")],
},
),
migrations.AddIndex(
"Pony",
models.Index(fields=["weight"], name="idx_pony_weight"),
),
],
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"indexes": [
models.Index(fields=["age"], name="idx_pony_age"),
models.Index(fields=["weight"], name="idx_pony_weight"),
],
},
),
],
)
def test_create_model_remove_index(self):
self.assertOptimizesTo(
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"indexes": [
models.Index(fields=["age"], name="idx_pony_age"),
models.Index(fields=["weight"], name="idx_pony_weight"),
],
},
),
migrations.RemoveIndex("Pony", "idx_pony_age"),
],
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"indexes": [
models.Index(fields=["weight"], name="idx_pony_weight"),
],
},
),
],
)
def test_create_model_remove_index_together_rename_index(self):
self.assertOptimizesTo(
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"index_together": [("age", "weight")],
},
),
migrations.RenameIndex(
"Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight")
),
],
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"indexes": [
models.Index(
fields=["age", "weight"], name="idx_pony_age_weight"
),
],
},
),
],
)
def test_create_model_index_together_rename_index(self):
self.assertOptimizesTo(
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
("height", models.IntegerField()),
("rank", models.IntegerField()),
],
options={
"index_together": [("age", "weight"), ("height", "rank")],
},
),
migrations.RenameIndex(
"Pony", new_name="idx_pony_age_weight", old_fields=("age", "weight")
),
],
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
("height", models.IntegerField()),
("rank", models.IntegerField()),
],
options={
"index_together": {("height", "rank")},
"indexes": [
models.Index(
fields=["age", "weight"], name="idx_pony_age_weight"
),
],
},
),
],
)
def test_create_model_rename_index_no_old_fields(self):
self.assertOptimizesTo(
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"indexes": [models.Index(fields=["age"], name="idx_pony_age")],
},
),
migrations.RenameIndex(
"Pony", new_name="idx_pony_age_new", old_name="idx_pony_age"
),
],
[
migrations.CreateModel(
name="Pony",
fields=[
("weight", models.IntegerField()),
("age", models.IntegerField()),
],
options={
"indexes": [models.Index(fields=["age"], name="idx_pony_age")],
},
),
migrations.RenameIndex(
"Pony", new_name="idx_pony_age_new", old_name="idx_pony_age"
),
],
)