diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py index db9013575e..aa2fe0883b 100644 --- a/django/db/migrations/executor.py +++ b/django/db/migrations/executor.py @@ -40,6 +40,15 @@ class MigrationExecutor: # If the migration is already applied, do backwards mode, # otherwise do forwards mode. elif target in applied: + # If the target is missing, it's likely a replaced migration. + # Reload the graph without replacements. + if ( + self.loader.replace_migrations and + target not in self.loader.graph.node_map + ): + self.loader.replace_migrations = False + self.loader.build_graph() + return self.migration_plan(targets, clean_start=clean_start) # Don't migrate backwards all the way to the target node (that # may roll back dependencies in other apps that don't need to # be rolled back); instead roll back through target's immediate diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py index 563b1bd146..d545f125dd 100644 --- a/tests/migrations/test_commands.py +++ b/tests/migrations/test_commands.py @@ -980,6 +980,19 @@ class MigrateTests(MigrationTestBase): # Unmigrate everything. call_command('migrate', 'migrations', 'zero', verbosity=0) + @override_settings(MIGRATION_MODULES={'migrations': 'migrations.test_migrations_squashed'}) + def test_migrate_backward_to_squashed_migration(self): + try: + call_command('migrate', 'migrations', '0001_squashed_0002', verbosity=0) + self.assertTableExists('migrations_author') + self.assertTableExists('migrations_book') + call_command('migrate', 'migrations', '0001_initial', verbosity=0) + self.assertTableExists('migrations_author') + self.assertTableNotExists('migrations_book') + finally: + # Unmigrate everything. + call_command('migrate', 'migrations', 'zero', verbosity=0) + @override_settings(MIGRATION_MODULES={'migrations': 'migrations.test_migrations'}) def test_migrate_inconsistent_history(self): """ diff --git a/tests/migrations/test_executor.py b/tests/migrations/test_executor.py index fc80c950fa..f8c321b0f3 100644 --- a/tests/migrations/test_executor.py +++ b/tests/migrations/test_executor.py @@ -104,6 +104,29 @@ class ExecutorTests(MigrationTestBase): self.assertTableNotExists("migrations_author") self.assertTableNotExists("migrations_book") + @override_settings( + MIGRATION_MODULES={'migrations': 'migrations.test_migrations_squashed'}, + ) + def test_migrate_backward_to_squashed_migration(self): + executor = MigrationExecutor(connection) + try: + self.assertTableNotExists('migrations_author') + self.assertTableNotExists('migrations_book') + executor.migrate([('migrations', '0001_squashed_0002')]) + self.assertTableExists('migrations_author') + self.assertTableExists('migrations_book') + executor.loader.build_graph() + # Migrate backward to a squashed migration. + executor.migrate([('migrations', '0001_initial')]) + self.assertTableExists('migrations_author') + self.assertTableNotExists('migrations_book') + finally: + # Unmigrate everything. + executor = MigrationExecutor(connection) + executor.migrate([('migrations', None)]) + self.assertTableNotExists('migrations_author') + self.assertTableNotExists('migrations_book') + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_non_atomic"}) def test_non_atomic_migration(self): """ @@ -733,6 +756,7 @@ class FakeLoader: def __init__(self, graph, applied): self.graph = graph self.applied_migrations = applied + self.replace_migrations = True class FakeMigration: