summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarkus Holtermann <info@markusholtermann.eu>2015-09-12 17:18:24 +1000
committerMarkus Holtermann <info@markusholtermann.eu>2015-09-19 14:55:34 +1000
commit43f2eb7ef327126271a8c70d6fde5713947150a5 (patch)
tree7ca77f515be7253a42cc952a2f9f4b57001988ca
parent5aa55038ca9ac44b440b56d1fc4e79c876e51393 (diff)
Fixed #25390 -- Allowed specifying a start migration in squashmigrations
Thanks Tim Graham for the review.
-rw-r--r--django/core/management/commands/squashmigrations.py59
-rw-r--r--docs/ref/django-admin.txt11
-rw-r--r--docs/releases/1.9.txt3
-rw-r--r--docs/topics/migrations.txt2
-rw-r--r--tests/migrations/test_commands.py31
5 files changed, 88 insertions, 18 deletions
diff --git a/django/core/management/commands/squashmigrations.py b/django/core/management/commands/squashmigrations.py
index 39225c2d75..9ad5f60d35 100644
--- a/django/core/management/commands/squashmigrations.py
+++ b/django/core/management/commands/squashmigrations.py
@@ -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)
+ )
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 02ab4f0614..18e8ec1ca0 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -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
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 58522e75fa..6bacdb1ae2 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -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
^^^^^^
diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt
index 4fe3b5d329..57b6c3216a 100644
--- a/docs/topics/migrations.txt
+++ b/docs/topics/migrations.txt
@@ -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
diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index 129a40dd2f..a0b7939a24 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -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)