mirror of https://github.com/django/django.git
Autodetect ForeignKeys and add dependencies/split on circulars
This commit is contained in:
parent
48493cff73
commit
e2d7e83256
|
@ -36,9 +36,41 @@ class MigrationAutodetector(object):
|
||||||
"""
|
"""
|
||||||
# We'll store migrations as lists by app names for now
|
# We'll store migrations as lists by app names for now
|
||||||
self.migrations = {}
|
self.migrations = {}
|
||||||
# Adding models.
|
old_app_cache = self.from_state.render()
|
||||||
|
new_app_cache = self.to_state.render()
|
||||||
|
# Adding models. Phase 1 is adding models with no outward relationships.
|
||||||
added_models = set(self.to_state.models.keys()) - set(self.from_state.models.keys())
|
added_models = set(self.to_state.models.keys()) - set(self.from_state.models.keys())
|
||||||
|
pending_add = {}
|
||||||
for app_label, model_name in added_models:
|
for app_label, model_name in added_models:
|
||||||
|
model_state = self.to_state.models[app_label, model_name]
|
||||||
|
# Are there any relationships out from this model? if so, punt it to the next phase.
|
||||||
|
related_fields = []
|
||||||
|
for field in new_app_cache.get_model(app_label, model_name)._meta.fields:
|
||||||
|
if hasattr(field, "rel"):
|
||||||
|
if hasattr(field.rel, "to"):
|
||||||
|
related_fields.append((field.name, field.rel.to._meta.app_label.lower(), field.rel.to._meta.object_name.lower()))
|
||||||
|
if hasattr(field.rel, "through") and not field.rel.though._meta.auto_created:
|
||||||
|
related_fields.append((field.name, field.rel.through._meta.app_label.lower(), field.rel.through._meta.object_name.lower()))
|
||||||
|
if related_fields:
|
||||||
|
pending_add[app_label, model_name] = related_fields
|
||||||
|
else:
|
||||||
|
self.add_to_migration(
|
||||||
|
app_label,
|
||||||
|
operations.CreateModel(
|
||||||
|
name = model_state.name,
|
||||||
|
fields = model_state.fields,
|
||||||
|
options = model_state.options,
|
||||||
|
bases = model_state.bases,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Phase 2 is progressively adding pending models, splitting up into two
|
||||||
|
# migrations if required.
|
||||||
|
pending_new_fks = []
|
||||||
|
while pending_add:
|
||||||
|
# Is there one we can add that has all dependencies satisfied?
|
||||||
|
satisfied = [(m, rf) for m, rf in pending_add.items() if all((al, mn) not in pending_add for f, al, mn in rf)]
|
||||||
|
if satisfied:
|
||||||
|
(app_label, model_name), related_fields = sorted(satisfied)[0]
|
||||||
model_state = self.to_state.models[app_label, model_name]
|
model_state = self.to_state.models[app_label, model_name]
|
||||||
self.add_to_migration(
|
self.add_to_migration(
|
||||||
app_label,
|
app_label,
|
||||||
|
@ -49,6 +81,42 @@ class MigrationAutodetector(object):
|
||||||
bases = model_state.bases,
|
bases = model_state.bases,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
for field_name, other_app_label, other_model_name in related_fields:
|
||||||
|
self.add_dependency(app_label, other_app_label)
|
||||||
|
del pending_add[app_label, model_name]
|
||||||
|
# Ah well, we'll need to split one. Pick deterministically.
|
||||||
|
else:
|
||||||
|
(app_label, model_name), related_fields = sorted(pending_add.items())[0]
|
||||||
|
model_state = self.to_state.models[app_label, model_name]
|
||||||
|
# Work out the fields that need splitting out
|
||||||
|
bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields if (al, mn) in pending_add)
|
||||||
|
# Create the model, without those
|
||||||
|
self.add_to_migration(
|
||||||
|
app_label,
|
||||||
|
operations.CreateModel(
|
||||||
|
name = model_state.name,
|
||||||
|
fields = [(n, f) for n, f in model_state.fields if n not in bad_fields],
|
||||||
|
options = model_state.options,
|
||||||
|
bases = model_state.bases,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Add the bad fields to be made in a phase 3
|
||||||
|
for field_name, (other_app_label, other_model_name) in bad_fields.items():
|
||||||
|
pending_new_fks.append((app_label, model_name, field_name, other_app_label))
|
||||||
|
del pending_add[app_label, model_name]
|
||||||
|
# Phase 3 is adding the final set of FKs as separate new migrations
|
||||||
|
for app_label, model_name, field_name, other_app_label in pending_new_fks:
|
||||||
|
model_state = self.to_state.models[app_label, model_name]
|
||||||
|
self.add_to_migration(
|
||||||
|
app_label,
|
||||||
|
operations.AddField(
|
||||||
|
model_name = model_name,
|
||||||
|
name = field_name,
|
||||||
|
field = model_state.get_field_by_name(field_name),
|
||||||
|
),
|
||||||
|
new = True,
|
||||||
|
)
|
||||||
|
self.add_dependency(app_label, other_app_label)
|
||||||
# Removing models
|
# Removing models
|
||||||
removed_models = set(self.from_state.models.keys()) - set(self.to_state.models.keys())
|
removed_models = set(self.from_state.models.keys()) - set(self.to_state.models.keys())
|
||||||
for app_label, model_name in removed_models:
|
for app_label, model_name in removed_models:
|
||||||
|
@ -127,16 +195,31 @@ class MigrationAutodetector(object):
|
||||||
for app_label, migrations in self.migrations.items():
|
for app_label, migrations in self.migrations.items():
|
||||||
for m1, m2 in zip(migrations, migrations[1:]):
|
for m1, m2 in zip(migrations, migrations[1:]):
|
||||||
m2.dependencies.append((app_label, m1.name))
|
m2.dependencies.append((app_label, m1.name))
|
||||||
|
# Clean up dependencies
|
||||||
|
for app_label, migrations in self.migrations.items():
|
||||||
|
for migration in migrations:
|
||||||
|
migration.dependencies = list(set(migration.dependencies))
|
||||||
return self.migrations
|
return self.migrations
|
||||||
|
|
||||||
def add_to_migration(self, app_label, operation):
|
def add_to_migration(self, app_label, operation, new=False):
|
||||||
migrations = self.migrations.setdefault(app_label, [])
|
migrations = self.migrations.setdefault(app_label, [])
|
||||||
if not migrations:
|
if not migrations or new:
|
||||||
subclass = type("Migration", (Migration,), {"operations": [], "dependencies": []})
|
subclass = type("Migration", (Migration,), {"operations": [], "dependencies": []})
|
||||||
instance = subclass("auto_%i" % (len(migrations) + 1), app_label)
|
instance = subclass("auto_%i" % (len(migrations) + 1), app_label)
|
||||||
migrations.append(instance)
|
migrations.append(instance)
|
||||||
migrations[-1].operations.append(operation)
|
migrations[-1].operations.append(operation)
|
||||||
|
|
||||||
|
def add_dependency(self, app_label, other_app_label):
|
||||||
|
"""
|
||||||
|
Adds a dependency to app_label's newest migration on
|
||||||
|
other_app_label's latest migration.
|
||||||
|
"""
|
||||||
|
if self.migrations.get(other_app_label, []):
|
||||||
|
dependency = (other_app_label, self.migrations[other_app_label][-1].name)
|
||||||
|
else:
|
||||||
|
dependency = (other_app_label, "__first__")
|
||||||
|
self.migrations[app_label][-1].dependencies.append(dependency)
|
||||||
|
|
||||||
def arrange_for_graph(self, changes, graph):
|
def arrange_for_graph(self, changes, graph):
|
||||||
"""
|
"""
|
||||||
Takes in a result from changes() and a MigrationGraph,
|
Takes in a result from changes() and a MigrationGraph,
|
||||||
|
|
|
@ -70,7 +70,7 @@ class ModelState(object):
|
||||||
"""
|
"""
|
||||||
# Deconstruct the fields
|
# Deconstruct the fields
|
||||||
fields = []
|
fields = []
|
||||||
for field in model._meta.local_fields:
|
for field in model._meta.fields:
|
||||||
name, path, args, kwargs = field.deconstruct()
|
name, path, args, kwargs = field.deconstruct()
|
||||||
field_class = import_by_path(path)
|
field_class = import_by_path(path)
|
||||||
fields.append((name, field_class(*args, **kwargs)))
|
fields.append((name, field_class(*args, **kwargs)))
|
||||||
|
@ -83,12 +83,15 @@ class ModelState(object):
|
||||||
if name in model._meta.original_attrs:
|
if name in model._meta.original_attrs:
|
||||||
options[name] = model._meta.original_attrs[name]
|
options[name] = model._meta.original_attrs[name]
|
||||||
# Make our record
|
# Make our record
|
||||||
|
bases = tuple(model for model in model.__bases__ if (not hasattr(model, "_meta") or not model._meta.abstract))
|
||||||
|
if not bases:
|
||||||
|
bases = (models.Model, )
|
||||||
return cls(
|
return cls(
|
||||||
model._meta.app_label,
|
model._meta.app_label,
|
||||||
model._meta.object_name,
|
model._meta.object_name,
|
||||||
fields,
|
fields,
|
||||||
options,
|
options,
|
||||||
model.__bases__,
|
bases,
|
||||||
)
|
)
|
||||||
|
|
||||||
def clone(self):
|
def clone(self):
|
||||||
|
|
|
@ -15,9 +15,12 @@ class AutodetectorTests(TestCase):
|
||||||
author_name = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200))])
|
author_name = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200))])
|
||||||
author_name_longer = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=400))])
|
author_name_longer = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=400))])
|
||||||
author_name_renamed = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("names", models.CharField(max_length=200))])
|
author_name_renamed = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("names", models.CharField(max_length=200))])
|
||||||
|
author_with_book = ModelState("testapp", "Author", [("id", models.AutoField(primary_key=True)), ("name", models.CharField(max_length=200)), ("book", models.ForeignKey("otherapp.Book"))])
|
||||||
other_pony = ModelState("otherapp", "Pony", [("id", models.AutoField(primary_key=True))])
|
other_pony = ModelState("otherapp", "Pony", [("id", models.AutoField(primary_key=True))])
|
||||||
other_stable = ModelState("otherapp", "Stable", [("id", models.AutoField(primary_key=True))])
|
other_stable = ModelState("otherapp", "Stable", [("id", models.AutoField(primary_key=True))])
|
||||||
third_thing = ModelState("thirdapp", "Thing", [("id", models.AutoField(primary_key=True))])
|
third_thing = ModelState("thirdapp", "Thing", [("id", models.AutoField(primary_key=True))])
|
||||||
|
book = ModelState("otherapp", "Book", [("id", models.AutoField(primary_key=True)), ("author", models.ForeignKey("testapp.Author")), ("title", models.CharField(max_length=200))])
|
||||||
|
edition = ModelState("thirdapp", "Edition", [("id", models.AutoField(primary_key=True)), ("book", models.ForeignKey("otherapp.Book"))])
|
||||||
|
|
||||||
def make_project_state(self, model_states):
|
def make_project_state(self, model_states):
|
||||||
"Shortcut to make ProjectStates from lists of predefined models"
|
"Shortcut to make ProjectStates from lists of predefined models"
|
||||||
|
@ -167,3 +170,67 @@ class AutodetectorTests(TestCase):
|
||||||
self.assertEqual(action.__class__.__name__, "RenameField")
|
self.assertEqual(action.__class__.__name__, "RenameField")
|
||||||
self.assertEqual(action.old_name, "name")
|
self.assertEqual(action.old_name, "name")
|
||||||
self.assertEqual(action.new_name, "names")
|
self.assertEqual(action.new_name, "names")
|
||||||
|
|
||||||
|
def test_fk_dependency(self):
|
||||||
|
"Tests that having a ForeignKey automatically adds a dependency"
|
||||||
|
# Make state
|
||||||
|
before = self.make_project_state([])
|
||||||
|
after = self.make_project_state([self.author_name, self.book, self.edition])
|
||||||
|
autodetector = MigrationAutodetector(before, after)
|
||||||
|
changes = autodetector.changes()
|
||||||
|
# Right number of migrations?
|
||||||
|
self.assertEqual(len(changes['testapp']), 1)
|
||||||
|
self.assertEqual(len(changes['otherapp']), 1)
|
||||||
|
self.assertEqual(len(changes['thirdapp']), 1)
|
||||||
|
# Right number of actions?
|
||||||
|
migration1 = changes['testapp'][0]
|
||||||
|
self.assertEqual(len(migration1.operations), 1)
|
||||||
|
migration2 = changes['otherapp'][0]
|
||||||
|
self.assertEqual(len(migration2.operations), 1)
|
||||||
|
migration3 = changes['thirdapp'][0]
|
||||||
|
self.assertEqual(len(migration3.operations), 1)
|
||||||
|
# Right actions?
|
||||||
|
action = migration1.operations[0]
|
||||||
|
self.assertEqual(action.__class__.__name__, "CreateModel")
|
||||||
|
action = migration2.operations[0]
|
||||||
|
self.assertEqual(action.__class__.__name__, "CreateModel")
|
||||||
|
action = migration3.operations[0]
|
||||||
|
self.assertEqual(action.__class__.__name__, "CreateModel")
|
||||||
|
# Right dependencies?
|
||||||
|
self.assertEqual(migration1.dependencies, [])
|
||||||
|
self.assertEqual(migration2.dependencies, [("testapp", "auto_1")])
|
||||||
|
self.assertEqual(migration3.dependencies, [("otherapp", "auto_1")])
|
||||||
|
|
||||||
|
def test_circular_fk_dependency(self):
|
||||||
|
"""
|
||||||
|
Tests that having a circular ForeignKey dependency automatically
|
||||||
|
resolves the situation into 2 migrations on one side and 1 on the other.
|
||||||
|
"""
|
||||||
|
# Make state
|
||||||
|
before = self.make_project_state([])
|
||||||
|
after = self.make_project_state([self.author_with_book, self.book])
|
||||||
|
autodetector = MigrationAutodetector(before, after)
|
||||||
|
changes = autodetector.changes()
|
||||||
|
# Right number of migrations?
|
||||||
|
self.assertEqual(len(changes['testapp']), 1)
|
||||||
|
self.assertEqual(len(changes['otherapp']), 2)
|
||||||
|
# Right number of actions?
|
||||||
|
migration1 = changes['testapp'][0]
|
||||||
|
self.assertEqual(len(migration1.operations), 1)
|
||||||
|
migration2 = changes['otherapp'][0]
|
||||||
|
self.assertEqual(len(migration2.operations), 1)
|
||||||
|
migration3 = changes['otherapp'][1]
|
||||||
|
self.assertEqual(len(migration2.operations), 1)
|
||||||
|
# Right actions?
|
||||||
|
action = migration1.operations[0]
|
||||||
|
self.assertEqual(action.__class__.__name__, "CreateModel")
|
||||||
|
action = migration2.operations[0]
|
||||||
|
self.assertEqual(action.__class__.__name__, "CreateModel")
|
||||||
|
self.assertEqual(len(action.fields), 2)
|
||||||
|
action = migration3.operations[0]
|
||||||
|
self.assertEqual(action.__class__.__name__, "AddField")
|
||||||
|
self.assertEqual(action.name, "author")
|
||||||
|
# Right dependencies?
|
||||||
|
self.assertEqual(migration1.dependencies, [("otherapp", "auto_1")])
|
||||||
|
self.assertEqual(migration2.dependencies, [])
|
||||||
|
self.assertEqual(set(migration3.dependencies), set([("otherapp", "auto_1"), ("testapp", "auto_1")]))
|
||||||
|
|
Loading…
Reference in New Issue