Fixed #24375 -- Added Migration.initial attribute
The new attribute is checked when the `migrate --fake-initial` option is used. initial will be set to True for all initial migrations (this is particularly useful when initial migrations are split) as well as for squashed migrations.
This commit is contained in:
parent
a2b999dfca
commit
db97a88495
1
AUTHORS
1
AUTHORS
|
@ -40,6 +40,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Ana Krivokapic <https://github.com/infraredgirl>
|
||||
Andi Albrecht <albrecht.andi@gmail.com>
|
||||
André Ericson <de.ericson@gmail.com>
|
||||
Andrei Kulakov <andrei.avk@gmail.com>
|
||||
Andreas
|
||||
Andreas Mock <andreas.mock@web.de>
|
||||
Andreas Pelme <andreas@pelme.se>
|
||||
|
|
|
@ -131,6 +131,7 @@ class Command(BaseCommand):
|
|||
"dependencies": dependencies,
|
||||
"operations": new_operations,
|
||||
"replaces": replaces,
|
||||
"initial": True,
|
||||
})
|
||||
new_migration = subclass("0001_squashed_%s" % migration.name, app_label)
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ class MigrationAutodetector(object):
|
|||
self.from_state = from_state
|
||||
self.to_state = to_state
|
||||
self.questioner = questioner or MigrationQuestioner()
|
||||
self.existing_apps = {app for app, model in from_state.models}
|
||||
|
||||
def changes(self, graph, trim_to_apps=None, convert_apps=None, migration_name=None):
|
||||
"""
|
||||
|
@ -297,6 +298,7 @@ class MigrationAutodetector(object):
|
|||
instance = subclass("auto_%i" % (len(self.migrations.get(app_label, [])) + 1), app_label)
|
||||
instance.dependencies = list(dependencies)
|
||||
instance.operations = chopped
|
||||
instance.initial = app_label not in self.existing_apps
|
||||
self.migrations.setdefault(app_label, []).append(instance)
|
||||
chop_mode = False
|
||||
else:
|
||||
|
|
|
@ -197,19 +197,25 @@ class MigrationExecutor(object):
|
|||
def detect_soft_applied(self, project_state, migration):
|
||||
"""
|
||||
Tests whether a migration has been implicitly applied - that the
|
||||
tables it would create exist. This is intended only for use
|
||||
on initial migrations (as it only looks for CreateModel).
|
||||
tables or columns it would create exist. This is intended only for use
|
||||
on initial migrations (as it only looks for CreateModel and AddField).
|
||||
"""
|
||||
if migration.initial is None:
|
||||
# Bail if the migration isn't the first one in its app
|
||||
if [name for app, name in migration.dependencies if app == migration.app_label]:
|
||||
if any(app == migration.app_label for app, name in migration.dependencies):
|
||||
return False, project_state
|
||||
elif migration.initial is False:
|
||||
# Bail if it's NOT an initial migration
|
||||
return False, project_state
|
||||
|
||||
if project_state is None:
|
||||
after_state = self.loader.project_state((migration.app_label, migration.name), at_end=True)
|
||||
else:
|
||||
after_state = migration.mutate_state(project_state)
|
||||
apps = after_state.apps
|
||||
found_create_migration = False
|
||||
# Make sure all create model are done
|
||||
found_create_model_migration = False
|
||||
found_add_field_migration = False
|
||||
# Make sure all create model and add field operations are done
|
||||
for operation in migration.operations:
|
||||
if isinstance(operation, migrations.CreateModel):
|
||||
model = apps.get_model(migration.app_label, operation.name)
|
||||
|
@ -217,9 +223,26 @@ class MigrationExecutor(object):
|
|||
# We have to fetch the model to test with from the
|
||||
# main app cache, as it's not a direct dependency.
|
||||
model = global_apps.get_model(model._meta.swapped)
|
||||
if model._meta.proxy or not model._meta.managed:
|
||||
continue
|
||||
if model._meta.db_table not in self.connection.introspection.table_names(self.connection.cursor()):
|
||||
return False, project_state
|
||||
found_create_migration = True
|
||||
# If we get this far and we found at least one CreateModel migration,
|
||||
found_create_model_migration = True
|
||||
elif isinstance(operation, migrations.AddField):
|
||||
model = apps.get_model(migration.app_label, operation.model_name)
|
||||
if model._meta.swapped:
|
||||
# We have to fetch the model to test with from the
|
||||
# main app cache, as it's not a direct dependency.
|
||||
model = global_apps.get_model(model._meta.swapped)
|
||||
if model._meta.proxy or not model._meta.managed:
|
||||
continue
|
||||
|
||||
table = model._meta.db_table
|
||||
db_field = model._meta.get_field(operation.name).column
|
||||
fields = self.connection.introspection.get_table_description(self.connection.cursor(), table)
|
||||
if db_field not in (f.name for f in fields):
|
||||
return False, project_state
|
||||
found_add_field_migration = True
|
||||
# If we get this far and we found at least one CreateModel or AddField migration,
|
||||
# the migration is considered implicitly applied.
|
||||
return found_create_migration, after_state
|
||||
return (found_create_model_migration or found_add_field_migration), after_state
|
||||
|
|
|
@ -41,6 +41,13 @@ class Migration(object):
|
|||
# are not applied.
|
||||
replaces = []
|
||||
|
||||
# Is this an initial migration? Initial migrations are skipped on
|
||||
# --fake-initial if the table or fields already exist. If None, check if
|
||||
# the migration has any dependencies to determine if there are dependencies
|
||||
# to tell if db introspection needs to be done. If True, always perform
|
||||
# introspection. If False, never perform introspection.
|
||||
initial = None
|
||||
|
||||
def __init__(self, name, app_label):
|
||||
self.name = name
|
||||
self.app_label = app_label
|
||||
|
|
|
@ -155,6 +155,7 @@ class MigrationWriter(object):
|
|||
"""
|
||||
items = {
|
||||
"replaces_str": "",
|
||||
"initial_str": "",
|
||||
}
|
||||
|
||||
imports = set()
|
||||
|
@ -211,6 +212,9 @@ class MigrationWriter(object):
|
|||
if self.migration.replaces:
|
||||
items['replaces_str'] = "\n replaces = %s\n" % self.serialize(self.migration.replaces)[0]
|
||||
|
||||
if self.migration.initial:
|
||||
items['initial_str'] = "\n initial = True\n"
|
||||
|
||||
return (MIGRATION_TEMPLATE % items).encode("utf8")
|
||||
|
||||
@staticmethod
|
||||
|
@ -508,7 +512,7 @@ from __future__ import unicode_literals
|
|||
%(imports)s
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
%(replaces_str)s
|
||||
%(replaces_str)s%(initial_str)s
|
||||
dependencies = [
|
||||
%(dependencies)s\
|
||||
]
|
||||
|
|
|
@ -368,6 +368,14 @@ Management Commands
|
|||
to the database using the password from your settings file (instead of
|
||||
requiring it to be manually entered).
|
||||
|
||||
Migrations
|
||||
^^^^^^^^^^
|
||||
|
||||
* Initial migrations are now marked with an :attr:`initial = True
|
||||
<django.db.migrations.Migration.initial>` class attribute which allows
|
||||
:djadminopt:`migrate --fake-initial <--fake-initial>` to more easily detect
|
||||
initial migrations.
|
||||
|
||||
Models
|
||||
^^^^^^
|
||||
|
||||
|
|
|
@ -276,6 +276,33 @@ class to make it importable::
|
|||
Please refer to the notes about :ref:`historical-models` in migrations to see
|
||||
the implications that come along.
|
||||
|
||||
Initial migrations
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. attribute:: Migration.initial
|
||||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
The "initial migrations" for an app are the migrations that create the first
|
||||
version of that app's tables. Usually an app will have just one initial
|
||||
migration, but in some cases of complex model interdependencies it may have two
|
||||
or more.
|
||||
|
||||
Initial migrations are marked with an ``initial = True`` class attribute on the
|
||||
migration class. If an ``initial`` class attribute isn't found, a migration
|
||||
will be considered "initial" if it is the first migration in the app (i.e. if
|
||||
it has no dependencies on any other migration in the same app).
|
||||
|
||||
When :djadmin:`migrate` is run with the :djadminopt:`--fake-initial` option,
|
||||
these initial migrations are treated specially. For an initial migration that
|
||||
creates one or more tables (``CreateModel`` operation), Django checks that all
|
||||
of those tables already exist in the database and fake-applies the migration
|
||||
if so. Similarly, for an initial migration that adds one or more fields
|
||||
(``AddField`` operation), Django checks that all of the respective columns
|
||||
already exist in the database and fake-applies the migration if so. Without
|
||||
:djadminopt:`--fake-initial`, initial migrations are treated no differently
|
||||
from any other migration.
|
||||
|
||||
Adding migrations to apps
|
||||
-------------------------
|
||||
|
||||
|
@ -425,6 +452,7 @@ Then, open up the file; it should look something like this::
|
|||
from django.db import models, migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('yourappname', '0001_initial'),
|
||||
|
@ -460,6 +488,7 @@ need to do is use the historical model and iterate over the rows::
|
|||
person.save()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('yourappname', '0001_initial'),
|
||||
|
@ -761,12 +790,6 @@ If you already have pre-existing migrations created with
|
|||
without running them. (Django won't check that the table schema match your
|
||||
models, just that the right table names exist).
|
||||
|
||||
That's it! The only complication is if you have a circular dependency loop
|
||||
of foreign keys; in this case, ``makemigrations`` might make more than one
|
||||
initial migration, and you'll need to mark them all as applied using::
|
||||
|
||||
python manage.py migrate --fake yourappnamehere
|
||||
|
||||
.. versionchanged:: 1.8
|
||||
|
||||
The :djadminopt:`--fake-initial` flag was added to :djadmin:`migrate`;
|
||||
|
|
|
@ -877,6 +877,9 @@ class AutodetectorTests(TestCase):
|
|||
self.assertOperationTypes(changes, 'otherapp', 1, ["AddField"])
|
||||
self.assertMigrationDependencies(changes, 'otherapp', 0, [])
|
||||
self.assertMigrationDependencies(changes, 'otherapp', 1, [("otherapp", "auto_1"), ("testapp", "auto_1")])
|
||||
# both split migrations should be `initial`
|
||||
self.assertTrue(changes['otherapp'][0].initial)
|
||||
self.assertTrue(changes['otherapp'][1].initial)
|
||||
|
||||
def test_same_app_circular_fk_dependency(self):
|
||||
"""
|
||||
|
|
|
@ -50,6 +50,30 @@ class MigrateTests(MigrationTestBase):
|
|||
self.assertTableNotExists("migrations_tribble")
|
||||
self.assertTableNotExists("migrations_book")
|
||||
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_initial_false"})
|
||||
def test_migrate_initial_false(self):
|
||||
"""
|
||||
`Migration.initial = False` skips fake-initial detection.
|
||||
"""
|
||||
# Make sure no tables are created
|
||||
self.assertTableNotExists("migrations_author")
|
||||
self.assertTableNotExists("migrations_tribble")
|
||||
# Run the migrations to 0001 only
|
||||
call_command("migrate", "migrations", "0001", verbosity=0)
|
||||
# Fake rollback
|
||||
call_command("migrate", "migrations", "zero", fake=True, verbosity=0)
|
||||
# Make sure fake-initial detection does not run
|
||||
with self.assertRaises(DatabaseError):
|
||||
call_command("migrate", "migrations", "0001", fake_initial=True, verbosity=0)
|
||||
|
||||
call_command("migrate", "migrations", "0001", fake=True, verbosity=0)
|
||||
# Real rollback
|
||||
call_command("migrate", "migrations", "zero", verbosity=0)
|
||||
# Make sure it's all gone
|
||||
self.assertTableNotExists("migrations_author")
|
||||
self.assertTableNotExists("migrations_tribble")
|
||||
self.assertTableNotExists("migrations_book")
|
||||
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
|
||||
def test_migrate_fake_initial(self):
|
||||
"""
|
||||
|
@ -109,6 +133,24 @@ class MigrateTests(MigrationTestBase):
|
|||
self.assertTableNotExists("migrations_tribble")
|
||||
self.assertTableNotExists("migrations_book")
|
||||
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_fake_split_initial"})
|
||||
def test_migrate_fake_split_initial(self):
|
||||
"""
|
||||
Split initial migrations can be faked with --fake-initial.
|
||||
"""
|
||||
call_command("migrate", "migrations", "0002", verbosity=0)
|
||||
call_command("migrate", "migrations", "zero", fake=True, verbosity=0)
|
||||
out = six.StringIO()
|
||||
with mock.patch('django.core.management.color.supports_color', lambda *args: False):
|
||||
call_command("migrate", "migrations", "0002", fake_initial=True, stdout=out, verbosity=1)
|
||||
value = out.getvalue().lower()
|
||||
self.assertIn("migrations.0001_initial... faked", value)
|
||||
self.assertIn("migrations.0002_second... faked", value)
|
||||
# Fake an apply
|
||||
call_command("migrate", "migrations", fake=True, verbosity=0)
|
||||
# Unmigrate everything
|
||||
call_command("migrate", "migrations", "zero", verbosity=0)
|
||||
|
||||
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_conflict"})
|
||||
def test_migrate_conflict_exit(self):
|
||||
"""
|
||||
|
@ -409,6 +451,7 @@ class MakeMigrationsTests(MigrationTestBase):
|
|||
content = fp.read()
|
||||
self.assertIn('# -*- coding: utf-8 -*-', content)
|
||||
self.assertIn('migrations.CreateModel', content)
|
||||
self.assertIn('initial = True', content)
|
||||
|
||||
if six.PY3:
|
||||
self.assertIn('úñí©óðé µóðéø', content) # Meta.verbose_name
|
||||
|
@ -882,6 +925,15 @@ class SquashMigrationsTests(MigrationTestBase):
|
|||
squashed_migration_file = os.path.join(migration_dir, "0001_squashed_0002_second.py")
|
||||
self.assertTrue(os.path.exists(squashed_migration_file))
|
||||
|
||||
def test_squashmigrations_initial_attribute(self):
|
||||
with self.temporary_migration_module(module="migrations.test_migrations") as migration_dir:
|
||||
call_command("squashmigrations", "migrations", "0002", interactive=False, verbosity=0)
|
||||
|
||||
squashed_migration_file = os.path.join(migration_dir, "0001_squashed_0002_second.py")
|
||||
with codecs.open(squashed_migration_file, "r", encoding="utf-8") as fp:
|
||||
content = fp.read()
|
||||
self.assertIn("initial = True", content)
|
||||
|
||||
def test_squashmigrations_optimizes(self):
|
||||
"""
|
||||
Tests that squashmigrations optimizes operations.
|
||||
|
|
|
@ -6,6 +6,8 @@ from django.db import migrations, models
|
|||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
operations = [
|
||||
|
||||
migrations.CreateModel(
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
"Author",
|
||||
[
|
||||
("id", models.AutoField(primary_key=True)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("slug", models.SlugField(null=True)),
|
||||
("age", models.IntegerField(default=0)),
|
||||
("silly_field", models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
"Tribble",
|
||||
[
|
||||
("id", models.AutoField(primary_key=True)),
|
||||
("fluffy", models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='author',
|
||||
unique_together=set([('name', 'slug')]),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("migrations", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField("Author", "rating", models.IntegerField(default=0)),
|
||||
migrations.CreateModel(
|
||||
"Book",
|
||||
[
|
||||
("id", models.AutoField(primary_key=True)),
|
||||
("author", models.ForeignKey("migrations.Author", null=True)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = False
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
"Author",
|
||||
[
|
||||
("id", models.AutoField(primary_key=True)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("slug", models.SlugField(null=True)),
|
||||
("age", models.IntegerField(default=0)),
|
||||
("silly_field", models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
"Tribble",
|
||||
[
|
||||
("id", models.AutoField(primary_key=True)),
|
||||
("fluffy", models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='author',
|
||||
unique_together=set([('name', 'slug')]),
|
||||
),
|
||||
]
|
Loading…
Reference in New Issue