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