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:
Andrei Kulakov 2015-03-31 16:30:39 -04:00 committed by Tim Graham
parent a2b999dfca
commit db97a88495
16 changed files with 232 additions and 16 deletions

View File

@ -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>

View File

@ -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)

View File

@ -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:

View File

@ -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).
"""
# 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 migration.initial is None:
# Bail if the migration isn't the first one in its app
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

View File

@ -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

View File

@ -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\
]

View File

@ -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
^^^^^^

View File

@ -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`;

View File

@ -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):
"""

View File

@ -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.

View File

@ -6,6 +6,8 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
operations = [
migrations.CreateModel(

View File

@ -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')]),
),
]

View File

@ -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)),
],
),
]

View File

@ -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')]),
),
]