Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Autodetect ForeignKeys and add dependencies/split on circulars

  • Loading branch information...
commit e2d7e83256234251a81ad3388428f6579795a672 1 parent 48493cf
@andrewgodwin andrewgodwin authored
View
101 django/db/migrations/autodetector.py
@@ -36,19 +36,87 @@ def changes(self):
"""
# We'll store migrations as lists by app names for now
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())
+ pending_add = {}
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]
+ self.add_to_migration(
+ app_label,
+ operations.CreateModel(
+ name = model_state.name,
+ fields = model_state.fields,
+ options = model_state.options,
+ 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.CreateModel(
- name = model_state.name,
- fields = model_state.fields,
- options = model_state.options,
- bases = model_state.bases,
- )
+ 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
removed_models = set(self.from_state.models.keys()) - set(self.to_state.models.keys())
for app_label, model_name in removed_models:
@@ -127,16 +195,31 @@ def changes(self):
for app_label, migrations in self.migrations.items():
for m1, m2 in zip(migrations, migrations[1:]):
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
- def add_to_migration(self, app_label, operation):
+ def add_to_migration(self, app_label, operation, new=False):
migrations = self.migrations.setdefault(app_label, [])
- if not migrations:
+ if not migrations or new:
subclass = type("Migration", (Migration,), {"operations": [], "dependencies": []})
instance = subclass("auto_%i" % (len(migrations) + 1), app_label)
migrations.append(instance)
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):
"""
Takes in a result from changes() and a MigrationGraph,
View
7 django/db/migrations/state.py
@@ -70,7 +70,7 @@ def from_model(cls, model):
"""
# Deconstruct the fields
fields = []
- for field in model._meta.local_fields:
+ for field in model._meta.fields:
name, path, args, kwargs = field.deconstruct()
field_class = import_by_path(path)
fields.append((name, field_class(*args, **kwargs)))
@@ -83,12 +83,15 @@ def from_model(cls, model):
if name in model._meta.original_attrs:
options[name] = model._meta.original_attrs[name]
# 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(
model._meta.app_label,
model._meta.object_name,
fields,
options,
- model.__bases__,
+ bases,
)
def clone(self):
View
67 tests/migrations/test_autodetector.py
@@ -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_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_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_stable = ModelState("otherapp", "Stable", [("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):
"Shortcut to make ProjectStates from lists of predefined models"
@@ -167,3 +170,67 @@ def test_rename_field(self):
self.assertEqual(action.__class__.__name__, "RenameField")
self.assertEqual(action.old_name, "name")
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")]))
Please sign in to comment.
Something went wrong with that request. Please try again.