Loading django/db/migrations/operations/base.py +12 −0 Original line number Diff line number Diff line Loading @@ -64,6 +64,18 @@ class Operation(object): """ return "%s: %s" % (self.__class__.__name__, self._constructor_args) def references_model(self, name, app_label=None): """ Returns True if there is a chance this operation references the given model name (as a string), with an optional app label for accuracy. Used for optimization. If in doubt, return True; returning a false positive will merely make the optimizer a little less efficient, while returning a false negative may result in an unusable optimized migration. """ return True def __repr__(self): return "<%s %s%s>" % ( self.__class__.__name__, Loading django/db/migrations/operations/models.py +30 −0 Original line number Diff line number Diff line from .base import Operation from django.utils import six from django.db import models, router from django.db.models.options import normalize_unique_together from django.db.migrations.state import ModelState Loading Loading @@ -33,6 +34,23 @@ class CreateModel(Operation): def describe(self): return "Create model %s" % (self.name, ) def references_model(self, name, app_label=None): strings_to_check = [self.name] # Check we didn't inherit from the model for base in self.bases: if isinstance(base, six.string_types): strings_to_check.append(base.split(".")[-1]) # Check we have no FKs/M2Ms with it for fname, field in self.fields: if field.rel: if isinstance(field.rel.to, six.string_types): strings_to_check.append(field.rel.to.split(".")[-1]) # Now go over all the strings and compare them for string in strings_to_check: if string.lower() == name.lower(): return True return False def __eq__(self, other): return ( (self.__class__ == other.__class__) and Loading Loading @@ -66,6 +84,9 @@ class DeleteModel(Operation): if router.allow_migrate(schema_editor.connection.alias, model): schema_editor.create_model(model) def references_model(self, name, app_label=None): return name.lower() == self.name.lower() def describe(self): return "Delete model %s" % (self.name, ) Loading Loading @@ -97,6 +118,9 @@ class AlterModelTable(Operation): def database_backwards(self, app_label, schema_editor, from_state, to_state): return self.database_forwards(app_label, schema_editor, from_state, to_state) def references_model(self, name, app_label=None): return name.lower() == self.name.lower() def describe(self): return "Rename table for %s to %s" % (self.name, self.table) Loading Loading @@ -131,6 +155,9 @@ class AlterUniqueTogether(Operation): def database_backwards(self, app_label, schema_editor, from_state, to_state): return self.database_forwards(app_label, schema_editor, from_state, to_state) def references_model(self, name, app_label=None): return name.lower() == self.name.lower() def describe(self): return "Alter unique_together for %s (%s constraints)" % (self.name, len(self.unique_together)) Loading Loading @@ -164,5 +191,8 @@ class AlterIndexTogether(Operation): def database_backwards(self, app_label, schema_editor, from_state, to_state): return self.database_forwards(app_label, schema_editor, from_state, to_state) def references_model(self, name, app_label=None): return name.lower() == self.name.lower() def describe(self): return "Alter index_together for %s (%s constraints)" % (self.name, len(self.index_together)) django/db/migrations/optimizer.py +20 −5 Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ class MigrationOptimizer(object): nothing. """ def optimize(self, operations): def optimize(self, operations, app_label=None): """ Main optimization entry point. Pass in a list of Operation instances, get out a new list of Operation instances. Loading @@ -27,17 +27,20 @@ class MigrationOptimizer(object): The inner loop is run until the starting list is the same as the result list, and then the result is returned. This means that operation optimization must be stable and always return an equal or shorter list. The app_label argument is optional, but if you pass it you'll get more efficient optimization. """ # Internal tracking variable for test assertions about # of loops self._iterations = 0 while True: result = self.optimize_inner(operations) result = self.optimize_inner(operations, app_label) self._iterations += 1 if result == operations: return result operations = result def optimize_inner(self, operations): def optimize_inner(self, operations, app_label=None): """ Inner optimization loop. """ Loading @@ -52,7 +55,7 @@ class MigrationOptimizer(object): new_operations.extend(operations[i+1:i+1+j]) new_operations.extend(operations[i+j+2:]) return new_operations if not self.can_optimize_through(operation, other): if not self.can_optimize_through(operation, other, app_label): new_operations.append(operation) break else: Loading Loading @@ -95,10 +98,22 @@ class MigrationOptimizer(object): #### THROUGH CHECKS #### def can_optimize_through(self, operation, other): def can_optimize_through(self, operation, other, app_label=None): """ Returns True if it's possible to optimize 'operation' with something the other side of 'other'. This is possible if, for example, they affect different models. """ MODEL_LEVEL_OPERATIONS = ( migrations.CreateModel, migrations.DeleteModel, migrations.AlterModelTable, migrations.AlterUniqueTogether, migrations.AlterIndexTogether, ) # If it's a model level operation, let it through if there's # nothing that looks like a reference to us in 'other'. if isinstance(operation, MODEL_LEVEL_OPERATIONS): if not other.references_model(operation.name, app_label): return True return False tests/migrations/test_optimizer.py +62 −0 Original line number Diff line number Diff line Loading @@ -93,3 +93,65 @@ class OptimizerTests(TestCase): ], [], ) def test_optimize_through_create(self): """ We should be able to optimize away create/delete through a create or delete of a different model, but only if the create operation does not mention the model at all. """ # These should work self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Bar", [("size", models.IntegerField())]), ], ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Bar"), migrations.DeleteModel("Foo"), ], [], ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Foo"), migrations.DeleteModel("Bar"), ], [], ) # This should not work - FK should block it self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo"))]), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo"))]), migrations.DeleteModel("Foo"), ], ) # This should not work - bases should block it self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo", )), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo", )), migrations.DeleteModel("Foo"), ], ) Loading
django/db/migrations/operations/base.py +12 −0 Original line number Diff line number Diff line Loading @@ -64,6 +64,18 @@ class Operation(object): """ return "%s: %s" % (self.__class__.__name__, self._constructor_args) def references_model(self, name, app_label=None): """ Returns True if there is a chance this operation references the given model name (as a string), with an optional app label for accuracy. Used for optimization. If in doubt, return True; returning a false positive will merely make the optimizer a little less efficient, while returning a false negative may result in an unusable optimized migration. """ return True def __repr__(self): return "<%s %s%s>" % ( self.__class__.__name__, Loading
django/db/migrations/operations/models.py +30 −0 Original line number Diff line number Diff line from .base import Operation from django.utils import six from django.db import models, router from django.db.models.options import normalize_unique_together from django.db.migrations.state import ModelState Loading Loading @@ -33,6 +34,23 @@ class CreateModel(Operation): def describe(self): return "Create model %s" % (self.name, ) def references_model(self, name, app_label=None): strings_to_check = [self.name] # Check we didn't inherit from the model for base in self.bases: if isinstance(base, six.string_types): strings_to_check.append(base.split(".")[-1]) # Check we have no FKs/M2Ms with it for fname, field in self.fields: if field.rel: if isinstance(field.rel.to, six.string_types): strings_to_check.append(field.rel.to.split(".")[-1]) # Now go over all the strings and compare them for string in strings_to_check: if string.lower() == name.lower(): return True return False def __eq__(self, other): return ( (self.__class__ == other.__class__) and Loading Loading @@ -66,6 +84,9 @@ class DeleteModel(Operation): if router.allow_migrate(schema_editor.connection.alias, model): schema_editor.create_model(model) def references_model(self, name, app_label=None): return name.lower() == self.name.lower() def describe(self): return "Delete model %s" % (self.name, ) Loading Loading @@ -97,6 +118,9 @@ class AlterModelTable(Operation): def database_backwards(self, app_label, schema_editor, from_state, to_state): return self.database_forwards(app_label, schema_editor, from_state, to_state) def references_model(self, name, app_label=None): return name.lower() == self.name.lower() def describe(self): return "Rename table for %s to %s" % (self.name, self.table) Loading Loading @@ -131,6 +155,9 @@ class AlterUniqueTogether(Operation): def database_backwards(self, app_label, schema_editor, from_state, to_state): return self.database_forwards(app_label, schema_editor, from_state, to_state) def references_model(self, name, app_label=None): return name.lower() == self.name.lower() def describe(self): return "Alter unique_together for %s (%s constraints)" % (self.name, len(self.unique_together)) Loading Loading @@ -164,5 +191,8 @@ class AlterIndexTogether(Operation): def database_backwards(self, app_label, schema_editor, from_state, to_state): return self.database_forwards(app_label, schema_editor, from_state, to_state) def references_model(self, name, app_label=None): return name.lower() == self.name.lower() def describe(self): return "Alter index_together for %s (%s constraints)" % (self.name, len(self.index_together))
django/db/migrations/optimizer.py +20 −5 Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ class MigrationOptimizer(object): nothing. """ def optimize(self, operations): def optimize(self, operations, app_label=None): """ Main optimization entry point. Pass in a list of Operation instances, get out a new list of Operation instances. Loading @@ -27,17 +27,20 @@ class MigrationOptimizer(object): The inner loop is run until the starting list is the same as the result list, and then the result is returned. This means that operation optimization must be stable and always return an equal or shorter list. The app_label argument is optional, but if you pass it you'll get more efficient optimization. """ # Internal tracking variable for test assertions about # of loops self._iterations = 0 while True: result = self.optimize_inner(operations) result = self.optimize_inner(operations, app_label) self._iterations += 1 if result == operations: return result operations = result def optimize_inner(self, operations): def optimize_inner(self, operations, app_label=None): """ Inner optimization loop. """ Loading @@ -52,7 +55,7 @@ class MigrationOptimizer(object): new_operations.extend(operations[i+1:i+1+j]) new_operations.extend(operations[i+j+2:]) return new_operations if not self.can_optimize_through(operation, other): if not self.can_optimize_through(operation, other, app_label): new_operations.append(operation) break else: Loading Loading @@ -95,10 +98,22 @@ class MigrationOptimizer(object): #### THROUGH CHECKS #### def can_optimize_through(self, operation, other): def can_optimize_through(self, operation, other, app_label=None): """ Returns True if it's possible to optimize 'operation' with something the other side of 'other'. This is possible if, for example, they affect different models. """ MODEL_LEVEL_OPERATIONS = ( migrations.CreateModel, migrations.DeleteModel, migrations.AlterModelTable, migrations.AlterUniqueTogether, migrations.AlterIndexTogether, ) # If it's a model level operation, let it through if there's # nothing that looks like a reference to us in 'other'. if isinstance(operation, MODEL_LEVEL_OPERATIONS): if not other.references_model(operation.name, app_label): return True return False
tests/migrations/test_optimizer.py +62 −0 Original line number Diff line number Diff line Loading @@ -93,3 +93,65 @@ class OptimizerTests(TestCase): ], [], ) def test_optimize_through_create(self): """ We should be able to optimize away create/delete through a create or delete of a different model, but only if the create operation does not mention the model at all. """ # These should work self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Bar", [("size", models.IntegerField())]), ], ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Bar"), migrations.DeleteModel("Foo"), ], [], ) self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())]), migrations.DeleteModel("Foo"), migrations.DeleteModel("Bar"), ], [], ) # This should not work - FK should block it self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo"))]), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("other", models.ForeignKey("testapp.Foo"))]), migrations.DeleteModel("Foo"), ], ) # This should not work - bases should block it self.assertOptimizesTo( [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo", )), migrations.DeleteModel("Foo"), ], [ migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), migrations.CreateModel("Bar", [("size", models.IntegerField())], bases=("testapp.Foo", )), migrations.DeleteModel("Foo"), ], )