Fixed #25390 -- Allowed specifying a start migration in squashmigrations
Thanks Tim Graham for the review.
This commit is contained in:
parent
5aa55038ca
commit
43f2eb7ef3
|
@ -15,6 +15,8 @@ class Command(BaseCommand):
|
|||
def add_arguments(self, parser):
|
||||
parser.add_argument('app_label',
|
||||
help='App label of the application to squash migrations for.')
|
||||
parser.add_argument('start_migration_name', default=None, nargs='?',
|
||||
help='Migrations will be squashed starting from and including this migration.')
|
||||
parser.add_argument('migration_name',
|
||||
help='Migrations will be squashed until and including this migration.')
|
||||
parser.add_argument('--no-optimize', action='store_true', dest='no_optimize', default=False,
|
||||
|
@ -28,6 +30,7 @@ class Command(BaseCommand):
|
|||
self.verbosity = options.get('verbosity')
|
||||
self.interactive = options.get('interactive')
|
||||
app_label = options['app_label']
|
||||
start_migration_name = options['start_migration_name']
|
||||
migration_name = options['migration_name']
|
||||
no_optimize = options['no_optimize']
|
||||
|
||||
|
@ -38,18 +41,8 @@ class Command(BaseCommand):
|
|||
"App '%s' does not have migrations (so squashmigrations on "
|
||||
"it makes no sense)" % app_label
|
||||
)
|
||||
try:
|
||||
migration = loader.get_migration_by_prefix(app_label, migration_name)
|
||||
except AmbiguityError:
|
||||
raise CommandError(
|
||||
"More than one migration matches '%s' in app '%s'. Please be "
|
||||
"more specific." % (migration_name, app_label)
|
||||
)
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
"Cannot find a migration matching '%s' from app '%s'." %
|
||||
(migration_name, app_label)
|
||||
)
|
||||
|
||||
migration = self.find_migration(loader, app_label, migration_name)
|
||||
|
||||
# Work out the list of predecessor migrations
|
||||
migrations_to_squash = [
|
||||
|
@ -58,6 +51,21 @@ class Command(BaseCommand):
|
|||
if al == migration.app_label
|
||||
]
|
||||
|
||||
if start_migration_name:
|
||||
start_migration = self.find_migration(loader, app_label, start_migration_name)
|
||||
start = loader.get_migration(start_migration.app_label, start_migration.name)
|
||||
try:
|
||||
start_index = migrations_to_squash.index(start)
|
||||
migrations_to_squash = migrations_to_squash[start_index:]
|
||||
except ValueError:
|
||||
raise CommandError(
|
||||
"The migration '%s' cannot be found. Maybe it comes after "
|
||||
"the migration '%s'?\n"
|
||||
"Have a look at:\n"
|
||||
" python manage.py showmigrations %s\n"
|
||||
"to debug this issue." % (start_migration, migration, app_label)
|
||||
)
|
||||
|
||||
# Tell them what we're doing and optionally ask if we should proceed
|
||||
if self.verbosity > 0 or self.interactive:
|
||||
self.stdout.write(self.style.MIGRATE_HEADING("Will squash the following migrations:"))
|
||||
|
@ -81,6 +89,9 @@ class Command(BaseCommand):
|
|||
# double-squashing
|
||||
operations = []
|
||||
dependencies = set()
|
||||
# We need to take all dependencies from the first migration in the list
|
||||
# as it may be 0002 depending on 0001
|
||||
first_migration = True
|
||||
for smigration in migrations_to_squash:
|
||||
if smigration.replaces:
|
||||
raise CommandError(
|
||||
|
@ -95,8 +106,9 @@ class Command(BaseCommand):
|
|||
dependencies.add(("__setting__", "AUTH_USER_MODEL"))
|
||||
else:
|
||||
dependencies.add(dependency)
|
||||
elif dependency[0] != smigration.app_label:
|
||||
elif dependency[0] != smigration.app_label or first_migration:
|
||||
dependencies.add(dependency)
|
||||
first_migration = False
|
||||
|
||||
if no_optimize:
|
||||
if self.verbosity > 0:
|
||||
|
@ -132,9 +144,12 @@ class Command(BaseCommand):
|
|||
"dependencies": dependencies,
|
||||
"operations": new_operations,
|
||||
"replaces": replaces,
|
||||
"initial": True,
|
||||
})
|
||||
new_migration = subclass("0001_squashed_%s" % migration.name, app_label)
|
||||
if start_migration_name:
|
||||
new_migration = subclass("%s_squashed_%s" % (start_migration.name, migration.name), app_label)
|
||||
else:
|
||||
new_migration = subclass("0001_squashed_%s" % migration.name, app_label)
|
||||
new_migration.initial = True
|
||||
|
||||
# Write out the new migration file
|
||||
writer = MigrationWriter(new_migration)
|
||||
|
@ -152,3 +167,17 @@ class Command(BaseCommand):
|
|||
self.stdout.write(" Your migrations contained functions that must be manually copied over,")
|
||||
self.stdout.write(" as we could not safely copy their implementation.")
|
||||
self.stdout.write(" See the comment at the top of the squashed migration for details.")
|
||||
|
||||
def find_migration(self, loader, app_label, name):
|
||||
try:
|
||||
return loader.get_migration_by_prefix(app_label, name)
|
||||
except AmbiguityError:
|
||||
raise CommandError(
|
||||
"More than one migration matches '%s' in app '%s'. Please be "
|
||||
"more specific." % (name, app_label)
|
||||
)
|
||||
except KeyError:
|
||||
raise CommandError(
|
||||
"Cannot find a migration matching '%s' from app '%s'." %
|
||||
(name, app_label)
|
||||
)
|
||||
|
|
|
@ -1049,8 +1049,8 @@ of sync with its automatically incremented field data.
|
|||
The :djadminopt:`--database` option can be used to specify the database for
|
||||
which to print the SQL.
|
||||
|
||||
squashmigrations <app_label> <migration_name>
|
||||
---------------------------------------------
|
||||
squashmigrations <app_label> [<start_migration_name>] <migration_name>
|
||||
----------------------------------------------------------------------
|
||||
|
||||
.. django-admin:: squashmigrations
|
||||
|
||||
|
@ -1059,6 +1059,13 @@ down into fewer migrations, if possible. The resulting squashed migrations
|
|||
can live alongside the unsquashed ones safely. For more information,
|
||||
please read :ref:`migration-squashing`.
|
||||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
When ``start_migration_name`` is given, Django will only include migrations
|
||||
starting from and including this migration. This helps to mitigate the
|
||||
squashing limitation of :class:`~django.db.migrations.operations.RunPython` and
|
||||
:class:`django.db.migrations.operations.RunSQL` migration operations.
|
||||
|
||||
.. django-admin-option:: --no-optimize
|
||||
|
||||
By default, Django will try to optimize the operations in your migrations
|
||||
|
|
|
@ -473,6 +473,9 @@ Migrations
|
|||
applied and others are being unapplied. This was never officially supported
|
||||
and never had a public API that supports this behavior.
|
||||
|
||||
* The :djadmin:`squashmigrations` command now supports specifying the starting
|
||||
migration from which migrations will be squashed.
|
||||
|
||||
Models
|
||||
^^^^^^
|
||||
|
||||
|
|
|
@ -572,7 +572,7 @@ possible depends on how closely intertwined your models are and if you have
|
|||
any :class:`~django.db.migrations.operations.RunSQL`
|
||||
or :class:`~django.db.migrations.operations.RunPython` operations (which can't
|
||||
be optimized through) - Django will then write it back out into a new set of
|
||||
initial migration files.
|
||||
migration files.
|
||||
|
||||
These files are marked to say they replace the previously-squashed migrations,
|
||||
so they can coexist with the old migration files, and Django will intelligently
|
||||
|
|
|
@ -1070,3 +1070,34 @@ class SquashMigrationsTests(MigrationTestBase):
|
|||
call_command("squashmigrations", "migrations", "0002",
|
||||
interactive=False, verbosity=1, no_optimize=True, stdout=out)
|
||||
self.assertIn("Skipping optimization", force_text(out.getvalue()))
|
||||
|
||||
def test_squashmigrations_valid_start(self):
|
||||
"""
|
||||
squashmigrations accepts a starting migration.
|
||||
"""
|
||||
out = six.StringIO()
|
||||
with self.temporary_migration_module(module="migrations.test_migrations_no_changes") as migration_dir:
|
||||
call_command("squashmigrations", "migrations", "0002", "0003",
|
||||
interactive=False, verbosity=1, stdout=out)
|
||||
|
||||
squashed_migration_file = os.path.join(migration_dir, "0002_second_squashed_0003_third.py")
|
||||
with codecs.open(squashed_migration_file, "r", encoding="utf-8") as fp:
|
||||
content = fp.read()
|
||||
self.assertIn(" ('migrations', '0001_initial')", content)
|
||||
self.assertNotIn("initial = True", content)
|
||||
out = force_text(out.getvalue())
|
||||
self.assertNotIn(" - 0001_initial", out)
|
||||
self.assertIn(" - 0002_second", out)
|
||||
self.assertIn(" - 0003_third", out)
|
||||
|
||||
def test_squashmigrations_invalid_start(self):
|
||||
"""
|
||||
squashmigrations doesn't accept a starting migration after the ending migration.
|
||||
"""
|
||||
with self.temporary_migration_module(module="migrations.test_migrations_no_changes"):
|
||||
msg = (
|
||||
"The migration 'migrations.0003_third' cannot be found. Maybe "
|
||||
"it comes after the migration 'migrations.0002_second'"
|
||||
)
|
||||
with self.assertRaisesMessage(CommandError, msg):
|
||||
call_command("squashmigrations", "migrations", "0003", "0002", interactive=False, verbosity=0)
|
||||
|
|
Loading…
Reference in New Issue