mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Initial version of MigrationOptimizer and tests
This commit is contained in:
		| @@ -63,3 +63,16 @@ class Operation(object): | ||||
|         Outputs a brief summary of what the action does. | ||||
|         """ | ||||
|         return "%s: %s" % (self.__class__.__name__, self._constructor_args) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return "<%s %s%s>" % ( | ||||
|             self.__class__.__name__, | ||||
|             ", ".join(map(repr, self._constructor_args[0])), | ||||
|             ",".join(" %s=%r" % x for x in self._constructor_args[1].items()), | ||||
|         ) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return (self.__class__ == other.__class__) and (self.deconstruct() == other.deconstruct()) | ||||
|  | ||||
|     def __ne__(self, other): | ||||
|         return not (self == other) | ||||
|   | ||||
| @@ -29,6 +29,14 @@ class AddField(Operation): | ||||
|     def describe(self): | ||||
|         return "Add field %s to %s" % (self.name, self.model_name) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return ( | ||||
|             (self.__class__ == other.__class__) and | ||||
|             (self.name == other.name) and | ||||
|             (self.model_name == other.model_name) and | ||||
|             (self.field.deconstruct()[1:] == other.field.deconstruct()[1:]) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class RemoveField(Operation): | ||||
|     """ | ||||
| @@ -92,6 +100,14 @@ class AlterField(Operation): | ||||
|     def describe(self): | ||||
|         return "Alter field %s on %s" % (self.name, self.model_name) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return ( | ||||
|             (self.__class__ == other.__class__) and | ||||
|             (self.name == other.name) and | ||||
|             (self.model_name == other.model_name) and | ||||
|             (self.field.deconstruct()[1:] == other.field.deconstruct()[1:]) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class RenameField(Operation): | ||||
|     """ | ||||
|   | ||||
| @@ -32,6 +32,15 @@ class CreateModel(Operation): | ||||
|     def describe(self): | ||||
|         return "Create model %s" % (self.name, ) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         return ( | ||||
|             (self.__class__ == other.__class__) and | ||||
|             (self.name == other.name) and | ||||
|             (self.options == other.options) and | ||||
|             (self.bases == other.bases) and | ||||
|             ([(k, f.deconstruct()[1:]) for k, f in self.fields] == [(k, f.deconstruct()[1:]) for k, f in other.fields]) | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class DeleteModel(Operation): | ||||
|     """ | ||||
|   | ||||
							
								
								
									
										104
									
								
								django/db/migrations/optimizer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								django/db/migrations/optimizer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| from django.db import migrations | ||||
|  | ||||
| class MigrationOptimizer(object): | ||||
|     """ | ||||
|     Powers the optimization process, where you provide a list of Operations | ||||
|     and you are returned a list of equal or shorter length - operations | ||||
|     are merged into one if possible. | ||||
|  | ||||
|     For example, a CreateModel and an AddField can be optimised into a | ||||
|     new CreateModel, and CreateModel and DeleteModel can be optimised into | ||||
|     nothing. | ||||
|     """ | ||||
|  | ||||
|     def optimize(self, operations): | ||||
|         """ | ||||
|         Main optimization entry point. Pass in a list of Operation instances, | ||||
|         get out a new list of Operation instances. | ||||
|  | ||||
|         Unfortunately, due to the scope of the optimisation (two combinable | ||||
|         operations might be separated by several hundred others), this can't be | ||||
|         done as a peephole optimisation with checks/output implemented on | ||||
|         the Operations themselves; instead, the optimizer looks at each | ||||
|         individual operation and scans forwards in the list to see if there | ||||
|         are any matches, stopping at boundaries - operations which can't | ||||
|         be optimized over (RunSQL, operations on the same field/model, etc.) | ||||
|  | ||||
|         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. | ||||
|         """ | ||||
|         # Internal tracking variable for test assertions about # of loops | ||||
|         self._iterations = 0 | ||||
|         while True: | ||||
|             result = self.optimize_inner(operations) | ||||
|             self._iterations += 1 | ||||
|             if result == operations: | ||||
|                 return result | ||||
|             operations = result | ||||
|  | ||||
|     def optimize_inner(self, operations): | ||||
|         """ | ||||
|         Inner optimization loop. | ||||
|         """ | ||||
|         new_operations = [] | ||||
|         for i, operation in enumerate(operations): | ||||
|             # Compare it to each operation after it | ||||
|             for j, other in enumerate(operations[i+1:]): | ||||
|                 result = self.reduce(operation, other) | ||||
|                 if result is not None: | ||||
|                     # Optimize! Add result, then remaining others, then return | ||||
|                     new_operations.extend(result) | ||||
|                     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): | ||||
|                     new_operations.append(operation) | ||||
|                     break | ||||
|             else: | ||||
|                 new_operations.append(operation) | ||||
|         return new_operations | ||||
|  | ||||
|     #### REDUCTION #### | ||||
|  | ||||
|     def reduce(self, operation, other): | ||||
|         """ | ||||
|         Either returns a list of zero, one or two operations, | ||||
|         or None, meaning this pair cannot be optimized. | ||||
|         """ | ||||
|         submethods = [ | ||||
|             (migrations.CreateModel, migrations.DeleteModel, self.reduce_model_create_delete), | ||||
|             (migrations.AlterModelTable, migrations.DeleteModel, self.reduce_model_alter_delete), | ||||
|             (migrations.AlterUniqueTogether, migrations.DeleteModel, self.reduce_model_alter_delete), | ||||
|             (migrations.AlterIndexTogether, migrations.DeleteModel, self.reduce_model_alter_delete), | ||||
|         ] | ||||
|         for ia, ib, om in submethods: | ||||
|             if isinstance(operation, ia) and isinstance(other, ib): | ||||
|                 return om(operation, other) | ||||
|         return None | ||||
|  | ||||
|     def reduce_model_create_delete(self, operation, other): | ||||
|         """ | ||||
|         Folds a CreateModel and a DeleteModel into nothing. | ||||
|         """ | ||||
|         if operation.name == other.name: | ||||
|             return [] | ||||
|         return None | ||||
|  | ||||
|     def reduce_model_alter_delete(self, operation, other): | ||||
|         """ | ||||
|         Folds an AlterModelSomething and a DeleteModel into nothing. | ||||
|         """ | ||||
|         if operation.name == other.name: | ||||
|             return [other] | ||||
|         return None | ||||
|  | ||||
|     #### THROUGH CHECKS #### | ||||
|  | ||||
|     def can_optimize_through(self, operation, other): | ||||
|         """ | ||||
|         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. | ||||
|         """ | ||||
|         return False | ||||
							
								
								
									
										95
									
								
								tests/migrations/test_optimizer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								tests/migrations/test_optimizer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| # encoding: utf8 | ||||
| import operator | ||||
| from django.test import TestCase | ||||
| from django.db.migrations.optimizer import MigrationOptimizer | ||||
| from django.db import migrations | ||||
| from django.db import models | ||||
|  | ||||
|  | ||||
| class OptimizerTests(TestCase): | ||||
|     """ | ||||
|     Tests the migration autodetector. | ||||
|     """ | ||||
|  | ||||
|     def optimize(self, operations): | ||||
|         """ | ||||
|         Handy shortcut for getting results + number of loops | ||||
|         """ | ||||
|         optimizer = MigrationOptimizer() | ||||
|         return optimizer.optimize(operations), optimizer._iterations | ||||
|  | ||||
|     def assertOptimizesTo(self, operations, expected, exact=None, less_than=None): | ||||
|         result, iterations = self.optimize(operations) | ||||
|         self.assertEqual(expected, result) | ||||
|         if exact is not None and iterations != exact: | ||||
|             raise self.failureException("Optimization did not take exactly %s iterations (it took %s)" % (exact, iterations)) | ||||
|         if less_than is not None and iterations >= less_than: | ||||
|             raise self.failureException("Optimization did not take less than %s iterations (it took %s)" % (less_than, iterations)) | ||||
|  | ||||
|     def test_operation_equality(self): | ||||
|         """ | ||||
|         Tests the equality operator on lists of operations. | ||||
|         If this is broken, then the optimizer will get stuck in an | ||||
|         infinite loop, so it's kind of important. | ||||
|         """ | ||||
|         self.assertEqual( | ||||
|             [migrations.DeleteModel("Test")], | ||||
|             [migrations.DeleteModel("Test")], | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             [migrations.CreateModel("Test", [("name", models.CharField(max_length=255))])], | ||||
|             [migrations.CreateModel("Test", [("name", models.CharField(max_length=255))])], | ||||
|         ) | ||||
|         self.assertNotEqual( | ||||
|             [migrations.CreateModel("Test", [("name", models.CharField(max_length=255))])], | ||||
|             [migrations.CreateModel("Test", [("name", models.CharField(max_length=100))])], | ||||
|         ) | ||||
|         self.assertEqual( | ||||
|             [migrations.AddField("Test", "name", models.CharField(max_length=255))], | ||||
|             [migrations.AddField("Test", "name", models.CharField(max_length=255))], | ||||
|         ) | ||||
|         self.assertNotEqual( | ||||
|             [migrations.AddField("Test", "name", models.CharField(max_length=255))], | ||||
|             [migrations.AddField("Test", "name", models.CharField(max_length=100))], | ||||
|         ) | ||||
|         self.assertNotEqual( | ||||
|             [migrations.AddField("Test", "name", models.CharField(max_length=255))], | ||||
|             [migrations.AlterField("Test", "name", models.CharField(max_length=255))], | ||||
|         ) | ||||
|  | ||||
|     def test_single(self): | ||||
|         """ | ||||
|         Tests that the optimizer does nothing on a single operation, | ||||
|         and that it does it in just one pass. | ||||
|         """ | ||||
|         self.assertOptimizesTo( | ||||
|             [migrations.DeleteModel("Foo")], | ||||
|             [migrations.DeleteModel("Foo")], | ||||
|             exact = 1, | ||||
|         ) | ||||
|  | ||||
|     def test_create_delete_model(self): | ||||
|         """ | ||||
|         CreateModel and DeleteModel should collapse into nothing. | ||||
|         """ | ||||
|         self.assertOptimizesTo( | ||||
|             [ | ||||
|                 migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), | ||||
|                 migrations.DeleteModel("Foo"), | ||||
|             ], | ||||
|             [], | ||||
|         ) | ||||
|  | ||||
|     def test_create_alter_delete_model(self): | ||||
|         """ | ||||
|         CreateModel, AlterModelTable, AlterUniqueTogether, and DeleteModel should collapse into nothing. | ||||
|         """ | ||||
|         self.assertOptimizesTo( | ||||
|             [ | ||||
|                 migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]), | ||||
|                 migrations.AlterModelTable("Foo", "woohoo"), | ||||
|                 migrations.AlterUniqueTogether("Foo", [["a", "b"]]), | ||||
|                 migrations.DeleteModel("Foo"), | ||||
|             ], | ||||
|             [], | ||||
|         ) | ||||
		Reference in New Issue
	
	Block a user