summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrei Kulakov <andrei.avk@gmail.com>2015-03-31 16:30:39 -0400
committerTim Graham <timograham@gmail.com>2015-07-13 15:57:40 -0400
commitdb97a8849519a3933bf4abd2184efd68ebc21965 (patch)
tree456edbf87bf6084e3345723b96492c7cd48a6a3a
parenta2b999dfcac9bc92513a36ec6b3033ded1561c66 (diff)
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.
-rw-r--r--AUTHORS1
-rw-r--r--django/core/management/commands/squashmigrations.py1
-rw-r--r--django/db/migrations/autodetector.py2
-rw-r--r--django/db/migrations/executor.py41
-rw-r--r--django/db/migrations/migration.py7
-rw-r--r--django/db/migrations/writer.py6
-rw-r--r--docs/releases/1.9.txt8
-rw-r--r--docs/topics/migrations.txt35
-rw-r--r--tests/migrations/test_autodetector.py3
-rw-r--r--tests/migrations/test_commands.py52
-rw-r--r--tests/migrations/test_migrations/0001_initial.py2
-rw-r--r--tests/migrations/test_migrations_fake_split_initial/0001_initial.py33
-rw-r--r--tests/migrations/test_migrations_fake_split_initial/0002_second.py24
-rw-r--r--tests/migrations/test_migrations_fake_split_initial/__init__.py0
-rw-r--r--tests/migrations/test_migrations_initial_false/0001_not_initial.py33
-rw-r--r--tests/migrations/test_migrations_initial_false/__init__.py0
16 files changed, 232 insertions, 16 deletions
diff --git a/AUTHORS b/AUTHORS
index 05f568c1ba..b982d35b1e 100644
--- a/AUTHORS
+++ b/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>
diff --git a/django/core/management/commands/squashmigrations.py b/django/core/management/commands/squashmigrations.py
index 74e5ce78c3..db55195187 100644
--- a/django/core/management/commands/squashmigrations.py
+++ b/django/core/management/commands/squashmigrations.py
@@ -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)
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index ddf8e36b83..b20946999f 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -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:
diff --git a/django/db/migrations/executor.py b/django/db/migrations/executor.py
index aeb021e33c..c25aee6193 100644
--- a/django/db/migrations/executor.py
+++ b/django/db/migrations/executor.py
@@ -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
diff --git a/django/db/migrations/migration.py b/django/db/migrations/migration.py
index 59edf599f6..7641eb45bc 100644
--- a/django/db/migrations/migration.py
+++ b/django/db/migrations/migration.py
@@ -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
diff --git a/django/db/migrations/writer.py b/django/db/migrations/writer.py
index a7848d744f..11ecb6ed2b 100644
--- a/django/db/migrations/writer.py
+++ b/django/db/migrations/writer.py
@@ -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\
]
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 4e8fad3bf8..d87e0a6618 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -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
^^^^^^
diff --git a/docs/topics/migrations.txt b/docs/topics/migrations.txt
index 75373537c4..82354f96a8 100644
--- a/docs/topics/migrations.txt
+++ b/docs/topics/migrations.txt
@@ -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`;
diff --git a/tests/migrations/test_autodetector.py b/tests/migrations/test_autodetector.py
index c7c775ae30..7afce3d20b 100644
--- a/tests/migrations/test_autodetector.py
+++ b/tests/migrations/test_autodetector.py
@@ -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):
"""
diff --git a/tests/migrations/test_commands.py b/tests/migrations/test_commands.py
index d1bef38716..f50a1820ae 100644
--- a/tests/migrations/test_commands.py
+++ b/tests/migrations/test_commands.py
@@ -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.
diff --git a/tests/migrations/test_migrations/0001_initial.py b/tests/migrations/test_migrations/0001_initial.py
index 9286d0f02b..7a18c10c0d 100644
--- a/tests/migrations/test_migrations/0001_initial.py
+++ b/tests/migrations/test_migrations/0001_initial.py
@@ -6,6 +6,8 @@ from django.db import migrations, models
class Migration(migrations.Migration):
+ initial = True
+
operations = [
migrations.CreateModel(
diff --git a/tests/migrations/test_migrations_fake_split_initial/0001_initial.py b/tests/migrations/test_migrations_fake_split_initial/0001_initial.py
new file mode 100644
index 0000000000..1f42af2fb8
--- /dev/null
+++ b/tests/migrations/test_migrations_fake_split_initial/0001_initial.py
@@ -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')]),
+ ),
+ ]
diff --git a/tests/migrations/test_migrations_fake_split_initial/0002_second.py b/tests/migrations/test_migrations_fake_split_initial/0002_second.py
new file mode 100644
index 0000000000..786b77a654
--- /dev/null
+++ b/tests/migrations/test_migrations_fake_split_initial/0002_second.py
@@ -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)),
+ ],
+ ),
+ ]
diff --git a/tests/migrations/test_migrations_fake_split_initial/__init__.py b/tests/migrations/test_migrations_fake_split_initial/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/migrations/test_migrations_fake_split_initial/__init__.py
diff --git a/tests/migrations/test_migrations_initial_false/0001_not_initial.py b/tests/migrations/test_migrations_initial_false/0001_not_initial.py
new file mode 100644
index 0000000000..9345321776
--- /dev/null
+++ b/tests/migrations/test_migrations_initial_false/0001_not_initial.py
@@ -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')]),
+ ),
+ ]
diff --git a/tests/migrations/test_migrations_initial_false/__init__.py b/tests/migrations/test_migrations_initial_false/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/migrations/test_migrations_initial_false/__init__.py