From 0ee842bb456cb3854d5546106e1259db83714d34 Mon Sep 17 00:00:00 2001 From: Georgi Yanchev Date: Tue, 11 Feb 2025 15:14:53 -0500 Subject: [PATCH] Fixed #36146 -- Recorded applied/unapplied migrations recursively. --- django/db/migrations/executor.py | 23 +++++----- tests/migrations/test_commands.py | 44 +++++++++++++++++++ .../0001_initial.py | 21 +++++++++ .../0002_auto.py | 12 +++++ .../0003_squashed_0001_and_0002.py | 22 ++++++++++ .../0004_auto.py | 12 +++++ .../0005_squashed_0003_and_0004.py | 25 +++++++++++ .../__init__.py | 0 8 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 tests/migrations/test_migrations_squashed_double/0001_initial.py create mode 100644 tests/migrations/test_migrations_squashed_double/0002_auto.py create mode 100644 tests/migrations/test_migrations_squashed_double/0003_squashed_0001_and_0002.py create mode 100644 tests/migrations/test_migrations_squashed_double/0004_auto.py create mode 100644 tests/migrations/test_migrations_squashed_double/0005_squashed_0003_and_0004.py create mode 100644 tests/migrations/test_migrations_squashed_double/__init__.py diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py index 13afa5988f..ebfe8572fe 100644 --- a/django/db/migrations/executor.py +++ b/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) diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index dd54e4fe50..3b4da2bbb7 100644 --- a/tests/migrations/test_commands.py +++ b/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" diff --git a/tests/migrations/test_migrations_squashed_double/0001_initial.py b/tests/migrations/test_migrations_squashed_double/0001_initial.py new file mode 100644 index 0000000000..03b4ed839d --- /dev/null +++ b/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()), + ], + ), + ] diff --git a/tests/migrations/test_migrations_squashed_double/0002_auto.py b/tests/migrations/test_migrations_squashed_double/0002_auto.py new file mode 100644 index 0000000000..c04820c6a4 --- /dev/null +++ b/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), + ), + ] diff --git a/tests/migrations/test_migrations_squashed_double/0003_squashed_0001_and_0002.py b/tests/migrations/test_migrations_squashed_double/0003_squashed_0001_and_0002.py new file mode 100644 index 0000000000..708f73a105 --- /dev/null +++ b/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)), + ], + ), + ] diff --git a/tests/migrations/test_migrations_squashed_double/0004_auto.py b/tests/migrations/test_migrations_squashed_double/0004_auto.py new file mode 100644 index 0000000000..d3a9769c9a --- /dev/null +++ b/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), + ), + ] diff --git a/tests/migrations/test_migrations_squashed_double/0005_squashed_0003_and_0004.py b/tests/migrations/test_migrations_squashed_double/0005_squashed_0003_and_0004.py new file mode 100644 index 0000000000..246d68272d --- /dev/null +++ b/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)), + ], + ), + ] diff --git a/tests/migrations/test_migrations_squashed_double/__init__.py b/tests/migrations/test_migrations_squashed_double/__init__.py new file mode 100644 index 0000000000..e69de29bb2