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.loader import AmbiguityError
|
||||||
from django.db.migrations.state import ModelState, ProjectState
|
from django.db.migrations.state import ModelState, ProjectState
|
||||||
from django.utils.module_loading import module_has_submodule
|
from django.utils.module_loading import module_has_submodule
|
||||||
|
from django.utils.text import Truncator
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -50,6 +51,10 @@ class Command(BaseCommand):
|
||||||
'that the current database schema matches your initial migration before using this '
|
'that the current database schema matches your initial migration before using this '
|
||||||
'flag. Django will only check for an existing table name.',
|
'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(
|
parser.add_argument(
|
||||||
'--run-syncdb', action='store_true',
|
'--run-syncdb', action='store_true',
|
||||||
help='Creates tables for apps without migrations.',
|
help='Creates tables for apps without migrations.',
|
||||||
|
@ -134,8 +139,20 @@ class Command(BaseCommand):
|
||||||
targets = executor.loader.graph.leaf_nodes()
|
targets = executor.loader.graph.leaf_nodes()
|
||||||
|
|
||||||
plan = executor.migration_plan(targets)
|
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
|
# Print some useful info
|
||||||
if self.verbosity >= 1:
|
if self.verbosity >= 1:
|
||||||
self.stdout.write(self.style.MIGRATE_HEADING("Operations to perform:"))
|
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.
|
# Deferred SQL is executed when exiting the editor's context.
|
||||||
if self.verbosity >= 1:
|
if self.verbosity >= 1:
|
||||||
self.stdout.write(" Running deferred SQL...\n")
|
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
|
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.
|
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
|
.. django-admin-option:: --run-syncdb
|
||||||
|
|
||||||
Allows creating tables for apps without migrations. While this isn't
|
Allows creating tables for apps without migrations. While this isn't
|
||||||
|
|
|
@ -175,7 +175,8 @@ Management Commands
|
||||||
Migrations
|
Migrations
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* The new :option:`migrate --plan` option prints the list of migration
|
||||||
|
operations that will be performed.
|
||||||
|
|
||||||
Models
|
Models
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
|
@ -298,6 +298,73 @@ class MigrateTests(MigrationTestBase):
|
||||||
# Cleanup by unmigrating everything
|
# Cleanup by unmigrating everything
|
||||||
call_command("migrate", "migrations", "zero", verbosity=0)
|
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"})
|
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_empty"})
|
||||||
def test_showmigrations_plan_no_migrations(self):
|
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