Loading django/db/migrations/autodetector.py +92 −9 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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: Loading Loading @@ -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, Loading django/db/migrations/state.py +5 −2 Original line number Diff line number Diff line Loading @@ -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))) Loading @@ -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): Loading tests/migrations/test_autodetector.py +67 −0 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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")])) Loading
django/db/migrations/autodetector.py +92 −9 Original line number Diff line number Diff line Loading @@ -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, Loading @@ -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: Loading Loading @@ -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, Loading
django/db/migrations/state.py +5 −2 Original line number Diff line number Diff line Loading @@ -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))) Loading @@ -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): Loading
tests/migrations/test_autodetector.py +67 −0 Original line number Diff line number Diff line Loading @@ -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" Loading Loading @@ -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")]))