summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/db/migrations/autodetector.py1
-rw-r--r--django/db/migrations/graph.py7
-rw-r--r--django/db/migrations/loader.py35
-rw-r--r--tests/migrations/test_loader.py29
-rw-r--r--tests/migrations/test_migrations/0002_second.py4
-rw-r--r--tests/migrations/test_migrations_unmigdep/0001_initial.py20
-rw-r--r--tests/migrations/test_migrations_unmigdep/__init__.py0
7 files changed, 92 insertions, 4 deletions
diff --git a/django/db/migrations/autodetector.py b/django/db/migrations/autodetector.py
index b98aef2f9a..c7ac756b2e 100644
--- a/django/db/migrations/autodetector.py
+++ b/django/db/migrations/autodetector.py
@@ -299,6 +299,7 @@ class MigrationAutodetector(object):
for migration in migrations:
name_map[(app_label, migration.name)] = (app_label, "__first__")
del changes[app_label]
+ continue
# Work out the next number in the sequence
if app_leaf is None:
next_number = 1
diff --git a/django/db/migrations/graph.py b/django/db/migrations/graph.py
index 4f89fa2909..eddabbd434 100644
--- a/django/db/migrations/graph.py
+++ b/django/db/migrations/graph.py
@@ -63,14 +63,14 @@ class MigrationGraph(object):
raise ValueError("Node %r not a valid node" % (node, ))
return self.dfs(node, lambda x: self.dependents.get(x, set()))
- def root_nodes(self):
+ def root_nodes(self, app=None):
"""
Returns all root nodes - that is, nodes with no dependencies inside
their app. These are the starting point for an app.
"""
roots = set()
for node in self.nodes:
- if not any(key[0] == node[0] for key in self.dependencies.get(node, set())):
+ if not any(key[0] == node[0] for key in self.dependencies.get(node, set())) and (not app or app == node[0]):
roots.add(node)
return roots
@@ -145,6 +145,9 @@ class MigrationGraph(object):
project_state = self.nodes[node].mutate_state(project_state)
return project_state
+ def __contains__(self, node):
+ return node in self.nodes
+
class CircularDependencyError(Exception):
"""
diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py
index 66d2259b79..73f9d2a71e 100644
--- a/django/db/migrations/loader.py
+++ b/django/db/migrations/loader.py
@@ -5,6 +5,9 @@ import sys
from django.apps import apps
from django.db.migrations.recorder import MigrationRecorder
from django.db.migrations.graph import MigrationGraph
+from django.db.migrations.migration import Migration
+from django.db.migrations.state import ModelState
+from django.db.migrations import operations
from django.utils import six
from django.conf import settings
@@ -191,6 +194,38 @@ class MigrationLoader(object):
self.graph.add_node(key, migration)
for key, migration in normal.items():
for parent in migration.dependencies:
+ # Special-case __first__, which means "the first migration" for
+ # migrated apps, and is ignored for unmigrated apps. It allows
+ # makemigrations to declare dependencies on apps before they
+ # even have migrations.
+ if parent[1] == "__first__" and parent not in self.graph:
+ if parent[0] in self.unmigrated_apps:
+ # This app isn't migrated, but something depends on it.
+ # We'll add a fake initial migration for it into the
+ # graph.
+ app_config = apps.get_app_config(parent[0])
+ ops = []
+ for model in app_config.get_models():
+ model_state = ModelState.from_model(model)
+ ops.append(
+ operations.CreateModel(
+ name=model_state.name,
+ fields=model_state.fields,
+ options=model_state.options,
+ bases=model_state.bases,
+ )
+ )
+ new_migration = type(
+ "FakeInitialMigration",
+ (Migration, ),
+ {"operations": ops},
+ )(parent[1], parent[0])
+ self.graph.add_node(parent, new_migration)
+ self.applied_migrations.add(parent)
+ elif parent[0] in self.migrated_apps:
+ parent = (parent[0], list(self.graph.root_nodes(parent[0]))[0])
+ else:
+ raise ValueError("Dependency on unknown app %s" % parent[0])
self.graph.add_dependency(key, parent)
def detect_conflicts(self):
diff --git a/tests/migrations/test_loader.py b/tests/migrations/test_loader.py
index 7b9ce72aab..564e5207b8 100644
--- a/tests/migrations/test_loader.py
+++ b/tests/migrations/test_loader.py
@@ -49,7 +49,10 @@ class LoaderTests(TestCase):
migration_loader = MigrationLoader(connection)
self.assertEqual(
migration_loader.graph.forwards_plan(("migrations", "0002_second")),
- [("migrations", "0001_initial"), ("migrations", "0002_second")],
+ [
+ ("migrations", "0001_initial"),
+ ("migrations", "0002_second"),
+ ],
)
# Now render it out!
project_state = migration_loader.graph.project_state(("migrations", "0002_second"))
@@ -67,6 +70,30 @@ class LoaderTests(TestCase):
["id", "author"]
)
+ @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations_unmigdep"})
+ def test_load_unmigrated_dependency(self):
+ """
+ Makes sure the loader can load migrations with a dependency on an unmigrated app.
+ """
+ # Load and test the plan
+ migration_loader = MigrationLoader(connection)
+ self.assertEqual(
+ migration_loader.graph.forwards_plan(("migrations", "0001_initial")),
+ [
+ ("auth", "__first__"),
+ ("migrations", "0001_initial"),
+ ],
+ )
+ # Now render it out!
+ project_state = migration_loader.graph.project_state(("migrations", "0001_initial"))
+ self.assertEqual(len(project_state.models), 4)
+
+ book_state = project_state.models["migrations", "book"]
+ self.assertEqual(
+ [x for x, y in book_state.fields],
+ ["id", "user"]
+ )
+
@override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_name_match(self):
"Tests prefix name matching"
diff --git a/tests/migrations/test_migrations/0002_second.py b/tests/migrations/test_migrations/0002_second.py
index ace9a83347..736e844825 100644
--- a/tests/migrations/test_migrations/0002_second.py
+++ b/tests/migrations/test_migrations/0002_second.py
@@ -3,7 +3,9 @@ from django.db import migrations, models
class Migration(migrations.Migration):
- dependencies = [("migrations", "0001_initial")]
+ dependencies = [
+ ("migrations", "0001_initial"),
+ ]
operations = [
diff --git a/tests/migrations/test_migrations_unmigdep/0001_initial.py b/tests/migrations/test_migrations_unmigdep/0001_initial.py
new file mode 100644
index 0000000000..f4c11b4657
--- /dev/null
+++ b/tests/migrations/test_migrations_unmigdep/0001_initial.py
@@ -0,0 +1,20 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("auth", "__first__"),
+ ]
+
+ operations = [
+
+ migrations.CreateModel(
+ "Book",
+ [
+ ("id", models.AutoField(primary_key=True)),
+ ("user", models.ForeignKey("auth.User", null=True)),
+ ],
+ )
+
+ ]
diff --git a/tests/migrations/test_migrations_unmigdep/__init__.py b/tests/migrations/test_migrations_unmigdep/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/migrations/test_migrations_unmigdep/__init__.py