1
0
mirror of https://github.com/django/django.git synced 2025-10-24 14:16:09 +00:00

Fixed #24529 -- Allowed double squashing of migrations.

Co-authored-by: Raphael Gaschignard <raphael@rtpg.co>
This commit is contained in:
Georgi Yanchev
2025-01-20 09:12:09 +01:00
committed by Sarah Boyce
parent 322785b08c
commit 64b1ac7292
14 changed files with 291 additions and 22 deletions

View File

@@ -2,6 +2,7 @@ import datetime
import importlib
import io
import os
import re
import shutil
import sys
from pathlib import Path
@@ -28,7 +29,9 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.backends.utils import truncate_name
from django.db.migrations.autodetector import MigrationAutodetector
from django.db.migrations.exceptions import InconsistentMigrationHistory
from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder
from django.db.migrations.writer import MigrationWriter
from django.test import TestCase, override_settings, skipUnlessDBFeature
from django.test.utils import captured_stdout, extend_sys_path, isolate_apps
from django.utils import timezone
@@ -2939,6 +2942,137 @@ class SquashMigrationsTests(MigrationTestBase):
" you can delete them.\n" % squashed_migration_file,
)
def test_squashmigrations_replacement_cycle(self):
out = io.StringIO()
with self.temporary_migration_module(
module="migrations.test_migrations_squashed_loop"
):
# Hits a squash replacement cycle check error, but the actual failure is
# dependent on the order in which the files are read on disk.
with self.assertRaisesRegex(
CommandError,
r"Cyclical squash replacement found, starting at"
r" \('migrations', '2_(squashed|auto)'\)",
):
call_command(
"migrate", "migrations", "--plan", interactive=False, stdout=out
)
def test_squashmigrations_squashes_already_squashed(self):
out = io.StringIO()
with self.temporary_migration_module(
module="migrations.test_migrations_squashed_complex"
):
call_command(
"squashmigrations",
"migrations",
"3_squashed_5",
"--squashed-name",
"double_squash",
stdout=out,
interactive=False,
)
loader = MigrationLoader(connection)
migration = loader.disk_migrations[("migrations", "0001_double_squash")]
# Confirm the replaces mechanism holds the squashed migration
# (and not what it squashes, as the squash operations are what
# end up being used).
self.assertEqual(
migration.replaces,
[
("migrations", "1_auto"),
("migrations", "2_auto"),
("migrations", "3_squashed_5"),
],
)
out = io.StringIO()
call_command(
"migrate", "migrations", "--plan", interactive=False, stdout=out
)
migration_plan = re.findall("migrations.(.+)\n", out.getvalue())
self.assertEqual(migration_plan, ["0001_double_squash", "6_auto", "7_auto"])
def test_squash_partially_applied(self):
"""
Replacement migrations are partially applied. Then we squash again and
verify that only unapplied migrations will be applied by "migrate".
"""
out = io.StringIO()
with self.temporary_migration_module(
module="migrations.test_migrations_squashed_partially_applied"
):
# Apply first 2 migrations.
call_command("migrate", "migrations", "0002", interactive=False, stdout=out)
# Squash the 2 migrations, that we just applied + 1 more.
call_command(
"squashmigrations",
"migrations",
"0001",
"0003",
"--squashed-name",
"squashed_0001_0003",
stdout=out,
interactive=False,
)
# Update the 4th migration to depend on the squash(replacement) migration.
loader = MigrationLoader(connection)
migration = loader.disk_migrations[
("migrations", "0004_remove_mymodel1_field_1_mymodel1_field_3_and_more")
]
migration.dependencies = [("migrations", "0001_squashed_0001_0003")]
writer = MigrationWriter(migration)
with open(writer.path, "w", encoding="utf-8") as fh:
fh.write(writer.as_string())
# Squash the squash(replacement) migration with the 4th migration.
call_command(
"squashmigrations",
"migrations",
"0001_squashed_0001_0003",
"0004",
"--squashed-name",
"squashed_0001_0004",
stdout=out,
interactive=False,
)
loader = MigrationLoader(connection)
migration = loader.disk_migrations[
("migrations", "0001_squashed_0001_0004")
]
self.assertEqual(
migration.replaces,
[
("migrations", "0001_squashed_0001_0003"),
(
"migrations",
"0004_remove_mymodel1_field_1_mymodel1_field_3_and_more",
),
],
)
# Verify that only unapplied migrations will be applied.
out = io.StringIO()
call_command(
"migrate", "migrations", "--plan", interactive=False, stdout=out
)
migration_plan = re.findall("migrations.(.+)\n", out.getvalue())
self.assertEqual(
migration_plan,
[
"0003_alter_mymodel2_unique_together",
"0004_remove_mymodel1_field_1_mymodel1_field_3_and_more",
],
)
def test_squashmigrations_initial_attribute(self):
with self.temporary_migration_module(
module="migrations.test_migrations"