From db588d4f0edde5095f8975be1da4e693268b86b3 Mon Sep 17 00:00:00 2001 From: David Wobrock Date: Thu, 2 Jun 2022 21:59:50 +0200 Subject: [PATCH] Refs #27236 -- Split index_together and unique_together autodetector tests. --- tests/migrations/test_autodetector.py | 978 +++++++++++++++++++------- 1 file changed, 721 insertions(+), 257 deletions(-) diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py index 14a1faa50ca..09da03c9fd7 100644 --- a/tests/migrations/test_autodetector.py +++ b/tests/migrations/test_autodetector.py @@ -1003,60 +1003,6 @@ class AutodetectorTests(BaseAutodetectorTests): ], }, ) - book_foo_together = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("author", "title")}, - "unique_together": {("author", "title")}, - }, - ) - book_foo_together_2 = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("title", "author")}, - "unique_together": {("title", "author")}, - }, - ) - book_foo_together_3 = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("newfield", models.IntegerField()), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("title", "newfield")}, - "unique_together": {("title", "newfield")}, - }, - ) - book_foo_together_4 = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("newfield2", models.IntegerField()), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "index_together": {("title", "newfield2")}, - "unique_together": {("title", "newfield2")}, - }, - ) book_unique_together = ModelState( "otherapp", "Book", @@ -1069,6 +1015,44 @@ class AutodetectorTests(BaseAutodetectorTests): "unique_together": {("author", "title")}, }, ) + book_unique_together_2 = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "unique_together": {("title", "author")}, + }, + ) + book_unique_together_3 = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("newfield", models.IntegerField()), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "unique_together": {("title", "newfield")}, + }, + ) + book_unique_together_4 = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("newfield2", models.IntegerField()), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "unique_together": {("title", "newfield2")}, + }, + ) attribution = ModelState( "otherapp", "Attribution", @@ -2455,23 +2439,20 @@ class AutodetectorTests(BaseAutodetectorTests): self.assertNumberMigrations(changes, "testapp", 1) self.assertOperationTypes(changes, "testapp", 0, ["AlterField"]) - def test_empty_foo_together(self): - """ - #23452 - Empty unique/index_together shouldn't generate a migration. - """ + def test_empty_unique_together(self): + """Empty unique_together shouldn't generate a migration.""" # Explicitly testing for not specified, since this is the case after # a CreateModel operation w/o any definition on the original model model_state_not_specified = ModelState( "a", "model", [("id", models.AutoField(primary_key=True))] ) # Explicitly testing for None, since this was the issue in #23452 after - # an AlterFooTogether operation with e.g. () as value + # an AlterUniqueTogether operation with e.g. () as value model_state_none = ModelState( "a", "model", [("id", models.AutoField(primary_key=True))], { - "index_together": None, "unique_together": None, }, ) @@ -2482,7 +2463,6 @@ class AutodetectorTests(BaseAutodetectorTests): "model", [("id", models.AutoField(primary_key=True))], { - "index_together": set(), "unique_together": set(), }, ) @@ -2612,79 +2592,6 @@ class AutodetectorTests(BaseAutodetectorTests): old_name="book_title_author_idx", ) - def test_rename_index_together_to_index(self): - changes = self.get_changes( - [self.author_empty, self.book_foo_together], - [self.author_empty, self.book_indexes], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, "otherapp", 0, ["RenameIndex", "AlterUniqueTogether"] - ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 0, - model_name="book", - new_name="book_title_author_idx", - old_fields=("author", "title"), - ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 1, - name="book", - unique_together=set(), - ) - - def test_rename_index_together_to_index_extra_options(self): - # Indexes with extra options don't match indexes in index_together. - book_partial_index = ModelState( - "otherapp", - "Book", - [ - ("id", models.AutoField(primary_key=True)), - ("author", models.ForeignKey("testapp.Author", models.CASCADE)), - ("title", models.CharField(max_length=200)), - ], - { - "indexes": [ - models.Index( - fields=["author", "title"], - condition=models.Q(title__startswith="The"), - name="book_title_author_idx", - ) - ], - }, - ) - changes = self.get_changes( - [self.author_empty, self.book_foo_together], - [self.author_empty, book_partial_index], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["AlterUniqueTogether", "AlterIndexTogether", "AddIndex"], - ) - - def test_rename_index_together_to_index_order_fields(self): - # Indexes with reordered fields don't match indexes in index_together. - changes = self.get_changes( - [self.author_empty, self.book_foo_together], - [self.author_empty, self.book_unordered_indexes], - ) - self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, - "otherapp", - 0, - ["AlterUniqueTogether", "AlterIndexTogether", "AddIndex"], - ) - def test_order_fields_indexes(self): """Test change detection of reordering of fields in indexes.""" changes = self.get_changes( @@ -2764,16 +2671,15 @@ class AutodetectorTests(BaseAutodetectorTests): changes, "testapp", 0, 0, model_name="author", name="name_contains_bob" ) - def test_add_foo_together(self): - """Tests index/unique_together detection.""" + def test_add_unique_together(self): + """Tests unique_together detection.""" changes = self.get_changes( - [self.author_empty, self.book], [self.author_empty, self.book_foo_together] + [self.author_empty, self.book], + [self.author_empty, self.book_unique_together], ) # Right number/type of migrations? self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, "otherapp", 0, ["AlterUniqueTogether", "AlterIndexTogether"] - ) + self.assertOperationTypes(changes, "otherapp", 0, ["AlterUniqueTogether"]) self.assertOperationAttributes( changes, "otherapp", @@ -2782,31 +2688,24 @@ class AutodetectorTests(BaseAutodetectorTests): name="book", unique_together={("author", "title")}, ) - self.assertOperationAttributes( - changes, "otherapp", 0, 1, name="book", index_together={("author", "title")} - ) - def test_remove_foo_together(self): - """Tests index/unique_together detection.""" + def test_remove_unique_together(self): + """Tests unique_together detection.""" changes = self.get_changes( - [self.author_empty, self.book_foo_together], [self.author_empty, self.book] + [self.author_empty, self.book_unique_together], + [self.author_empty, self.book], ) # Right number/type of migrations? self.assertNumberMigrations(changes, "otherapp", 1) - self.assertOperationTypes( - changes, "otherapp", 0, ["AlterUniqueTogether", "AlterIndexTogether"] - ) + self.assertOperationTypes(changes, "otherapp", 0, ["AlterUniqueTogether"]) self.assertOperationAttributes( changes, "otherapp", 0, 0, name="book", unique_together=set() ) - self.assertOperationAttributes( - changes, "otherapp", 0, 1, name="book", index_together=set() - ) - def test_foo_together_remove_fk(self): + def test_unique_together_remove_fk(self): """Tests unique_together and field removal detection & ordering""" changes = self.get_changes( - [self.author_empty, self.book_foo_together], + [self.author_empty, self.book_unique_together], [self.author_empty, self.book_with_no_author], ) # Right number/type of migrations? @@ -2815,37 +2714,34 @@ class AutodetectorTests(BaseAutodetectorTests): changes, "otherapp", 0, - ["AlterUniqueTogether", "AlterIndexTogether", "RemoveField"], + ["AlterUniqueTogether", "RemoveField"], ) self.assertOperationAttributes( changes, "otherapp", 0, 0, name="book", unique_together=set() ) self.assertOperationAttributes( - changes, "otherapp", 0, 1, name="book", index_together=set() - ) - self.assertOperationAttributes( - changes, "otherapp", 0, 2, model_name="book", name="author" + changes, "otherapp", 0, 1, model_name="book", name="author" ) - def test_foo_together_no_changes(self): + def test_unique_together_no_changes(self): """ - index/unique_together doesn't generate a migration if no + unique_together doesn't generate a migration if no changes have been made. """ changes = self.get_changes( - [self.author_empty, self.book_foo_together], - [self.author_empty, self.book_foo_together], + [self.author_empty, self.book_unique_together], + [self.author_empty, self.book_unique_together], ) # Right number of migrations? self.assertEqual(len(changes), 0) - def test_foo_together_ordering(self): + def test_unique_together_ordering(self): """ - index/unique_together also triggers on ordering changes. + unique_together also triggers on ordering changes. """ changes = self.get_changes( - [self.author_empty, self.book_foo_together], - [self.author_empty, self.book_foo_together_2], + [self.author_empty, self.book_unique_together], + [self.author_empty, self.book_unique_together_2], ) # Right number/type of migrations? self.assertNumberMigrations(changes, "otherapp", 1) @@ -2853,10 +2749,7 @@ class AutodetectorTests(BaseAutodetectorTests): changes, "otherapp", 0, - [ - "AlterUniqueTogether", - "AlterIndexTogether", - ], + ["AlterUniqueTogether"], ) self.assertOperationAttributes( changes, @@ -2866,22 +2759,14 @@ class AutodetectorTests(BaseAutodetectorTests): name="book", unique_together={("title", "author")}, ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 1, - name="book", - index_together={("title", "author")}, - ) - def test_add_field_and_foo_together(self): + def test_add_field_and_unique_together(self): """ - Added fields will be created before using them in index/unique_together. + Added fields will be created before using them in unique_together. """ changes = self.get_changes( [self.author_empty, self.book], - [self.author_empty, self.book_foo_together_3], + [self.author_empty, self.book_unique_together_3], ) # Right number/type of migrations? self.assertNumberMigrations(changes, "otherapp", 1) @@ -2889,7 +2774,7 @@ class AutodetectorTests(BaseAutodetectorTests): changes, "otherapp", 0, - ["AddField", "AlterUniqueTogether", "AlterIndexTogether"], + ["AddField", "AlterUniqueTogether"], ) self.assertOperationAttributes( changes, @@ -2899,14 +2784,6 @@ class AutodetectorTests(BaseAutodetectorTests): name="book", unique_together={("title", "newfield")}, ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 2, - name="book", - index_together={("title", "newfield")}, - ) def test_create_model_and_unique_together(self): author = ModelState( @@ -2926,7 +2803,6 @@ class AutodetectorTests(BaseAutodetectorTests): ("title", models.CharField(max_length=200)), ], { - "index_together": {("title", "author")}, "unique_together": {("title", "author")}, }, ) @@ -2937,22 +2813,22 @@ class AutodetectorTests(BaseAutodetectorTests): self.assertEqual(len(changes["otherapp"]), 1) # Right number of actions? migration = changes["otherapp"][0] - self.assertEqual(len(migration.operations), 4) + self.assertEqual(len(migration.operations), 3) # Right actions order? self.assertOperationTypes( changes, "otherapp", 0, - ["CreateModel", "AddField", "AlterUniqueTogether", "AlterIndexTogether"], + ["CreateModel", "AddField", "AlterUniqueTogether"], ) - def test_remove_field_and_foo_together(self): + def test_remove_field_and_unique_together(self): """ - Removed fields will be removed after updating index/unique_together. + Removed fields will be removed after updating unique_together. """ changes = self.get_changes( - [self.author_empty, self.book_foo_together_3], - [self.author_empty, self.book_foo_together], + [self.author_empty, self.book_unique_together_3], + [self.author_empty, self.book_unique_together], ) # Right number/type of migrations? self.assertNumberMigrations(changes, "otherapp", 1) @@ -2960,11 +2836,7 @@ class AutodetectorTests(BaseAutodetectorTests): changes, "otherapp", 0, - [ - "AlterUniqueTogether", - "AlterIndexTogether", - "RemoveField", - ], + ["AlterUniqueTogether", "RemoveField"], ) self.assertOperationAttributes( changes, @@ -2979,20 +2851,12 @@ class AutodetectorTests(BaseAutodetectorTests): "otherapp", 0, 1, - name="book", - index_together={("author", "title")}, - ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 2, model_name="book", name="newfield", ) - def test_alter_field_and_foo_together(self): - """Fields are altered after deleting some index/unique_together.""" + def test_alter_field_and_unique_together(self): + """Fields are altered after deleting some unique_together.""" initial_author = ModelState( "testapp", "Author", @@ -3014,7 +2878,7 @@ class AutodetectorTests(BaseAutodetectorTests): ("age", models.IntegerField()), ], { - "index_together": {("age",)}, + "unique_together": {("age",)}, }, ) changes = self.get_changes([initial_author], [author_reversed_constraints]) @@ -3028,7 +2892,7 @@ class AutodetectorTests(BaseAutodetectorTests): "AlterUniqueTogether", "AlterField", "AlterField", - "AlterIndexTogether", + "AlterUniqueTogether", ], ) self.assertOperationAttributes( @@ -3061,10 +2925,53 @@ class AutodetectorTests(BaseAutodetectorTests): 0, 3, name="author", - index_together={("age",)}, + unique_together={("age",)}, ) - def test_partly_alter_foo_together(self): + def test_partly_alter_unique_together_increase(self): + initial_author = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("age", models.IntegerField()), + ], + { + "unique_together": {("name",)}, + }, + ) + author_new_constraints = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("age", models.IntegerField()), + ], + { + "unique_together": {("name",), ("age",)}, + }, + ) + changes = self.get_changes([initial_author], [author_new_constraints]) + + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes( + changes, + "testapp", + 0, + ["AlterUniqueTogether"], + ) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 0, + name="author", + unique_together={("name",), ("age",)}, + ) + + def test_partly_alter_unique_together_decrease(self): initial_author = ModelState( "testapp", "Author", @@ -3075,10 +2982,9 @@ class AutodetectorTests(BaseAutodetectorTests): ], { "unique_together": {("name",), ("age",)}, - "index_together": {("name",)}, }, ) - author_reversed_constraints = ModelState( + author_new_constraints = ModelState( "testapp", "Author", [ @@ -3087,21 +2993,17 @@ class AutodetectorTests(BaseAutodetectorTests): ("age", models.IntegerField()), ], { - "unique_together": {("age",)}, - "index_together": {("name",), ("age",)}, + "unique_together": {("name",)}, }, ) - changes = self.get_changes([initial_author], [author_reversed_constraints]) + changes = self.get_changes([initial_author], [author_new_constraints]) self.assertNumberMigrations(changes, "testapp", 1) self.assertOperationTypes( changes, "testapp", 0, - [ - "AlterUniqueTogether", - "AlterIndexTogether", - ], + ["AlterUniqueTogether"], ) self.assertOperationAttributes( changes, @@ -3109,22 +3011,14 @@ class AutodetectorTests(BaseAutodetectorTests): 0, 0, name="author", - unique_together={("age",)}, - ) - self.assertOperationAttributes( - changes, - "testapp", - 0, - 1, - name="author", - index_together={("name",), ("age",)}, + unique_together={("name",)}, ) - def test_rename_field_and_foo_together(self): - """Fields are renamed before updating index/unique_together.""" + def test_rename_field_and_unique_together(self): + """Fields are renamed before updating unique_together.""" changes = self.get_changes( - [self.author_empty, self.book_foo_together_3], - [self.author_empty, self.book_foo_together_4], + [self.author_empty, self.book_unique_together_3], + [self.author_empty, self.book_unique_together_4], MigrationQuestioner({"ask_rename": True}), ) # Right number/type of migrations? @@ -3133,11 +3027,7 @@ class AutodetectorTests(BaseAutodetectorTests): changes, "otherapp", 0, - [ - "RenameField", - "AlterUniqueTogether", - "AlterIndexTogether", - ], + ["RenameField", "AlterUniqueTogether"], ) self.assertOperationAttributes( changes, @@ -3147,14 +3037,6 @@ class AutodetectorTests(BaseAutodetectorTests): name="book", unique_together={("title", "newfield2")}, ) - self.assertOperationAttributes( - changes, - "otherapp", - 0, - 2, - name="book", - index_together={("title", "newfield2")}, - ) def test_proxy(self): """The autodetector correctly deals with proxy models.""" @@ -3961,7 +3843,7 @@ class AutodetectorTests(BaseAutodetectorTests): [name for name, field in changes["testapp"][0].operations[0].fields], ) - def test_add_model_order_with_respect_to_index_foo_together(self): + def test_add_model_order_with_respect_to_unique_together(self): changes = self.get_changes( [], [ @@ -3976,7 +3858,6 @@ class AutodetectorTests(BaseAutodetectorTests): ], options={ "order_with_respect_to": "book", - "index_together": {("name", "_order")}, "unique_together": {("id", "_order")}, }, ), @@ -3992,7 +3873,6 @@ class AutodetectorTests(BaseAutodetectorTests): name="Author", options={ "order_with_respect_to": "book", - "index_together": {("name", "_order")}, "unique_together": {("id", "_order")}, }, ) @@ -4054,7 +3934,7 @@ class AutodetectorTests(BaseAutodetectorTests): options={"order_with_respect_to": "book"}, ) - def test_set_alter_order_with_respect_to_index_constraint_foo_together(self): + def test_set_alter_order_with_respect_to_index_constraint_unique_together(self): tests = [ ( "AddIndex", @@ -4075,7 +3955,6 @@ class AutodetectorTests(BaseAutodetectorTests): ] }, ), - ("AlterIndexTogether", {"index_together": {("name", "_order")}}), ("AlterUniqueTogether", {"unique_together": {("id", "_order")}}), ] for operation, extra_option in tests: @@ -4709,6 +4588,591 @@ class AutodetectorTests(BaseAutodetectorTests): self.assertOperationAttributes(changes, "testapp", 0, 0, name="Book") +class AutodetectorIndexTogetherTests(BaseAutodetectorTests): + book_index_together = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "index_together": {("author", "title")}, + }, + ) + book_index_together_2 = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "index_together": {("title", "author")}, + }, + ) + book_index_together_3 = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("newfield", models.IntegerField()), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "index_together": {("title", "newfield")}, + }, + ) + book_index_together_4 = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("newfield2", models.IntegerField()), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "index_together": {("title", "newfield2")}, + }, + ) + + def test_empty_index_together(self): + """Empty index_together shouldn't generate a migration.""" + # Explicitly testing for not specified, since this is the case after + # a CreateModel operation w/o any definition on the original model + model_state_not_specified = ModelState( + "a", "model", [("id", models.AutoField(primary_key=True))] + ) + # Explicitly testing for None, since this was the issue in #23452 after + # an AlterIndexTogether operation with e.g. () as value + model_state_none = ModelState( + "a", + "model", + [("id", models.AutoField(primary_key=True))], + { + "index_together": None, + }, + ) + # Explicitly testing for the empty set, since we now always have sets. + # During removal (('col1', 'col2'),) --> () this becomes set([]) + model_state_empty = ModelState( + "a", + "model", + [("id", models.AutoField(primary_key=True))], + { + "index_together": set(), + }, + ) + + def test(from_state, to_state, msg): + changes = self.get_changes([from_state], [to_state]) + if changes: + ops = ", ".join( + o.__class__.__name__ for o in changes["a"][0].operations + ) + self.fail("Created operation(s) %s from %s" % (ops, msg)) + + tests = ( + ( + model_state_not_specified, + model_state_not_specified, + '"not specified" to "not specified"', + ), + (model_state_not_specified, model_state_none, '"not specified" to "None"'), + ( + model_state_not_specified, + model_state_empty, + '"not specified" to "empty"', + ), + (model_state_none, model_state_not_specified, '"None" to "not specified"'), + (model_state_none, model_state_none, '"None" to "None"'), + (model_state_none, model_state_empty, '"None" to "empty"'), + ( + model_state_empty, + model_state_not_specified, + '"empty" to "not specified"', + ), + (model_state_empty, model_state_none, '"empty" to "None"'), + (model_state_empty, model_state_empty, '"empty" to "empty"'), + ) + + for t in tests: + test(*t) + + def test_rename_index_together_to_index(self): + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together], + [AutodetectorTests.author_empty, AutodetectorTests.book_indexes], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes(changes, "otherapp", 0, ["RenameIndex"]) + self.assertOperationAttributes( + changes, + "otherapp", + 0, + 0, + model_name="book", + new_name="book_title_author_idx", + old_fields=("author", "title"), + ) + + def test_rename_index_together_to_index_extra_options(self): + # Indexes with extra options don't match indexes in index_together. + book_partial_index = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("testapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "indexes": [ + models.Index( + fields=["author", "title"], + condition=models.Q(title__startswith="The"), + name="book_title_author_idx", + ) + ], + }, + ) + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together], + [AutodetectorTests.author_empty, book_partial_index], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["AlterIndexTogether", "AddIndex"], + ) + + def test_rename_index_together_to_index_order_fields(self): + # Indexes with reordered fields don't match indexes in index_together. + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together], + [AutodetectorTests.author_empty, AutodetectorTests.book_unordered_indexes], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["AlterIndexTogether", "AddIndex"], + ) + + def test_add_index_together(self): + changes = self.get_changes( + [AutodetectorTests.author_empty, AutodetectorTests.book], + [AutodetectorTests.author_empty, self.book_index_together], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes(changes, "otherapp", 0, ["AlterIndexTogether"]) + self.assertOperationAttributes( + changes, "otherapp", 0, 0, name="book", index_together={("author", "title")} + ) + + def test_remove_index_together(self): + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together], + [AutodetectorTests.author_empty, AutodetectorTests.book], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes(changes, "otherapp", 0, ["AlterIndexTogether"]) + self.assertOperationAttributes( + changes, "otherapp", 0, 0, name="book", index_together=set() + ) + + def test_index_together_remove_fk(self): + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together], + [AutodetectorTests.author_empty, AutodetectorTests.book_with_no_author], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["AlterIndexTogether", "RemoveField"], + ) + self.assertOperationAttributes( + changes, "otherapp", 0, 0, name="book", index_together=set() + ) + self.assertOperationAttributes( + changes, "otherapp", 0, 1, model_name="book", name="author" + ) + + def test_index_together_no_changes(self): + """ + index_together doesn't generate a migration if no changes have been + made. + """ + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together], + [AutodetectorTests.author_empty, self.book_index_together], + ) + self.assertEqual(len(changes), 0) + + def test_index_together_ordering(self): + """index_together triggers on ordering changes.""" + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together], + [AutodetectorTests.author_empty, self.book_index_together_2], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["AlterIndexTogether"], + ) + self.assertOperationAttributes( + changes, + "otherapp", + 0, + 0, + name="book", + index_together={("title", "author")}, + ) + + def test_add_field_and_index_together(self): + """ + Added fields will be created before using them in index_together. + """ + changes = self.get_changes( + [AutodetectorTests.author_empty, AutodetectorTests.book], + [AutodetectorTests.author_empty, self.book_index_together_3], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["AddField", "AlterIndexTogether"], + ) + self.assertOperationAttributes( + changes, + "otherapp", + 0, + 1, + name="book", + index_together={("title", "newfield")}, + ) + + def test_create_model_and_index_together(self): + author = ModelState( + "otherapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ], + ) + book_with_author = ModelState( + "otherapp", + "Book", + [ + ("id", models.AutoField(primary_key=True)), + ("author", models.ForeignKey("otherapp.Author", models.CASCADE)), + ("title", models.CharField(max_length=200)), + ], + { + "index_together": {("title", "author")}, + }, + ) + changes = self.get_changes( + [AutodetectorTests.book_with_no_author], [author, book_with_author] + ) + self.assertEqual(len(changes["otherapp"]), 1) + migration = changes["otherapp"][0] + self.assertEqual(len(migration.operations), 3) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["CreateModel", "AddField", "AlterIndexTogether"], + ) + + def test_remove_field_and_index_together(self): + """ + Removed fields will be removed after updating index_together. + """ + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together_3], + [AutodetectorTests.author_empty, self.book_index_together], + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["AlterIndexTogether", "RemoveField"], + ) + self.assertOperationAttributes( + changes, + "otherapp", + 0, + 0, + name="book", + index_together={("author", "title")}, + ) + self.assertOperationAttributes( + changes, + "otherapp", + 0, + 1, + model_name="book", + name="newfield", + ) + + def test_alter_field_and_index_together(self): + """Fields are altered after deleting some index_together.""" + initial_author = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("age", models.IntegerField(db_index=True)), + ], + { + "index_together": {("name",)}, + }, + ) + author_reversed_constraints = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200, unique=True)), + ("age", models.IntegerField()), + ], + { + "index_together": {("age",)}, + }, + ) + changes = self.get_changes([initial_author], [author_reversed_constraints]) + + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes( + changes, + "testapp", + 0, + [ + "AlterIndexTogether", + "AlterField", + "AlterField", + "AlterIndexTogether", + ], + ) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 0, + name="author", + index_together=set(), + ) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 1, + model_name="author", + name="age", + ) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 2, + model_name="author", + name="name", + ) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 3, + name="author", + index_together={("age",)}, + ) + + def test_partly_alter_index_together_increase(self): + initial_author = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("age", models.IntegerField()), + ], + { + "index_together": {("name",)}, + }, + ) + author_new_constraints = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("age", models.IntegerField()), + ], + { + "index_together": {("name",), ("age",)}, + }, + ) + changes = self.get_changes([initial_author], [author_new_constraints]) + + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes( + changes, + "testapp", + 0, + ["AlterIndexTogether"], + ) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 0, + name="author", + index_together={("name",), ("age",)}, + ) + + def test_partly_alter_index_together_decrease(self): + initial_author = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("age", models.IntegerField()), + ], + { + "index_together": {("name",), ("age",)}, + }, + ) + author_new_constraints = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("age", models.IntegerField()), + ], + { + "index_together": {("age",)}, + }, + ) + changes = self.get_changes([initial_author], [author_new_constraints]) + + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes( + changes, + "testapp", + 0, + ["AlterIndexTogether"], + ) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 0, + name="author", + index_together={("age",)}, + ) + + def test_rename_field_and_index_together(self): + """Fields are renamed before updating index_together.""" + changes = self.get_changes( + [AutodetectorTests.author_empty, self.book_index_together_3], + [AutodetectorTests.author_empty, self.book_index_together_4], + MigrationQuestioner({"ask_rename": True}), + ) + self.assertNumberMigrations(changes, "otherapp", 1) + self.assertOperationTypes( + changes, + "otherapp", + 0, + ["RenameField", "AlterIndexTogether"], + ) + self.assertOperationAttributes( + changes, + "otherapp", + 0, + 1, + name="book", + index_together={("title", "newfield2")}, + ) + + def test_add_model_order_with_respect_to_index_together(self): + changes = self.get_changes( + [], + [ + AutodetectorTests.book, + ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), + ], + options={ + "order_with_respect_to": "book", + "index_together": {("name", "_order")}, + }, + ), + ], + ) + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes(changes, "testapp", 0, ["CreateModel"]) + self.assertOperationAttributes( + changes, + "testapp", + 0, + 0, + name="Author", + options={ + "order_with_respect_to": "book", + "index_together": {("name", "_order")}, + }, + ) + + def test_set_alter_order_with_respect_to_index_together(self): + after = ModelState( + "testapp", + "Author", + [ + ("id", models.AutoField(primary_key=True)), + ("name", models.CharField(max_length=200)), + ("book", models.ForeignKey("otherapp.Book", models.CASCADE)), + ], + options={ + "order_with_respect_to": "book", + "index_together": {("name", "_order")}, + }, + ) + changes = self.get_changes( + [AutodetectorTests.book, AutodetectorTests.author_with_book], + [AutodetectorTests.book, after], + ) + self.assertNumberMigrations(changes, "testapp", 1) + self.assertOperationTypes( + changes, + "testapp", + 0, + ["AlterOrderWithRespectTo", "AlterIndexTogether"], + ) + + class MigrationSuggestNameTests(SimpleTestCase): def test_no_operations(self): class Migration(migrations.Migration):