summaryrefslogtreecommitdiff
path: root/django/db/migrations/optimizer.py
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2013-10-02 17:33:41 +0100
committerAndrew Godwin <andrew@aeracode.org>2013-10-02 17:34:22 +0100
commita80d9ab0fe25abdf80fd78849ddf5dd977f14316 (patch)
tree009d2e019a774d1e6748a0d05aa531299695be6a /django/db/migrations/optimizer.py
parent75bb6ba96660e2a06e18d99120c05db2bb9fa9cc (diff)
downloaddjango-a80d9ab0fe25abdf80fd78849ddf5dd977f14316.tar.gz
Initial version of MigrationOptimizer and tests
Diffstat (limited to 'django/db/migrations/optimizer.py')
-rw-r--r--django/db/migrations/optimizer.py104
1 files changed, 104 insertions, 0 deletions
diff --git a/django/db/migrations/optimizer.py b/django/db/migrations/optimizer.py
new file mode 100644
index 0000000000..9c9613bb37
--- /dev/null
+++ b/django/db/migrations/optimizer.py
@@ -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