Loading django/core/management/commands/squashmigrations.py +44 −15 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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'] Loading @@ -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 = [ Loading @@ -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:")) Loading @@ -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( Loading @@ -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: Loading Loading @@ -132,9 +144,12 @@ class Command(BaseCommand): "dependencies": dependencies, "operations": new_operations, "replaces": replaces, "initial": True, }) 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) Loading @@ -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) ) docs/ref/django-admin.txt +9 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading docs/releases/1.9.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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 ^^^^^^ Loading docs/topics/migrations.txt +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading tests/migrations/test_commands.py +31 −0 Original line number Diff line number Diff line Loading @@ -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
django/core/management/commands/squashmigrations.py +44 −15 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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'] Loading @@ -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 = [ Loading @@ -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:")) Loading @@ -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( Loading @@ -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: Loading Loading @@ -132,9 +144,12 @@ class Command(BaseCommand): "dependencies": dependencies, "operations": new_operations, "replaces": replaces, "initial": True, }) 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) Loading @@ -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) )
docs/ref/django-admin.txt +9 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading
docs/releases/1.9.txt +3 −0 Original line number Diff line number Diff line Loading @@ -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 ^^^^^^ Loading
docs/topics/migrations.txt +1 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
tests/migrations/test_commands.py +31 −0 Original line number Diff line number Diff line Loading @@ -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)