Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First pass on squashmigrations command; files are right, execution not.

  • Loading branch information...
commit 763ac8b642bc21b5a05c0d540c962804b36454ec 1 parent 42f8666
@andrewgodwin andrewgodwin authored
View
11 django/core/management/commands/migrate.py
@@ -76,7 +76,7 @@ def handle(self, *args, **options):
except AmbiguityError:
raise CommandError("More than one migration matches '%s' in app '%s'. Please be more specific." % (app_label, migration_name))
except KeyError:
- raise CommandError("Cannot find a migration matching '%s' from app '%s'. Is it in INSTALLED_APPS?" % (app_label, migration_name))
+ raise CommandError("Cannot find a migration matching '%s' from app '%s'." % (app_label, migration_name))
targets = [(app_label, migration.name)]
target_app_labels_only = False
elif len(args) == 1:
@@ -279,10 +279,15 @@ def show_migration_list(self, connection, apps=None):
for node in graph.leaf_nodes(app):
for plan_node in graph.forwards_plan(node):
if plan_node not in shown and plan_node[0] == app:
+ # Give it a nice title if it's a squashed one
+ title = plan_node[1]
+ if graph.nodes[plan_node].replaces:
+ title += " (%s squashed migrations)" % len(graph.nodes[plan_node].replaces)
+ # Mark it as applied/unapplied
if plan_node in loader.applied_migrations:
- self.stdout.write(" [X] %s" % plan_node[1])
+ self.stdout.write(" [X] %s" % title)
else:
- self.stdout.write(" [ ] %s" % plan_node[1])
+ self.stdout.write(" [ ] %s" % title)
shown.add(plan_node)
# If we didn't print anything, then a small message
if not shown:
View
108 django/core/management/commands/squashmigrations.py
@@ -0,0 +1,108 @@
+import sys
+import os
+from optparse import make_option
+
+from django.core.management.base import BaseCommand, CommandError
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import six
+from django.db import connections, DEFAULT_DB_ALIAS, migrations
+from django.db.migrations.loader import MigrationLoader, AmbiguityError
+from django.db.migrations.autodetector import MigrationAutodetector, InteractiveMigrationQuestioner
+from django.db.migrations.executor import MigrationExecutor
+from django.db.migrations.writer import MigrationWriter
+from django.db.models.loading import cache
+from django.db.migrations.optimizer import MigrationOptimizer
+
+
+class Command(BaseCommand):
+ option_list = BaseCommand.option_list + (
+ make_option('--no-optimize', action='store_true', dest='no_optimize', default=False,
+ help='Do not try to optimize the squashed operations.'),
+ make_option('--noinput', action='store_false', dest='interactive', default=True,
+ help='Tells Django to NOT prompt the user for input of any kind.'),
+ )
+
+ help = "Squashes an existing set of migrations (from first until specified) into a single new one."
+ usage_str = "Usage: ./manage.py squashmigrations app migration_name"
+
+ def handle(self, app_label=None, migration_name=None, **options):
+
+ self.verbosity = int(options.get('verbosity'))
+ self.interactive = options.get('interactive')
+
+ if app_label is None or migration_name is None:
+ self.stderr.write(self.usage_str)
+ sys.exit(1)
+
+ # Load the current graph state, check the app and migration they asked for exists
+ executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
+ if app_label not in executor.loader.migrated_apps:
+ raise CommandError("App '%s' does not have migrations (so squashmigrations on it makes no sense)" % app_label)
+ try:
+ migration = executor.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." % (app_label, migration_name))
+ except KeyError:
+ raise CommandError("Cannot find a migration matching '%s' from app '%s'." % (app_label, migration_name))
+
+ # Work out the list of predecessor migrations
+ migrations_to_squash = [
+ executor.loader.get_migration(al, mn)
+ for al, mn in executor.loader.graph.forwards_plan((migration.app_label, migration.name))
+ if al == 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:"))
+ for migration in migrations_to_squash:
+ self.stdout.write(" - %s" % migration.name)
+
+ if self.interactive:
+ answer = None
+ while not answer or answer not in "yn":
+ answer = six.moves.input("Do you wish to proceed? [yN] ")
+ if not answer:
+ answer = "n"
+ break
+ else:
+ answer = answer[0].lower()
+ if answer != "y":
+ return
+
+ # Load the operations from all those migrations and concat together
+ operations = []
+ for smigration in migrations_to_squash:
+ operations.extend(smigration.operations)
+
+ if self.verbosity > 0:
+ self.stdout.write(self.style.MIGRATE_HEADING("Optimizing..."))
+
+ optimizer = MigrationOptimizer()
+ new_operations = optimizer.optimize(operations, migration.app_label)
+
+ if self.verbosity > 0:
+ if len(new_operations) == len(operations):
+ self.stdout.write(" No optimizations possible.")
+ else:
+ self.stdout.write(" Optimized from %s operations to %s operations." % (len(operations), len(new_operations)))
+
+ # Make a new migration with those operations
+ subclass = type("Migration", (migrations.Migration, ), {
+ "dependencies": [],
+ "operations": new_operations,
+ "replaces": [(m.app_label, m.name) for m in migrations_to_squash],
+ })
+ new_migration = subclass("0001_squashed_%s" % migration.name, app_label)
+
+ # Write out the new migration file
+ writer = MigrationWriter(new_migration)
+ with open(writer.path, "wb") as fh:
+ fh.write(writer.as_string())
+
+ if self.verbosity > 0:
+ self.stdout.write(self.style.MIGRATE_HEADING("Created new squashed migration %s" % writer.path))
+ self.stdout.write(" You should commit this migration but leave the old ones in place;")
+ self.stdout.write(" the new migration will be used for new installs. Once you are sure")
+ self.stdout.write(" all instances of the codebase have applied the migrations you squashed,")
+ self.stdout.write(" you can delete them.")
View
6 django/db/migrations/loader.py
@@ -101,6 +101,10 @@ def load_disk(self):
if south_style_migrations:
self.unmigrated_apps.add(app_label)
+ def get_migration(self, app_label, name_prefix):
+ "Gets the migration exactly named, or raises KeyError"
+ return self.graph.nodes[app_label, name_prefix]
+
def get_migration_by_prefix(self, app_label, name_prefix):
"Returns the migration(s) which match the given app label and name _prefix_"
# Make sure we have the disk data
@@ -160,6 +164,8 @@ def graph(self):
# and remove, repointing dependencies if needs be.
for replaced in migration.replaces:
if replaced in normal:
+ # We don't care if the replaced migration doesn't exist;
+ # the usage pattern here is to delete things after a while.
del normal[replaced]
for child_key in reverse_dependencies.get(replaced, set()):
normal[child_key].dependencies.remove(replaced)
View
6 django/db/migrations/writer.py
@@ -26,6 +26,7 @@ def as_string(self):
"""
items = {
"dependencies": repr(self.migration.dependencies),
+ "replaces_str": "",
}
imports = set()
# Deconstruct operations
@@ -49,6 +50,9 @@ def as_string(self):
items["imports"] = ""
else:
items["imports"] = "\n".join(imports) + "\n"
+ # If there's a replaces, make a string for it
+ if self.migration.replaces:
+ items['replaces_str'] = "\n replaces = %s\n" % repr(self.migration.replaces)
return (MIGRATION_TEMPLATE % items).encode("utf8")
@property
@@ -186,7 +190,7 @@ def serialize(cls, value):
%(imports)s
class Migration(migrations.Migration):
-
+ %(replaces_str)s
dependencies = %(dependencies)s
operations = %(operations)s
Please sign in to comment.
Something went wrong with that request. Please try again.