Fixed #29198 -- Added migrate --plan option.
This commit is contained in:
parent
1160a97596
commit
058d33f3ed
|
@ -16,6 +16,7 @@ from django.db.migrations.executor import MigrationExecutor
|
|||
from django.db.migrations.loader import AmbiguityError
|
||||
from django.db.migrations.state import ModelState, ProjectState
|
||||
from django.utils.module_loading import module_has_submodule
|
||||
from django.utils.text import Truncator
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -50,6 +51,10 @@ class Command(BaseCommand):
|
|||
'that the current database schema matches your initial migration before using this '
|
||||
'flag. Django will only check for an existing table name.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--plan', action='store_true',
|
||||
help='Shows a list of the migration actions that will be performed.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--run-syncdb', action='store_true',
|
||||
help='Creates tables for apps without migrations.',
|
||||
|
@ -134,8 +139,20 @@ class Command(BaseCommand):
|
|||
targets = executor.loader.graph.leaf_nodes()
|
||||
|
||||
plan = executor.migration_plan(targets)
|
||||
run_syncdb = options['run_syncdb'] and executor.loader.unmigrated_apps
|
||||
|
||||
if options['plan']:
|
||||
self.stdout.write('Planned operations:', self.style.MIGRATE_LABEL)
|
||||
if not plan:
|
||||
self.stdout.write(' No planned migration operations.')
|
||||
for migration, backwards in plan:
|
||||
self.stdout.write(str(migration), self.style.MIGRATE_HEADING)
|
||||
for operation in migration.operations:
|
||||
message, is_error = self.describe_operation(operation, backwards)
|
||||
style = self.style.WARNING if is_error else None
|
||||
self.stdout.write(' ' + message, style)
|
||||
return
|
||||
|
||||
run_syncdb = options['run_syncdb'] and executor.loader.unmigrated_apps
|
||||
# Print some useful info
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:"))
|
||||
|
@ -309,3 +326,27 @@ class Command(BaseCommand):
|
|||
# Deferred SQL is executed when exiting the editor's context.
|
||||
if self.verbosity >= 1:
|
||||
self.stdout.write(" Running deferred SQL...\n")
|
||||
|
||||
@staticmethod
|
||||
def describe_operation(operation, backwards):
|
||||
"""Return a string that describes a migration operation for --plan."""
|
||||
prefix = ''
|
||||
if hasattr(operation, 'code'):
|
||||
code = operation.reverse_code if backwards else operation.code
|
||||
action = code.__doc__ if code else ''
|
||||
elif hasattr(operation, 'sql'):
|
||||
action = operation.reverse_sql if backwards else operation.sql
|
||||
else:
|
||||
action = ''
|
||||
if backwards:
|
||||
prefix = 'Undo '
|
||||
if action is None:
|
||||
action = 'IRREVERSIBLE'
|
||||
is_error = True
|
||||
else:
|
||||
action = action.replace('\n', '')
|
||||
is_error = False
|
||||
if action:
|
||||
action = ' -> ' + action
|
||||
truncated = Truncator(action)
|
||||
return prefix + operation.describe() + truncated.chars(40), is_error
|
||||
|
|
|
@ -804,6 +804,13 @@ option does not, however, check for matching database schema beyond matching
|
|||
table names and so is only safe to use if you are confident that your existing
|
||||
schema matches what is recorded in your initial migration.
|
||||
|
||||
.. django-admin-option:: --plan
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
Shows the migration operations that will be performed for the given ``migrate``
|
||||
command.
|
||||
|
||||
.. django-admin-option:: --run-syncdb
|
||||
|
||||
Allows creating tables for apps without migrations. While this isn't
|
||||
|
|
|
@ -175,7 +175,8 @@ Management Commands
|
|||
Migrations
|
||||
~~~~~~~~~~
|
||||
|
||||
* ...
|
||||
* The new :option:`migrate --plan` option prints the list of migration
|
||||
operations that will be performed.
|
||||
|
||||
Models
|
||||
~~~~~~
|
||||
|
|
|
@ -298,6 +298,73 @@ class MigrateTests(MigrationTestBase):
|
|||
# Cleanup by unmigrating everything
|
||||
call_command("migrate", "migrations", "zero", verbosity=0)
|
||||
|
||||
@override_settings(MIGRATION_MODULES={'migrations': 'migrations.test_migrations_plan'})
|
||||
def test_migrate_plan(self):
|
||||
"""Tests migrate --plan output."""
|
||||
out = io.StringIO()
|
||||
# Show the plan up to the third migration.
|
||||
call_command('migrate', 'migrations', '0003', plan=True, stdout=out, no_color=True)
|
||||
self.assertEqual(
|
||||
'Planned operations:\n'
|
||||
'migrations.0001_initial\n'
|
||||
' Create model Salamander\n'
|
||||
' Raw Python operation -> Grow salamander tail.\n'
|
||||
'migrations.0002_second\n'
|
||||
' Create model Book\n'
|
||||
' Raw SQL operation -> SELECT * FROM migrations_book\n'
|
||||
'migrations.0003_third\n'
|
||||
' Create model Author\n'
|
||||
' Raw SQL operation -> SELECT * FROM migrations_author\n',
|
||||
out.getvalue()
|
||||
)
|
||||
# Migrate to the third migration.
|
||||
call_command('migrate', 'migrations', '0003', verbosity=0)
|
||||
out = io.StringIO()
|
||||
# Show the plan for when there is nothing to apply.
|
||||
call_command('migrate', 'migrations', '0003', plan=True, stdout=out, no_color=True)
|
||||
self.assertEqual(
|
||||
'Planned operations:\n'
|
||||
' No planned migration operations.\n',
|
||||
out.getvalue()
|
||||
)
|
||||
out = io.StringIO()
|
||||
# Show the plan for reverse migration back to 0001.
|
||||
call_command('migrate', 'migrations', '0001', plan=True, stdout=out, no_color=True)
|
||||
self.assertEqual(
|
||||
'Planned operations:\n'
|
||||
'migrations.0003_third\n'
|
||||
' Undo Create model Author\n'
|
||||
' Raw SQL operation -> SELECT * FROM migrations_book\n'
|
||||
'migrations.0002_second\n'
|
||||
' Undo Create model Book\n'
|
||||
' Raw SQL operation -> SELECT * FROM migrations_salamander\n',
|
||||
out.getvalue()
|
||||
)
|
||||
out = io.StringIO()
|
||||
# Show the migration plan to fourth, with truncated details.
|
||||
call_command('migrate', 'migrations', '0004', plan=True, stdout=out, no_color=True)
|
||||
self.assertEqual(
|
||||
'Planned operations:\n'
|
||||
'migrations.0004_fourth\n'
|
||||
' Raw SQL operation -> SELECT * FROM migrations_author W...\n',
|
||||
out.getvalue()
|
||||
)
|
||||
# Migrate to the fourth migration.
|
||||
call_command('migrate', 'migrations', '0004', verbosity=0)
|
||||
out = io.StringIO()
|
||||
# Show the plan when an operation is irreversible.
|
||||
call_command('migrate', 'migrations', '0003', plan=True, stdout=out, no_color=True)
|
||||
self.assertEqual(
|
||||
'Planned operations:\n'
|
||||
'migrations.0004_fourth\n'
|
||||
' Raw SQL operation -> IRREVERSIBLE\n',
|
||||
out.getvalue()
|
||||
)
|
||||
# Cleanup by unmigrating everything: fake the irreversible, then
|
||||
# migrate all to zero.
|
||||
call_command('migrate', 'migrations', '0003', fake=True, verbosity=0)
|
||||
call_command('migrate', 'migrations', 'zero', verbosity=0)
|
||||
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_empty"})
|
||||
def test_showmigrations_plan_no_migrations(self):
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
def grow_tail(x, y):
|
||||
"""Grow salamander tail."""
|
||||
pass
|
||||
|
||||
|
||||
def shrink_tail(x, y):
|
||||
"""Shrink salamander tail."""
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
'Salamander',
|
||||
[
|
||||
('id', models.AutoField(primary_key=True)),
|
||||
('tail', models.IntegerField(default=0)),
|
||||
('silly_field', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(grow_tail, shrink_tail),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('migrations', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
migrations.CreateModel(
|
||||
'Book',
|
||||
[
|
||||
('id', models.AutoField(primary_key=True)),
|
||||
],
|
||||
),
|
||||
migrations.RunSQL('SELECT * FROM migrations_book', 'SELECT * FROM migrations_salamander')
|
||||
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('migrations', '0002_second'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
migrations.CreateModel(
|
||||
'Author',
|
||||
[
|
||||
('id', models.AutoField(primary_key=True)),
|
||||
],
|
||||
),
|
||||
migrations.RunSQL('SELECT * FROM migrations_author', 'SELECT * FROM migrations_book')
|
||||
]
|
|
@ -0,0 +1,12 @@
|
|||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("migrations", "0003_third"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL('SELECT * FROM migrations_author WHERE id = 1')
|
||||
]
|
Loading…
Reference in New Issue