mirror of
https://github.com/django/django.git
synced 2024-11-29 22:56:46 +01:00
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:
|
||||
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
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -3,7 +3,9 @@ from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("migrations", "0001_initial")]
|
||||
dependencies = [
|
||||
("migrations", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
|
20
tests/migrations/test_migrations_unmigdep/0001_initial.py
Normal file
20
tests/migrations/test_migrations_unmigdep/0001_initial.py
Normal 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)),
|
||||
],
|
||||
)
|
||||
|
||||
]
|
Loading…
Reference in New Issue
Block a user