Commit 5a917cfe authored by Andrew Godwin's avatar Andrew Godwin
Browse files

Fixed #22496: Data migrations get transactions again!

parent e9a456d1
Loading
Loading
Loading
Loading
+15 −2
Original line number Diff line number Diff line
from __future__ import unicode_literals
from django.db.transaction import atomic


class Migration(object):
@@ -97,6 +98,12 @@ class Migration(object):
            new_state = project_state.clone()
            operation.state_forwards(self.app_label, new_state)
            # Run the operation
            if not schema_editor.connection.features.can_rollback_ddl and operation.atomic:
                # We're forcing a transaction on a non-transactional-DDL backend
                with atomic(schema_editor.connection.alias):
                    operation.database_forwards(self.app_label, schema_editor, project_state, new_state)
            else:
                # Normal behaviour
                operation.database_forwards(self.app_label, schema_editor, project_state, new_state)
            # Switch states
            project_state = new_state
@@ -129,6 +136,12 @@ class Migration(object):
        # Now run them in reverse
        to_run.reverse()
        for operation, to_state, from_state in to_run:
            if not schema_editor.connection.features.can_rollback_ddl and operation.atomic:
                # We're forcing a transaction on a non-transactional-DDL backend
                with atomic(schema_editor.connection.alias):
                    operation.database_backwards(self.app_label, schema_editor, from_state, to_state)
            else:
                # Normal behaviour
                operation.database_backwards(self.app_label, schema_editor, from_state, to_state)
        return project_state

+4 −0
Original line number Diff line number Diff line
@@ -24,6 +24,10 @@ class Operation(object):
    # Can this migration be represented as SQL? (things like RunPython cannot)
    reduces_to_sql = True

    # Should this operation be forced as atomic even on backends with no
    # DDL transaction support (i.e., does it have no DDL, like RunPython)
    atomic = False

    serialization_expand_args = []

    def __new__(cls, *args, **kwargs):
+2 −1
Original line number Diff line number Diff line
@@ -86,7 +86,8 @@ class RunPython(Operation):

    reduces_to_sql = False

    def __init__(self, code, reverse_code=None):
    def __init__(self, code, reverse_code=None, atomic=True):
        self.atomic = atomic
        # Forwards code
        if not callable(code):
            raise ValueError("RunPython must be supplied with a callable")
+7 −1
Original line number Diff line number Diff line
@@ -178,7 +178,7 @@ operation that adds that field and so will try to run it again).
RunPython
---------

.. class:: RunPython(code, reverse_code=None)
.. class:: RunPython(code, reverse_code=None, atomic=True)

Runs custom Python code in a historical context. ``code`` (and ``reverse_code``
if supplied) should be callable objects that accept two arguments; the first is
@@ -230,6 +230,12 @@ or that you use :class:`SeparateDatabaseAndState` to add in operations that will
reflect your changes to the model state - otherwise, the versioned ORM and
the autodetector will stop working correctly.

By default, ``RunPython`` will run its contents inside a transaction even
on databases that do not support DDL transactions (for example, MySQL and
Oracle). This should be safe, but may cause a crash if you attempt to use
the ``schema_editor`` provided on these backends; in this case, please
set ``atomic=False``.

SeparateDatabaseAndState
------------------------

+37 −0
Original line number Diff line number Diff line
@@ -752,6 +752,43 @@ class OperationTests(MigrationTestBase):
        self.assertEqual(project_state.render().get_model("test_runpython", "Pony").objects.count(), 6)
        self.assertEqual(project_state.render().get_model("test_runpython", "ShetlandPony").objects.count(), 2)

    def test_run_python_atomic(self):
        """
        Tests the RunPython operation correctly handles the "atomic" keyword
        """
        project_state = self.set_up_test_model("test_runpythonatomic", mti_model=True)
        def inner_method(models, schema_editor):
            Pony = models.get_model("test_runpythonatomic", "Pony")
            Pony.objects.create(pink=1, weight=3.55)
            raise ValueError("Adrian hates ponies.")
        atomic_migration = Migration("test", "test_runpythonatomic")
        atomic_migration.operations = [migrations.RunPython(inner_method)]
        non_atomic_migration = Migration("test", "test_runpythonatomic")
        non_atomic_migration.operations = [migrations.RunPython(inner_method, atomic=False)]
        # If we're a fully-transactional database, both versions should rollback
        if connection.features.can_rollback_ddl:
            self.assertEqual(project_state.render().get_model("test_runpythonatomic", "Pony").objects.count(), 0)
            with self.assertRaises(ValueError):
                with connection.schema_editor() as editor:
                    atomic_migration.apply(project_state, editor)
            self.assertEqual(project_state.render().get_model("test_runpythonatomic", "Pony").objects.count(), 0)
            with self.assertRaises(ValueError):
                with connection.schema_editor() as editor:
                    non_atomic_migration.apply(project_state, editor)
            self.assertEqual(project_state.render().get_model("test_runpythonatomic", "Pony").objects.count(), 0)
        # Otherwise, the non-atomic operation should leave a row there
        else:
            self.assertEqual(project_state.render().get_model("test_runpythonatomic", "Pony").objects.count(), 0)
            with self.assertRaises(ValueError):
                with connection.schema_editor() as editor:
                    atomic_migration.apply(project_state, editor)
            self.assertEqual(project_state.render().get_model("test_runpythonatomic", "Pony").objects.count(), 0)
            with self.assertRaises(ValueError):
                with connection.schema_editor() as editor:
                    non_atomic_migration.apply(project_state, editor)
            self.assertEqual(project_state.render().get_model("test_runpythonatomic", "Pony").objects.count(), 1)



class MigrateNothingRouter(object):
    """