mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Autodetect ForeignKeys and add dependencies/split on circulars
This commit is contained in:
		| @@ -36,9 +36,41 @@ class MigrationAutodetector(object): | ||||
|         """ | ||||
|         # 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, | ||||
| @@ -49,6 +81,42 @@ class MigrationAutodetector(object): | ||||
|                         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 | ||||
|         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 @@ class MigrationAutodetector(object): | ||||
|         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, | ||||
|   | ||||
| @@ -70,7 +70,7 @@ class ModelState(object): | ||||
|         """ | ||||
|         # 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 @@ class ModelState(object): | ||||
|             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): | ||||
|   | ||||
| @@ -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 @@ class AutodetectorTests(TestCase): | ||||
|         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")])) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user