Kaydet (Commit) 106b019d authored tarafından Andrew Godwin's avatar Andrew Godwin

Massive migration optimiser improvements + RenameModel opn

üst 8265323c
from .models import (CreateModel, DeleteModel, AlterModelTable, from .models import (CreateModel, DeleteModel, AlterModelTable,
AlterUniqueTogether, AlterIndexTogether) AlterUniqueTogether, AlterIndexTogether, RenameModel)
from .fields import AddField, RemoveField, AlterField, RenameField from .fields import AddField, RemoveField, AlterField, RenameField
from .special import SeparateDatabaseAndState, RunSQL, RunPython from .special import SeparateDatabaseAndState, RunSQL, RunPython
__all__ = [ __all__ = [
'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether', 'CreateModel', 'DeleteModel', 'AlterModelTable', 'AlterUniqueTogether',
'AlterIndexTogether', 'RenameModel', 'AlterIndexTogether',
'AddField', 'RemoveField', 'AlterField', 'RenameField', 'AddField', 'RemoveField', 'AlterField', 'RenameField',
'SeparateDatabaseAndState', 'RunSQL', 'RunPython', 'SeparateDatabaseAndState', 'RunSQL', 'RunPython',
] ]
...@@ -9,6 +9,9 @@ class Operation(object): ...@@ -9,6 +9,9 @@ class Operation(object):
Note that some operations won't modify memory state at all (e.g. data Note that some operations won't modify memory state at all (e.g. data
copying operations), and some will need their modifications to be copying operations), and some will need their modifications to be
optionally specified by the user (e.g. custom Python code snippets) optionally specified by the user (e.g. custom Python code snippets)
Due to the way this class deals with deconstruction, it should be
considered immutable.
""" """
# If this migration can be run in reverse. # If this migration can be run in reverse.
...@@ -76,6 +79,15 @@ class Operation(object): ...@@ -76,6 +79,15 @@ class Operation(object):
""" """
return True return True
def references_field(self, model_name, name, app_label=None):
"""
Returns True if there is a chance this operation references the given
field name, with an optional app label for accuracy.
Used for optimization. If in doubt, return True.
"""
return self.references_model(model_name, app_label)
def __repr__(self): def __repr__(self):
return "<%s %s%s>" % ( return "<%s %s%s>" % (
self.__class__.__name__, self.__class__.__name__,
......
...@@ -8,7 +8,7 @@ class AddField(Operation): ...@@ -8,7 +8,7 @@ class AddField(Operation):
""" """
def __init__(self, model_name, name, field): def __init__(self, model_name, name, field):
self.model_name = model_name.lower() self.model_name = model_name
self.name = name self.name = name
self.field = field self.field = field
...@@ -33,10 +33,16 @@ class AddField(Operation): ...@@ -33,10 +33,16 @@ class AddField(Operation):
return ( return (
(self.__class__ == other.__class__) and (self.__class__ == other.__class__) and
(self.name == other.name) and (self.name == other.name) and
(self.model_name == other.model_name) and (self.model_name.lower() == other.model_name.lower()) and
(self.field.deconstruct()[1:] == other.field.deconstruct()[1:]) (self.field.deconstruct()[1:] == other.field.deconstruct()[1:])
) )
def references_model(self, name, app_label=None):
return name.lower() == self.model_name.lower()
def references_field(self, model_name, name, app_label=None):
return self.references_model(model_name) and name.lower() == self.name.lower()
class RemoveField(Operation): class RemoveField(Operation):
""" """
...@@ -44,7 +50,7 @@ class RemoveField(Operation): ...@@ -44,7 +50,7 @@ class RemoveField(Operation):
""" """
def __init__(self, model_name, name): def __init__(self, model_name, name):
self.model_name = model_name.lower() self.model_name = model_name
self.name = name self.name = name
def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
...@@ -68,6 +74,12 @@ class RemoveField(Operation): ...@@ -68,6 +74,12 @@ class RemoveField(Operation):
def describe(self): def describe(self):
return "Remove field %s from %s" % (self.name, self.model_name) return "Remove field %s from %s" % (self.name, self.model_name)
def references_model(self, name, app_label=None):
return name.lower() == self.model_name.lower()
def references_field(self, model_name, name, app_label=None):
return self.references_model(model_name) and name.lower() == self.name.lower()
class AlterField(Operation): class AlterField(Operation):
""" """
...@@ -75,7 +87,7 @@ class AlterField(Operation): ...@@ -75,7 +87,7 @@ class AlterField(Operation):
""" """
def __init__(self, model_name, name, field): def __init__(self, model_name, name, field):
self.model_name = model_name.lower() self.model_name = model_name
self.name = name self.name = name
self.field = field self.field = field
...@@ -104,10 +116,16 @@ class AlterField(Operation): ...@@ -104,10 +116,16 @@ class AlterField(Operation):
return ( return (
(self.__class__ == other.__class__) and (self.__class__ == other.__class__) and
(self.name == other.name) and (self.name == other.name) and
(self.model_name == other.model_name) and (self.model_name.lower() == other.model_name.lower()) and
(self.field.deconstruct()[1:] == other.field.deconstruct()[1:]) (self.field.deconstruct()[1:] == other.field.deconstruct()[1:])
) )
def references_model(self, name, app_label=None):
return name.lower() == self.model_name.lower()
def references_field(self, model_name, name, app_label=None):
return self.references_model(model_name) and name.lower() == self.name.lower()
class RenameField(Operation): class RenameField(Operation):
""" """
...@@ -115,7 +133,7 @@ class RenameField(Operation): ...@@ -115,7 +133,7 @@ class RenameField(Operation):
""" """
def __init__(self, model_name, old_name, new_name): def __init__(self, model_name, old_name, new_name):
self.model_name = model_name.lower() self.model_name = model_name
self.old_name = old_name self.old_name = old_name
self.new_name = new_name self.new_name = new_name
...@@ -146,3 +164,12 @@ class RenameField(Operation): ...@@ -146,3 +164,12 @@ class RenameField(Operation):
def describe(self): def describe(self):
return "Rename field %s on %s to %s" % (self.old_name, self.model_name, self.new_name) return "Rename field %s on %s to %s" % (self.old_name, self.model_name, self.new_name)
def references_model(self, name, app_label=None):
return name.lower() == self.model_name.lower()
def references_field(self, model_name, name, app_label=None):
return self.references_model(model_name) and (
name.lower() == self.old_name.lower() or
name.lower() == self.new_name.lower()
)
...@@ -91,6 +91,54 @@ class DeleteModel(Operation): ...@@ -91,6 +91,54 @@ class DeleteModel(Operation):
return "Delete model %s" % (self.name, ) return "Delete model %s" % (self.name, )
class RenameModel(Operation):
"""
Renames a model.
"""
def __init__(self, old_name, new_name):
self.old_name = old_name
self.new_name = new_name
def state_forwards(self, app_label, state):
state.models[app_label, self.new_name.lower()] = state.models[app_label, self.old_name.lower()]
state.models[app_label, self.new_name.lower()].name = self.new_name
del state.models[app_label, self.old_name.lower()]
def database_forwards(self, app_label, schema_editor, from_state, to_state):
old_app_cache = from_state.render()
new_app_cache = to_state.render()
old_model = old_app_cache.get_model(app_label, self.old_name)
new_model = new_app_cache.get_model(app_label, self.new_name)
if router.allow_migrate(schema_editor.connection.alias, new_model):
schema_editor.alter_db_table(
new_model,
old_model._meta.db_table,
new_model._meta.db_table,
)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
old_app_cache = from_state.render()
new_app_cache = to_state.render()
old_model = old_app_cache.get_model(app_label, self.new_name)
new_model = new_app_cache.get_model(app_label, self.old_name)
if router.allow_migrate(schema_editor.connection.alias, new_model):
schema_editor.alter_db_table(
new_model,
old_model._meta.db_table,
new_model._meta.db_table,
)
def references_model(self, name, app_label=None):
return (
name.lower() == self.old_name.lower() or
name.lower() == self.new_name.lower()
)
def describe(self):
return "Rename model %s to %s" % (self.old_name, self.new_name)
class AlterModelTable(Operation): class AlterModelTable(Operation):
""" """
Renames a model's table Renames a model's table
......
...@@ -71,10 +71,86 @@ class MigrationOptimizer(object): ...@@ -71,10 +71,86 @@ class MigrationOptimizer(object):
or None, meaning this pair cannot be optimized. or None, meaning this pair cannot be optimized.
""" """
submethods = [ submethods = [
(migrations.CreateModel, migrations.DeleteModel, self.reduce_model_create_delete), (
(migrations.AlterModelTable, migrations.DeleteModel, self.reduce_model_alter_delete), migrations.CreateModel,
(migrations.AlterUniqueTogether, migrations.DeleteModel, self.reduce_model_alter_delete), migrations.DeleteModel,
(migrations.AlterIndexTogether, migrations.DeleteModel, self.reduce_model_alter_delete), 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,
),
(
migrations.CreateModel,
migrations.RenameModel,
self.reduce_model_create_rename,
),
(
migrations.RenameModel,
migrations.RenameModel,
self.reduce_model_rename_self,
),
(
migrations.CreateModel,
migrations.AddField,
self.reduce_create_model_add_field,
),
(
migrations.CreateModel,
migrations.AlterField,
self.reduce_create_model_alter_field,
),
(
migrations.CreateModel,
migrations.RemoveField,
self.reduce_create_model_remove_field,
),
(
migrations.AddField,
migrations.AlterField,
self.reduce_add_field_alter_field,
),
(
migrations.AddField,
migrations.RemoveField,
self.reduce_add_field_delete_field,
),
(
migrations.AlterField,
migrations.RemoveField,
self.reduce_alter_field_delete_field,
),
(
migrations.AddField,
migrations.RenameField,
self.reduce_add_field_rename_field,
),
(
migrations.AlterField,
migrations.RenameField,
self.reduce_alter_field_rename_field,
),
(
migrations.CreateModel,
migrations.RenameField,
self.reduce_create_model_rename_field,
),
(
migrations.RenameField,
migrations.RenameField,
self.reduce_rename_field_self,
),
] ]
for ia, ib, om in submethods: for ia, ib, om in submethods:
if isinstance(operation, ia) and isinstance(other, ib): if isinstance(operation, ia) and isinstance(other, ib):
...@@ -85,17 +161,130 @@ class MigrationOptimizer(object): ...@@ -85,17 +161,130 @@ class MigrationOptimizer(object):
""" """
Folds a CreateModel and a DeleteModel into nothing. Folds a CreateModel and a DeleteModel into nothing.
""" """
if operation.name == other.name: if operation.name.lower() == other.name.lower():
return [] return []
return None
def reduce_model_alter_delete(self, operation, other): def reduce_model_alter_delete(self, operation, other):
""" """
Folds an AlterModelSomething and a DeleteModel into nothing. Folds an AlterModelSomething and a DeleteModel into just delete.
""" """
if operation.name == other.name: if operation.name.lower() == other.name.lower():
return [other] return [other]
return None
def reduce_model_create_rename(self, operation, other):
"""
Folds a model rename into its create
"""
if operation.name.lower() == other.old_name.lower():
return [migrations.CreateModel(
other.new_name,
fields = operation.fields,
options = operation.options,
bases = operation.bases,
)]
def reduce_model_rename_self(self, operation, other):
"""
Folds a model rename into another one
"""
if operation.new_name.lower() == other.old_name.lower():
return [
migrations.RenameModel(
operation.old_name,
other.new_name,
)
]
def reduce_create_model_add_field(self, operation, other):
if operation.name.lower() == other.model_name.lower():
return [migrations.CreateModel(
operation.name,
fields = operation.fields + [(other.name, other.field)],
options = operation.options,
bases = operation.bases,
)]
def reduce_create_model_alter_field(self, operation, other):
if operation.name.lower() == other.model_name.lower():
return [migrations.CreateModel(
operation.name,
fields = [
(n, other.field if n == other.name else v)
for n, v in operation.fields
],
options = operation.options,
bases = operation.bases,
)]
def reduce_create_model_rename_field(self, operation, other):
if operation.name.lower() == other.model_name.lower():
return [migrations.CreateModel(
operation.name,
fields = [
(other.new_name if n == other.old_name else n, v)
for n, v in operation.fields
],
options = operation.options,
bases = operation.bases,
)]
def reduce_create_model_remove_field(self, operation, other):
if operation.name.lower() == other.model_name.lower():
return [migrations.CreateModel(
operation.name,
fields = [
(n, v)
for n, v in operation.fields
if n.lower() != other.name.lower()
],
options = operation.options,
bases = operation.bases,
)]
def reduce_add_field_alter_field(self, operation, other):
if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower():
return [migrations.AddField(
model_name = operation.model_name,
name = operation.name,
field = other.field,
)]
def reduce_add_field_delete_field(self, operation, other):
if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower():
return []
def reduce_alter_field_delete_field(self, operation, other):
if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.name.lower():
return [other]
def reduce_add_field_rename_field(self, operation, other):
if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.old_name.lower():
return [migrations.AddField(
model_name = operation.model_name,
name = other.new_name,
field = operation.field,
)]
def reduce_alter_field_rename_field(self, operation, other):
if operation.model_name.lower() == other.model_name.lower() and operation.name.lower() == other.old_name.lower():
return [
other,
migrations.AlterField(
model_name = operation.model_name,
name = other.new_name,
field = operation.field,
),
]
def reduce_rename_field_self(self, operation, other):
if operation.model_name.lower() == other.model_name.lower() and operation.new_name.lower() == other.old_name.lower():
return [
migrations.RenameField(
operation.model_name,
operation.old_name,
other.new_name,
),
]
#### THROUGH CHECKS #### #### THROUGH CHECKS ####
...@@ -107,14 +296,22 @@ class MigrationOptimizer(object): ...@@ -107,14 +296,22 @@ class MigrationOptimizer(object):
""" """
MODEL_LEVEL_OPERATIONS = ( MODEL_LEVEL_OPERATIONS = (
migrations.CreateModel, migrations.CreateModel,
migrations.DeleteModel,
migrations.AlterModelTable, migrations.AlterModelTable,
migrations.AlterUniqueTogether, migrations.AlterUniqueTogether,
migrations.AlterIndexTogether, migrations.AlterIndexTogether,
) )
FIELD_LEVEL_OPERATIONS = (
migrations.AddField,
migrations.AlterField,
)
# If it's a model level operation, let it through if there's # If it's a model level operation, let it through if there's
# nothing that looks like a reference to us in 'other'. # nothing that looks like a reference to us in 'other'.
if isinstance(operation, MODEL_LEVEL_OPERATIONS): if isinstance(operation, MODEL_LEVEL_OPERATIONS):
if not other.references_model(operation.name, app_label): if not other.references_model(operation.name, app_label):
return True return True
# If it's field level, only let it through things that don't reference
# the field (which includes not referencing the model)
if isinstance(operation, FIELD_LEVEL_OPERATIONS):
if not other.references_field(operation.model_name, operation.name, app_label):
return True
return False return False
...@@ -100,6 +100,30 @@ class OperationTests(MigrationTestBase): ...@@ -100,6 +100,30 @@ class OperationTests(MigrationTestBase):
operation.database_backwards("test_dlmo", editor, new_state, project_state) operation.database_backwards("test_dlmo", editor, new_state, project_state)
self.assertTableExists("test_dlmo_pony") self.assertTableExists("test_dlmo_pony")
def test_rename_model(self):
"""
Tests the RenameModel operation.
"""
project_state = self.set_up_test_model("test_rnmo")
# Test the state alteration
operation = migrations.RenameModel("Pony", "Horse")
new_state = project_state.clone()
operation.state_forwards("test_rnmo", new_state)
self.assertNotIn(("test_rnmo", "pony"), new_state.models)
self.assertIn(("test_rnmo", "horse"), new_state.models)
# Test the database alteration
self.assertTableExists("test_rnmo_pony")
self.assertTableNotExists("test_rnmo_horse")
with connection.schema_editor() as editor:
operation.database_forwards("test_rnmo", editor, project_state, new_state)
self.assertTableNotExists("test_rnmo_pony")
self.assertTableExists("test_rnmo_horse")
# And test reversal
with connection.schema_editor() as editor:
operation.database_backwards("test_rnmo", editor, new_state, project_state)
self.assertTableExists("test_dlmo_pony")
self.assertTableNotExists("test_rnmo_horse")
def test_add_field(self): def test_add_field(self):
""" """
Tests the AddField operation. Tests the AddField operation.
......
# encoding: utf8 # encoding: utf8
import operator
from django.test import TestCase from django.test import TestCase
from django.db.migrations.optimizer import MigrationOptimizer from django.db.migrations.optimizer import MigrationOptimizer
from django.db import migrations from django.db import migrations
...@@ -64,7 +65,7 @@ class OptimizerTests(TestCase): ...@@ -64,7 +65,7 @@ class OptimizerTests(TestCase):
self.assertOptimizesTo( self.assertOptimizesTo(
[migrations.DeleteModel("Foo")], [migrations.DeleteModel("Foo")],
[migrations.DeleteModel("Foo")], [migrations.DeleteModel("Foo")],
exact=1, exact = 1,
) )
def test_create_delete_model(self): def test_create_delete_model(self):
...@@ -79,6 +80,34 @@ class OptimizerTests(TestCase): ...@@ -79,6 +80,34 @@ class OptimizerTests(TestCase):
[], [],
) )
def test_create_rename_model(self):
"""
CreateModel should absorb RenameModels.
"""
self.assertOptimizesTo(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
migrations.RenameModel("Foo", "Bar"),
],
[
migrations.CreateModel("Bar", [("name", models.CharField(max_length=255))]),
],
)
def test_rename_model_self(self):
"""
RenameModels should absorb themselves.
"""
self.assertOptimizesTo(
[
migrations.RenameModel("Foo", "Baa"),
migrations.RenameModel("Baa", "Bar"),
],
[
migrations.RenameModel("Foo", "Bar"),
],
)
def test_create_alter_delete_model(self): def test_create_alter_delete_model(self):
""" """
CreateModel, AlterModelTable, AlterUniqueTogether, and DeleteModel should collapse into nothing. CreateModel, AlterModelTable, AlterUniqueTogether, and DeleteModel should collapse into nothing.
...@@ -154,3 +183,167 @@ class OptimizerTests(TestCase): ...@@ -154,3 +183,167 @@ class OptimizerTests(TestCase):
migrations.DeleteModel("Foo"), migrations.DeleteModel("Foo"),
], ],
) )
def test_create_model_add_field(self):
"""
AddField should optimize into CreateModel.
"""
self.assertOptimizesTo(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
migrations.AddField("Foo", "age", models.IntegerField()),
],
[
migrations.CreateModel("Foo", [
("name", models.CharField(max_length=255)),
("age", models.IntegerField()),
]),
],
)
def test_create_model_alter_field(self):
"""
AlterField should optimize into CreateModel.
"""
self.assertOptimizesTo(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
migrations.AlterField("Foo", "name", models.IntegerField()),
],
[
migrations.CreateModel("Foo", [
("name", models.IntegerField()),
]),
],
)
def test_create_model_rename_field(self):
"""
RenameField should optimize into CreateModel.
"""
self.assertOptimizesTo(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
migrations.RenameField("Foo", "name", "title"),
],
[
migrations.CreateModel("Foo", [
("title", models.CharField(max_length=255)),
]),
],
)
def test_add_field_rename_field(self):
"""
RenameField should optimize into AddField
"""
self.assertOptimizesTo(
[
migrations.AddField("Foo", "name", models.CharField(max_length=255)),
migrations.RenameField("Foo", "name", "title"),
],
[
migrations.AddField("Foo", "title", models.CharField(max_length=255)),
],
)
def test_alter_field_rename_field(self):
"""
RenameField should optimize to the other side of AlterField,
and into itself.
"""
self.assertOptimizesTo(
[
migrations.AlterField("Foo", "name", models.CharField(max_length=255)),
migrations.RenameField("Foo", "name", "title"),
migrations.RenameField("Foo", "title", "nom"),
],
[
migrations.RenameField("Foo", "name", "nom"),
migrations.AlterField("Foo", "nom", models.CharField(max_length=255)),
],
)
def test_create_model_remove_field(self):
"""
RemoveField should optimize into CreateModel.
"""
self.assertOptimizesTo(
[
migrations.CreateModel("Foo", [
("name", models.CharField(max_length=255)),
("age", models.IntegerField()),
]),
migrations.RemoveField("Foo", "age"),
],
[
migrations.CreateModel("Foo", [
("name", models.CharField(max_length=255)),
]),
],
)
def test_add_field_alter_field(self):
"""
AlterField should optimize into AddField.
"""
self.assertOptimizesTo(
[
migrations.AddField("Foo", "age", models.IntegerField()),
migrations.AlterField("Foo", "age", models.FloatField(default=2.4)),
],
[
migrations.AddField("Foo", "age", models.FloatField(default=2.4)),
],
)
def test_add_field_delete_field(self):
"""
RemoveField should cancel AddField
"""
self.assertOptimizesTo(
[
migrations.AddField("Foo", "age", models.IntegerField()),
migrations.RemoveField("Foo", "age"),
],
[],
)
def test_alter_field_delete_field(self):
"""
RemoveField should absorb AlterField
"""
self.assertOptimizesTo(
[
migrations.AlterField("Foo", "age", models.IntegerField()),
migrations.RemoveField("Foo", "age"),
],
[
migrations.RemoveField("Foo", "age"),
],
)
def test_optimize_through_fields(self):
"""
Checks that field-level through checking is working.
This should manage to collapse model Foo to nonexistence,
and model Bar to a single IntegerField called "width".
"""
self.assertOptimizesTo(
[
migrations.CreateModel("Foo", [("name", models.CharField(max_length=255))]),
migrations.CreateModel("Bar", [("size", models.IntegerField())]),
migrations.AddField("Foo", "age", models.IntegerField()),
migrations.AddField("Bar", "width", models.IntegerField()),
migrations.AlterField("Foo", "age", models.IntegerField()),
migrations.RenameField("Bar", "size", "dimensions"),
migrations.RemoveField("Foo", "age"),
migrations.RenameModel("Foo", "Phou"),
migrations.RemoveField("Bar", "dimensions"),
migrations.RenameModel("Phou", "Fou"),
migrations.DeleteModel("Fou"),
],
[
migrations.CreateModel("Bar", [("width", models.IntegerField())]),
],
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment