Loading django/core/management/commands/migrate.py +48 −14 Original line number Diff line number Diff line Loading @@ -4,17 +4,18 @@ import traceback from django.conf import settings from django.core.management import call_command from django.core.management.base import NoArgsCommand from django.core.management.base import BaseCommand, CommandError from django.core.management.color import color_style, no_style from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.db.migrations.executor import MigrationExecutor from django.db.migrations.loader import AmbiguityError from django.utils.datastructures import SortedDict from django.utils.importlib import import_module class Command(NoArgsCommand): option_list = NoArgsCommand.option_list + ( class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--noinput', action='store_false', dest='interactive', default=True, help='Tells Django to NOT prompt the user for input of any kind.'), make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True, Loading @@ -26,7 +27,7 @@ class Command(NoArgsCommand): help = "Updates database schema. Manages both apps with migrations and those without." def handle_noargs(self, **options): def handle(self, *args, **options): self.verbosity = int(options.get('verbosity')) self.interactive = options.get('interactive') Loading Loading @@ -60,22 +61,55 @@ class Command(NoArgsCommand): connection = connections[db] # Work out which apps have migrations and which do not if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_HEADING("Calculating migration plan:")) executor = MigrationExecutor(connection, self.migration_progress_callback) if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_LABEL(" Apps without migrations: ") + (", ".join(executor.loader.unmigrated_apps) or "(none)")) # Work out what targets they want, and then make a migration plan # TODO: Let users select targets # If they supplied command line arguments, work out what they mean. run_syncdb = False target_app_labels_only = True if len(args) > 2: raise CommandError("Too many command-line arguments (expecting 'appname' or 'appname migrationname')") elif len(args) == 2: app_label, migration_name = args if app_label not in executor.loader.migrated_apps: raise CommandError("App '%s' does not have migrations (you cannot selectively sync unmigrated apps)" % app_label) if migration_name == "zero": migration_name = None else: 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'. Is it in INSTALLED_APPS?" % (app_label, migration_name)) targets = [(app_label, migration.name)] target_app_labels_only = False elif len(args) == 1: app_label = args[0] if app_label not in executor.loader.migrated_apps: raise CommandError("App '%s' does not have migrations (you cannot selectively sync unmigrated apps)" % app_label) targets = [key for key in executor.loader.graph.leaf_nodes() if key[0] == app_label] else: targets = executor.loader.graph.leaf_nodes() run_syncdb = True plan = executor.migration_plan(targets) # Print some useful info if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_LABEL(" Apps with migrations: ") + (", ".join(executor.loader.migrated_apps) or "(none)")) self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:")) if run_syncdb: self.stdout.write(self.style.MIGRATE_LABEL(" Synchronize unmigrated apps: ") + (", ".join(executor.loader.unmigrated_apps) or "(none)")) if target_app_labels_only: self.stdout.write(self.style.MIGRATE_LABEL(" Apply all migrations: ") + (", ".join(set(a for a, n in targets)) or "(none)")) else: if targets[0][1] is None: self.stdout.write(self.style.MIGRATE_LABEL(" Unapply all migrations: ") + "%s" % (targets[0][0], )) else: self.stdout.write(self.style.MIGRATE_LABEL(" Target specific migration: ") + "%s, from %s" % (targets[0][1], targets[0][0])) # Run the syncdb phase. # If you ever manage to get rid of this, I owe you many, many drinks. if run_syncdb: self.stdout.write(self.style.MIGRATE_HEADING("Synchronizing apps without migrations:")) self.sync_apps(connection, executor.loader.unmigrated_apps) Loading django/db/migrations/executor.py +9 −1 Original line number Diff line number Diff line Loading @@ -22,9 +22,17 @@ class MigrationExecutor(object): plan = [] applied = self.recorder.applied_migrations() for target in targets: # If the target is (appname, None), that means unmigrate everything if target[1] is None: for root in self.loader.graph.root_nodes(): if root[0] == target[0]: for migration in self.loader.graph.backwards_plan(root): if migration in applied: plan.append((self.loader.graph.nodes[migration], True)) applied.remove(migration) # If the migration is already applied, do backwards mode, # otherwise do forwards mode. if target in applied: elif target in applied: for migration in self.loader.graph.backwards_plan(target)[:-1]: if migration in applied: plan.append((self.loader.graph.nodes[migration], True)) Loading django/db/migrations/loader.py +24 −0 Original line number Diff line number Diff line Loading @@ -79,6 +79,23 @@ class MigrationLoader(object): raise BadMigrationError("Migration %s in app %s has no Migration class" % (migration_name, app_label)) self.disk_migrations[app_label, migration_name] = migration_module.Migration(migration_name, app_label) 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 if self.disk_migrations is None: self.load_disk() # Do the search results = [] for l, n in self.disk_migrations: if l == app_label and n.startswith(name_prefix): results.append((l, n)) if len(results) > 1: raise AmbiguityError("There is more than one migration for '%s' with the prefix '%s'" % (app_label, name_prefix)) elif len(results) == 0: raise KeyError("There no migrations for '%s' with the prefix '%s'" % (app_label, name_prefix)) else: return self.disk_migrations[results[0]] @cached_property def graph(self): """ Loading Loading @@ -141,3 +158,10 @@ class BadMigrationError(Exception): Raised when there's a bad migration (unreadable/bad format/etc.) """ pass class AmbiguityError(Exception): """ Raised when more than one migration matches a name prefix """ pass tests/migrations/test_loader.py +14 −1 Original line number Diff line number Diff line from django.test import TestCase from django.test.utils import override_settings from django.db import connection from django.db.migrations.loader import MigrationLoader from django.db.migrations.loader import MigrationLoader, AmbiguityError from django.db.migrations.recorder import MigrationRecorder Loading Loading @@ -64,3 +64,16 @@ class LoaderTests(TestCase): [x for x, y in book_state.fields], ["id", "author"] ) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) def test_name_match(self): "Tests prefix name matching" migration_loader = MigrationLoader(connection) self.assertEqual( migration_loader.get_migration_by_prefix("migrations", "0001").name, "0001_initial", ) with self.assertRaises(AmbiguityError): migration_loader.get_migration_by_prefix("migrations", "0") with self.assertRaises(KeyError): migration_loader.get_migration_by_prefix("migrations", "blarg") Loading
django/core/management/commands/migrate.py +48 −14 Original line number Diff line number Diff line Loading @@ -4,17 +4,18 @@ import traceback from django.conf import settings from django.core.management import call_command from django.core.management.base import NoArgsCommand from django.core.management.base import BaseCommand, CommandError from django.core.management.color import color_style, no_style from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.db.migrations.executor import MigrationExecutor from django.db.migrations.loader import AmbiguityError from django.utils.datastructures import SortedDict from django.utils.importlib import import_module class Command(NoArgsCommand): option_list = NoArgsCommand.option_list + ( class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--noinput', action='store_false', dest='interactive', default=True, help='Tells Django to NOT prompt the user for input of any kind.'), make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True, Loading @@ -26,7 +27,7 @@ class Command(NoArgsCommand): help = "Updates database schema. Manages both apps with migrations and those without." def handle_noargs(self, **options): def handle(self, *args, **options): self.verbosity = int(options.get('verbosity')) self.interactive = options.get('interactive') Loading Loading @@ -60,22 +61,55 @@ class Command(NoArgsCommand): connection = connections[db] # Work out which apps have migrations and which do not if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_HEADING("Calculating migration plan:")) executor = MigrationExecutor(connection, self.migration_progress_callback) if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_LABEL(" Apps without migrations: ") + (", ".join(executor.loader.unmigrated_apps) or "(none)")) # Work out what targets they want, and then make a migration plan # TODO: Let users select targets # If they supplied command line arguments, work out what they mean. run_syncdb = False target_app_labels_only = True if len(args) > 2: raise CommandError("Too many command-line arguments (expecting 'appname' or 'appname migrationname')") elif len(args) == 2: app_label, migration_name = args if app_label not in executor.loader.migrated_apps: raise CommandError("App '%s' does not have migrations (you cannot selectively sync unmigrated apps)" % app_label) if migration_name == "zero": migration_name = None else: 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'. Is it in INSTALLED_APPS?" % (app_label, migration_name)) targets = [(app_label, migration.name)] target_app_labels_only = False elif len(args) == 1: app_label = args[0] if app_label not in executor.loader.migrated_apps: raise CommandError("App '%s' does not have migrations (you cannot selectively sync unmigrated apps)" % app_label) targets = [key for key in executor.loader.graph.leaf_nodes() if key[0] == app_label] else: targets = executor.loader.graph.leaf_nodes() run_syncdb = True plan = executor.migration_plan(targets) # Print some useful info if self.verbosity >= 1: self.stdout.write(self.style.MIGRATE_LABEL(" Apps with migrations: ") + (", ".join(executor.loader.migrated_apps) or "(none)")) self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:")) if run_syncdb: self.stdout.write(self.style.MIGRATE_LABEL(" Synchronize unmigrated apps: ") + (", ".join(executor.loader.unmigrated_apps) or "(none)")) if target_app_labels_only: self.stdout.write(self.style.MIGRATE_LABEL(" Apply all migrations: ") + (", ".join(set(a for a, n in targets)) or "(none)")) else: if targets[0][1] is None: self.stdout.write(self.style.MIGRATE_LABEL(" Unapply all migrations: ") + "%s" % (targets[0][0], )) else: self.stdout.write(self.style.MIGRATE_LABEL(" Target specific migration: ") + "%s, from %s" % (targets[0][1], targets[0][0])) # Run the syncdb phase. # If you ever manage to get rid of this, I owe you many, many drinks. if run_syncdb: self.stdout.write(self.style.MIGRATE_HEADING("Synchronizing apps without migrations:")) self.sync_apps(connection, executor.loader.unmigrated_apps) Loading
django/db/migrations/executor.py +9 −1 Original line number Diff line number Diff line Loading @@ -22,9 +22,17 @@ class MigrationExecutor(object): plan = [] applied = self.recorder.applied_migrations() for target in targets: # If the target is (appname, None), that means unmigrate everything if target[1] is None: for root in self.loader.graph.root_nodes(): if root[0] == target[0]: for migration in self.loader.graph.backwards_plan(root): if migration in applied: plan.append((self.loader.graph.nodes[migration], True)) applied.remove(migration) # If the migration is already applied, do backwards mode, # otherwise do forwards mode. if target in applied: elif target in applied: for migration in self.loader.graph.backwards_plan(target)[:-1]: if migration in applied: plan.append((self.loader.graph.nodes[migration], True)) Loading
django/db/migrations/loader.py +24 −0 Original line number Diff line number Diff line Loading @@ -79,6 +79,23 @@ class MigrationLoader(object): raise BadMigrationError("Migration %s in app %s has no Migration class" % (migration_name, app_label)) self.disk_migrations[app_label, migration_name] = migration_module.Migration(migration_name, app_label) 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 if self.disk_migrations is None: self.load_disk() # Do the search results = [] for l, n in self.disk_migrations: if l == app_label and n.startswith(name_prefix): results.append((l, n)) if len(results) > 1: raise AmbiguityError("There is more than one migration for '%s' with the prefix '%s'" % (app_label, name_prefix)) elif len(results) == 0: raise KeyError("There no migrations for '%s' with the prefix '%s'" % (app_label, name_prefix)) else: return self.disk_migrations[results[0]] @cached_property def graph(self): """ Loading Loading @@ -141,3 +158,10 @@ class BadMigrationError(Exception): Raised when there's a bad migration (unreadable/bad format/etc.) """ pass class AmbiguityError(Exception): """ Raised when more than one migration matches a name prefix """ pass
tests/migrations/test_loader.py +14 −1 Original line number Diff line number Diff line from django.test import TestCase from django.test.utils import override_settings from django.db import connection from django.db.migrations.loader import MigrationLoader from django.db.migrations.loader import MigrationLoader, AmbiguityError from django.db.migrations.recorder import MigrationRecorder Loading Loading @@ -64,3 +64,16 @@ class LoaderTests(TestCase): [x for x, y in book_state.fields], ["id", "author"] ) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"}) def test_name_match(self): "Tests prefix name matching" migration_loader = MigrationLoader(connection) self.assertEqual( migration_loader.get_migration_by_prefix("migrations", "0001").name, "0001_initial", ) with self.assertRaises(AmbiguityError): migration_loader.get_migration_by_prefix("migrations", "0") with self.assertRaises(KeyError): migration_loader.get_migration_by_prefix("migrations", "blarg")