Fixed #21142: Dependency failures on unmigrated apps.
This commit is contained in:
parent
0423e0796a
commit
64887c644a
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
||||||
|
|
|
@ -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)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
]
|
Loading…
Reference in New Issue