Просмотр исходного кода

Fixed #36146 -- Recorded applied/unapplied migrations recursively.

Georgi Yanchev 1 месяц назад
Родитель
Сommit
0ee842bb45

+ 11 - 12
django/db/migrations/executor.py

@@ -254,22 +254,25 @@ class MigrationExecutor:
                 ) as schema_editor:
                     state = migration.apply(state, schema_editor)
                     if not schema_editor.deferred_sql:
-                        self.record_migration(migration)
+                        self.record_migration(migration.app_label, migration.name)
                         migration_recorded = True
         if not migration_recorded:
-            self.record_migration(migration)
+            self.record_migration(migration.app_label, migration.name)
         # Report progress
         if self.progress_callback:
             self.progress_callback("apply_success", migration, fake)
         return state
 
-    def record_migration(self, migration):
+    def record_migration(self, app_label, name, forward=True):
+        migration = self.loader.disk_migrations.get((app_label, name))
         # For replacement migrations, record individual statuses
-        if migration.replaces:
-            for app_label, name in migration.replaces:
-                self.recorder.record_applied(app_label, name)
+        if migration and migration.replaces:
+            for replaced_app_label, replaced_name in migration.replaces:
+                self.record_migration(replaced_app_label, replaced_name, forward)
+        if forward:
+            self.recorder.record_applied(app_label, name)
         else:
-            self.recorder.record_applied(migration.app_label, migration.name)
+            self.recorder.record_unapplied(app_label, name)
 
     def unapply_migration(self, state, migration, fake=False):
         """Run a migration backwards."""
@@ -280,11 +283,7 @@ class MigrationExecutor:
                 atomic=migration.atomic
             ) as schema_editor:
                 state = migration.unapply(state, schema_editor)
-        # For replacement migrations, also record individual statuses.
-        if migration.replaces:
-            for app_label, name in migration.replaces:
-                self.recorder.record_unapplied(app_label, name)
-        self.recorder.record_unapplied(migration.app_label, migration.name)
+        self.record_migration(migration.app_label, migration.name, forward=False)
         # Report progress
         if self.progress_callback:
             self.progress_callback("unapply_success", migration, fake)

+ 44 - 0
tests/migrations/test_commands.py

@@ -3073,6 +3073,50 @@ class SquashMigrationsTests(MigrationTestBase):
                 ],
             )
 
+    def test_double_replaced_migrations_are_recorded(self):
+        """
+        All recursively replaced migrations should be recorded/unrecorded, when
+        migrating an app with double squashed migrations.
+        """
+        out = io.StringIO()
+        with self.temporary_migration_module(
+            module="migrations.test_migrations_squashed_double"
+        ):
+            recorder = MigrationRecorder(connection)
+            applied_app_labels = [
+                app_label for app_label, _ in recorder.applied_migrations()
+            ]
+            self.assertNotIn("migrations", applied_app_labels)
+
+            call_command(
+                "migrate", "migrations", "--plan", interactive=False, stdout=out
+            )
+            migration_plan = re.findall("migrations.(.+)\n", out.getvalue())
+            # Only the top-level replacement migration should be applied.
+            self.assertEqual(migration_plan, ["0005_squashed_0003_and_0004"])
+
+            call_command("migrate", "migrations", interactive=False, verbosity=0)
+            applied_migrations = recorder.applied_migrations()
+            # Make sure all replaced migrations are recorded.
+            self.assertIn(("migrations", "0001_initial"), applied_migrations)
+            self.assertIn(("migrations", "0002_auto"), applied_migrations)
+            self.assertIn(
+                ("migrations", "0003_squashed_0001_and_0002"), applied_migrations
+            )
+            self.assertIn(("migrations", "0004_auto"), applied_migrations)
+            self.assertIn(
+                ("migrations", "0005_squashed_0003_and_0004"), applied_migrations
+            )
+
+            # Unapply all migrations from this app.
+            call_command(
+                "migrate", "migrations", "zero", interactive=False, verbosity=0
+            )
+            applied_app_labels = [
+                app_label for app_label, _ in recorder.applied_migrations()
+            ]
+            self.assertNotIn("migrations", applied_app_labels)
+
     def test_squashmigrations_initial_attribute(self):
         with self.temporary_migration_module(
             module="migrations.test_migrations"

+ 21 - 0
tests/migrations/test_migrations_squashed_double/0001_initial.py

@@ -0,0 +1,21 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    operations = [
+        migrations.CreateModel(
+            name="A",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("foo", models.BooleanField()),
+            ],
+        ),
+    ]

+ 12 - 0
tests/migrations/test_migrations_squashed_double/0002_auto.py

@@ -0,0 +1,12 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [("migrations", "0001_initial")]
+    operations = [
+        migrations.AlterField(
+            model_name="a",
+            name="foo",
+            field=models.BooleanField(default=True),
+        ),
+    ]

+ 22 - 0
tests/migrations/test_migrations_squashed_double/0003_squashed_0001_and_0002.py

@@ -0,0 +1,22 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    replaces = [("migrations", "0001_initial"), ("migrations", "0002_auto")]
+    operations = [
+        migrations.CreateModel(
+            name="A",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("foo", models.BooleanField(default=True)),
+            ],
+        ),
+    ]

+ 12 - 0
tests/migrations/test_migrations_squashed_double/0004_auto.py

@@ -0,0 +1,12 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [("migrations", "0002_auto")]
+    operations = [
+        migrations.AlterField(
+            model_name="a",
+            name="foo",
+            field=models.BooleanField(default=False),
+        ),
+    ]

+ 25 - 0
tests/migrations/test_migrations_squashed_double/0005_squashed_0003_and_0004.py

@@ -0,0 +1,25 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    replaces = [
+        ("migrations", "0003_squashed_0001_and_0002"),
+        ("migrations", "0004_auto"),
+    ]
+    operations = [
+        migrations.CreateModel(
+            name="A",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("foo", models.BooleanField(default=False)),
+            ],
+        ),
+    ]

+ 0 - 0
tests/migrations/test_migrations_squashed_double/__init__.py