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):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument('app_label',
|
parser.add_argument('app_label',
|
||||||
help='App label of the application to squash migrations for.')
|
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',
|
parser.add_argument('migration_name',
|
||||||
help='Migrations will be squashed until and including this migration.')
|
help='Migrations will be squashed until and including this migration.')
|
||||||
parser.add_argument('--no-optimize', action='store_true', dest='no_optimize', default=False,
|
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.verbosity = options.get('verbosity')
|
||||||
self.interactive = options.get('interactive')
|
self.interactive = options.get('interactive')
|
||||||
app_label = options['app_label']
|
app_label = options['app_label']
|
||||||
|
start_migration_name = options['start_migration_name']
|
||||||
migration_name = options['migration_name']
|
migration_name = options['migration_name']
|
||||||
no_optimize = options['no_optimize']
|
no_optimize = options['no_optimize']
|
||||||
|
|
||||||
|
@ -38,18 +41,8 @@ class Command(BaseCommand):
|
||||||
"App '%s' does not have migrations (so squashmigrations on "
|
"App '%s' does not have migrations (so squashmigrations on "
|
||||||
"it makes no sense)" % app_label
|
"it makes no sense)" % app_label
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
migration = loader.get_migration_by_prefix(app_label, migration_name)
|
migration = self.find_migration(loader, 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Work out the list of predecessor migrations
|
# Work out the list of predecessor migrations
|
||||||
migrations_to_squash = [
|
migrations_to_squash = [
|
||||||
|
@ -58,6 +51,21 @@ class Command(BaseCommand):
|
||||||
if al == migration.app_label
|
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
|
# Tell them what we're doing and optionally ask if we should proceed
|
||||||
if self.verbosity > 0 or self.interactive:
|
if self.verbosity > 0 or self.interactive:
|
||||||
self.stdout.write(self.style.MIGRATE_HEADING("Will squash the following migrations:"))
|
self.stdout.write(self.style.MIGRATE_HEADING("Will squash the following migrations:"))
|
||||||
|
@ -81,6 +89,9 @@ class Command(BaseCommand):
|
||||||
# double-squashing
|
# double-squashing
|
||||||
operations = []
|
operations = []
|
||||||
dependencies = set()
|
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:
|
for smigration in migrations_to_squash:
|
||||||
if smigration.replaces:
|
if smigration.replaces:
|
||||||
raise CommandError(
|
raise CommandError(
|
||||||
|
@ -95,8 +106,9 @@ class Command(BaseCommand):
|
||||||
dependencies.add(("__setting__", "AUTH_USER_MODEL"))
|
dependencies.add(("__setting__", "AUTH_USER_MODEL"))
|
||||||
else:
|
else:
|
||||||
dependencies.add(dependency)
|
dependencies.add(dependency)
|
||||||
elif dependency[0] != smigration.app_label:
|
elif dependency[0] != smigration.app_label or first_migration:
|
||||||
dependencies.add(dependency)
|
dependencies.add(dependency)
|
||||||
|
first_migration = False
|
||||||
|
|
||||||
if no_optimize:
|
if no_optimize:
|
||||||
if self.verbosity > 0:
|
if self.verbosity > 0:
|
||||||
|
@ -132,9 +144,12 @@ class Command(BaseCommand):
|
||||||
"dependencies": dependencies,
|
"dependencies": dependencies,
|
||||||
"operations": new_operations,
|
"operations": new_operations,
|
||||||
"replaces": replaces,
|
"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
|
# Write out the new migration file
|
||||||
writer = MigrationWriter(new_migration)
|
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(" Your migrations contained functions that must be manually copied over,")
|
||||||
self.stdout.write(" as we could not safely copy their implementation.")
|
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.")
|
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
|
The :djadminopt:`--database` option can be used to specify the database for
|
||||||
which to print the SQL.
|
which to print the SQL.
|
||||||
|
|
||||||
squashmigrations <app_label> <migration_name>
|
squashmigrations <app_label> [<start_migration_name>] <migration_name>
|
||||||
---------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
.. django-admin:: squashmigrations
|
.. 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,
|
can live alongside the unsquashed ones safely. For more information,
|
||||||
please read :ref:`migration-squashing`.
|
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
|
.. django-admin-option:: --no-optimize
|
||||||
|
|
||||||
By default, Django will try to optimize the operations in your migrations
|
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
|
applied and others are being unapplied. This was never officially supported
|
||||||
and never had a public API that supports this behavior.
|
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
|
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`
|
any :class:`~django.db.migrations.operations.RunSQL`
|
||||||
or :class:`~django.db.migrations.operations.RunPython` operations (which can't
|
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
|
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,
|
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
|
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",
|
call_command("squashmigrations", "migrations", "0002",
|
||||||
interactive=False, verbosity=1, no_optimize=True, stdout=out)
|
interactive=False, verbosity=1, no_optimize=True, stdout=out)
|
||||||
self.assertIn("Skipping optimization", force_text(out.getvalue()))
|
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