diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py index 7dc0350927..af2207c9ab 100644 --- a/django/db/migrations/executor.py +++ b/django/db/migrations/executor.py @@ -110,6 +110,7 @@ class MigrationExecutor(object): self.apply_migration(states[migration], migration, fake=fake, fake_initial=fake_initial) else: self.unapply_migration(states[migration], migration, fake=fake) + self.check_replacements() def collect_sql(self, plan): """ @@ -176,6 +177,16 @@ class MigrationExecutor(object): self.progress_callback("unapply_success", migration, fake) return state + def check_replacements(self): + """ + Mark replacement migrations applied if their replaced set all are. + """ + applied = self.recorder.applied_migrations() + for key, migration in self.loader.replacements.items(): + all_applied = all(m in applied for m in migration.replaces) + if all_applied and key not in applied: + self.recorder.record_applied(*key) + def detect_soft_applied(self, project_state, migration): """ Tests whether a migration has been implicitly applied - that the diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py index b2ba335ade..a8f4be47b9 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -251,6 +251,8 @@ class MigrationLoader(object): # Mark the replacement as applied if all its replaced ones are if all(applied_statuses): self.applied_migrations.add(key) + # Store the replacement migrations for later checks + self.replacements = replacing # Finally, make a graph and load everything into it self.graph = MigrationGraph() for key, migration in normal.items(): diff --git a/docs/releases/1.8.3.txt b/docs/releases/1.8.3.txt index 2940623e55..4f98027f7e 100644 --- a/docs/releases/1.8.3.txt +++ b/docs/releases/1.8.3.txt @@ -47,3 +47,6 @@ Bugfixes * Fixed a crash when loading squashed migrations from two apps with a dependency between them, where the dependent app's replaced migrations are partially applied (:ticket:`24895`). + +* Fixed recording of applied status for squashed (replacement) migrations + (:ticket:`24628`). diff --git a/tests/migrations/test_executor.py b/tests/migrations/test_executor.py index b05eb04514..9946dd4503 100644 --- a/tests/migrations/test_executor.py +++ b/tests/migrations/test_executor.py @@ -2,6 +2,7 @@ from django.apps.registry import apps as global_apps from django.db import connection from django.db.migrations.executor import MigrationExecutor from django.db.migrations.graph import MigrationGraph +from django.db.migrations.recorder import MigrationRecorder from django.db.utils import DatabaseError from django.test import TestCase, modify_settings, override_settings @@ -411,6 +412,31 @@ class ExecutorTests(MigrationTestBase): self.assertTableNotExists("author_app_author") self.assertTableNotExists("book_app_book") + @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_squashed"}) + def test_apply_all_replaced_marks_replacement_as_applied(self): + """ + Applying all replaced migrations marks the replacement as applied. + + Ticket #24628. + """ + recorder = MigrationRecorder(connection) + # Place the database in a state where the replaced migrations are + # partially applied: 0001 is applied, 0002 is not. + recorder.record_applied("migrations", "0001_initial") + executor = MigrationExecutor(connection) + # Use fake because we don't actually have the first migration + # applied, so the second will fail. And there's no need to actually + # create/modify tables here, we're just testing the + # MigrationRecord, which works the same with or without fake. + executor.migrate([("migrations", "0002_second")], fake=True) + + # Because we've now applied 0001 and 0002 both, their squashed + # replacement should be marked as applied. + self.assertIn( + ("migrations", "0001_squashed_0002"), + recorder.applied_migrations(), + ) + class FakeLoader(object): def __init__(self, graph, applied):