1
0
mirror of https://github.com/django/django.git synced 2025-04-07 23:16:46 +00:00

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

This commit is contained in:
Georgi Yanchev 2025-02-11 15:14:53 -05:00 committed by Sarah Boyce
parent 54a902c6e8
commit 0ee842bb45
8 changed files with 147 additions and 12 deletions

View File

@ -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)

View File

@ -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"

View File

@ -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()),
],
),
]

View File

@ -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),
),
]

View File

@ -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)),
],
),
]

View File

@ -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),
),
]

View File

@ -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)),
],
),
]