Fixed #21142: Dependency failures on unmigrated apps.

This commit is contained in:
Andrew Godwin 2014-01-08 13:00:12 +00:00
parent 0423e0796a
commit 64887c644a
7 changed files with 92 additions and 4 deletions

View File

@ -299,6 +299,7 @@ class MigrationAutodetector(object):
for migration in migrations: for migration in migrations:
name_map[(app_label, migration.name)] = (app_label, "__first__") name_map[(app_label, migration.name)] = (app_label, "__first__")
del changes[app_label] del changes[app_label]
continue
# Work out the next number in the sequence # Work out the next number in the sequence
if app_leaf is None: if app_leaf is None:
next_number = 1 next_number = 1

View File

@ -63,14 +63,14 @@ class MigrationGraph(object):
raise ValueError("Node %r not a valid node" % (node, )) raise ValueError("Node %r not a valid node" % (node, ))
return self.dfs(node, lambda x: self.dependents.get(x, set())) 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 Returns all root nodes - that is, nodes with no dependencies inside
their app. These are the starting point for an app. their app. These are the starting point for an app.
""" """
roots = set() roots = set()
for node in self.nodes: 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) roots.add(node)
return roots return roots
@ -145,6 +145,9 @@ class MigrationGraph(object):
project_state = self.nodes[node].mutate_state(project_state) project_state = self.nodes[node].mutate_state(project_state)
return project_state return project_state
def __contains__(self, node):
return node in self.nodes
class CircularDependencyError(Exception): class CircularDependencyError(Exception):
""" """

View File

@ -5,6 +5,9 @@ import sys
from django.apps import apps from django.apps import apps
from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.recorder import MigrationRecorder
from django.db.migrations.graph import MigrationGraph 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.utils import six
from django.conf import settings from django.conf import settings
@ -191,6 +194,38 @@ class MigrationLoader(object):
self.graph.add_node(key, migration) self.graph.add_node(key, migration)
for key, migration in normal.items(): for key, migration in normal.items():
for parent in migration.dependencies: 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) self.graph.add_dependency(key, parent)
def detect_conflicts(self): def detect_conflicts(self):

View File

@ -49,7 +49,10 @@ class LoaderTests(TestCase):
migration_loader = MigrationLoader(connection) migration_loader = MigrationLoader(connection)
self.assertEqual( self.assertEqual(
migration_loader.graph.forwards_plan(("migrations", "0002_second")), migration_loader.graph.forwards_plan(("migrations", "0002_second")),
[("migrations", "0001_initial"), ("migrations", "0002_second")], [
("migrations", "0001_initial"),
("migrations", "0002_second"),
],
) )
# Now render it out! # Now render it out!
project_state = migration_loader.graph.project_state(("migrations", "0002_second")) project_state = migration_loader.graph.project_state(("migrations", "0002_second"))
@ -67,6 +70,30 @@ class LoaderTests(TestCase):
["id", "author"] ["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"}) @override_settings(MIGRATION_MODULES={"migrations": "migrations.test_migrations"})
def test_name_match(self): def test_name_match(self):
"Tests prefix name matching" "Tests prefix name matching"

View File

@ -3,7 +3,9 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("migrations", "0001_initial")] dependencies = [
("migrations", "0001_initial"),
]
operations = [ operations = [

View File

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