Make migrate command recognise prefixes and 'zero'.
This commit is contained in:
parent
52eb19b545
commit
162f7b938f
|
@ -4,17 +4,18 @@ import traceback
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management import call_command
|
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.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.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 import connections, router, transaction, models, DEFAULT_DB_ALIAS
|
||||||
from django.db.migrations.executor import MigrationExecutor
|
from django.db.migrations.executor import MigrationExecutor
|
||||||
|
from django.db.migrations.loader import AmbiguityError
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from django.utils.importlib import import_module
|
from django.utils.importlib import import_module
|
||||||
|
|
||||||
|
|
||||||
class Command(NoArgsCommand):
|
class Command(BaseCommand):
|
||||||
option_list = NoArgsCommand.option_list + (
|
option_list = BaseCommand.option_list + (
|
||||||
make_option('--noinput', action='store_false', dest='interactive', default=True,
|
make_option('--noinput', action='store_false', dest='interactive', default=True,
|
||||||
help='Tells Django to NOT prompt the user for input of any kind.'),
|
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,
|
make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True,
|
||||||
|
@ -26,7 +27,7 @@ class Command(NoArgsCommand):
|
||||||
|
|
||||||
help = "Updates database schema. Manages both apps with migrations and those without."
|
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.verbosity = int(options.get('verbosity'))
|
||||||
self.interactive = options.get('interactive')
|
self.interactive = options.get('interactive')
|
||||||
|
@ -60,22 +61,55 @@ class Command(NoArgsCommand):
|
||||||
connection = connections[db]
|
connection = connections[db]
|
||||||
|
|
||||||
# Work out which apps have migrations and which do not
|
# 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)
|
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
|
# If they supplied command line arguments, work out what they mean.
|
||||||
# TODO: Let users select targets
|
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()
|
targets = executor.loader.graph.leaf_nodes()
|
||||||
|
run_syncdb = True
|
||||||
|
|
||||||
plan = executor.migration_plan(targets)
|
plan = executor.migration_plan(targets)
|
||||||
|
|
||||||
|
# Print some useful info
|
||||||
if self.verbosity >= 1:
|
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.
|
# Run the syncdb phase.
|
||||||
# If you ever manage to get rid of this, I owe you many, many drinks.
|
# 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.stdout.write(self.style.MIGRATE_HEADING("Synchronizing apps without migrations:"))
|
||||||
self.sync_apps(connection, executor.loader.unmigrated_apps)
|
self.sync_apps(connection, executor.loader.unmigrated_apps)
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,17 @@ class MigrationExecutor(object):
|
||||||
plan = []
|
plan = []
|
||||||
applied = self.recorder.applied_migrations()
|
applied = self.recorder.applied_migrations()
|
||||||
for target in targets:
|
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,
|
# If the migration is already applied, do backwards mode,
|
||||||
# otherwise do forwards mode.
|
# otherwise do forwards mode.
|
||||||
if target in applied:
|
elif target in applied:
|
||||||
for migration in self.loader.graph.backwards_plan(target)[:-1]:
|
for migration in self.loader.graph.backwards_plan(target)[:-1]:
|
||||||
if migration in applied:
|
if migration in applied:
|
||||||
plan.append((self.loader.graph.nodes[migration], True))
|
plan.append((self.loader.graph.nodes[migration], True))
|
||||||
|
|
|
@ -79,6 +79,23 @@ class MigrationLoader(object):
|
||||||
raise BadMigrationError("Migration %s in app %s has no Migration class" % (migration_name, app_label))
|
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)
|
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
|
@cached_property
|
||||||
def graph(self):
|
def graph(self):
|
||||||
"""
|
"""
|
||||||
|
@ -141,3 +158,10 @@ class BadMigrationError(Exception):
|
||||||
Raised when there's a bad migration (unreadable/bad format/etc.)
|
Raised when there's a bad migration (unreadable/bad format/etc.)
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AmbiguityError(Exception):
|
||||||
|
"""
|
||||||
|
Raised when more than one migration matches a name prefix
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
from django.db import connection
|
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
|
from django.db.migrations.recorder import MigrationRecorder
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,3 +64,16 @@ class LoaderTests(TestCase):
|
||||||
[x for x, y in book_state.fields],
|
[x for x, y in book_state.fields],
|
||||||
["id", "author"]
|
["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…
Reference in New Issue