Ticket #9964: 9964-r15054-bugfix.patch

File 9964-r15054-bugfix.patch, 14.6 KB (added by Shai Berger, 13 years ago)

Improved patch against r15054

  • django/db/backends/util.py

     
    5252    def __iter__(self):
    5353        return iter(self.cursor)
    5454
     55class CursorUseNotifyWrapper(object):
     56    def __init__(self, cursor, on_use):
     57        self.cursor = cursor
     58        self.on_use = on_use
     59    def __getattr__(self, attr):
     60        self.on_use()
     61        return getattr(self.cursor, attr)
     62    def __iter__(self):
     63        return iter(self.cursor)
     64
    5565###############################################
    5666# Converters from database (string) to Python #
    5767###############################################
  • django/db/backends/__init__.py

     
    2222        self.settings_dict = settings_dict
    2323        self.alias = alias
    2424        self.use_debug_cursor = None
     25        self.on_used = None
    2526
    2627    def __eq__(self, other):
    2728        return self.alias == other.alias
     
    7576    def cursor(self):
    7677        from django.conf import settings
    7778        cursor = self._cursor()
     79        # Used by transaction mechanism to be notified that db was used
     80        if self.on_used:
     81            cursor = util.CursorUseNotifyWrapper(cursor, self.on_used)
    7882        if (self.use_debug_cursor or
    7983            (self.use_debug_cursor is None and settings.DEBUG)):
    8084            return self.make_debug_cursor(cursor)
  • django/db/transaction.py

     
    6767    if thread_ident not in dirty or using not in dirty[thread_ident]:
    6868        dirty.setdefault(thread_ident, {})
    6969        dirty[thread_ident][using] = False
     70    def set_dirty_if_managed():
     71        if is_managed(using): set_dirty(using)
     72    connection.on_used = set_dirty_if_managed
    7073    connection._enter_transaction_management(managed)
    7174
    7275def leave_transaction_management(using=None):
  • tests/regressiontests/transaction_regress/tests.py

     
     1from django.test import TransactionTestCase
     2from django.db import connection, transaction
     3from django.db.transaction import \
     4    commit_on_success, commit_manually, \
     5    TransactionManagementError
     6from django.core.exceptions import ImproperlyConfigured
     7from models import Mod
     8
     9class Test9964(TransactionTestCase):
     10    """
     11    Tests to make sure that transactions are properly closed
     12    when they should be, and aren't left pending after operations
     13    have been performed in them.
     14
     15    Prompted by http://code.djangoproject.com/ticket/9964
     16    """
     17    def test_raw_committed_on_success(self):
     18        """
     19        Make sure a transaction consisting of raw SQL execution gets
     20        committed by the commit_on_success decorator.
     21        """
     22        @commit_on_success
     23        def raw_sql():
     24            "Write a record using raw sql under a commit_on_success decorator"
     25            cursor = connection.cursor()
     26            cursor.execute("INSERT into transaction_regress_mod (id,fld) values (17,18)")
     27           
     28        raw_sql()
     29        # Rollback so that if the decorator didn't commit, the record is unwritten
     30        transaction.rollback()
     31        try:
     32            # Check that the record is in the DB
     33            obj = Mod.objects.get(pk=17)
     34            self.assertEqual(obj.fld, 18)
     35        except Mod.DoesNotExist:
     36            self.fail("transaction with raw sql not committed")
     37
     38    def test_commit_manually_enforced(self):
     39        """
     40        Make sure that under commit_manually, even "read-only" transaction require closure
     41        (commit or rollback), and a transaction left pending is treated as an error.
     42        """
     43        @commit_manually
     44        def non_comitter():
     45            "Execute a managed transaction with read-only operations and fail to commit"
     46            _ = Mod.objects.count()
     47           
     48        self.assertRaises(TransactionManagementError, non_comitter)
     49
     50    def test_commit_manually_commit_ok(self):
     51        """
     52        Test that under commit_manually, a committed transaction is accepted by the transaction
     53        management mechanisms
     54        """
     55        @commit_manually
     56        def committer():
     57            """
     58            Perform a database query, then commit the transaction
     59            """
     60            _ = Mod.objects.count()
     61            transaction.commit()
     62       
     63        try:
     64            committer()
     65        except TransactionManagementError:
     66            self.fail("Commit did not clear the transaction state")
     67
     68    def test_commit_manually_rollback_ok(self):
     69        """
     70        Test that under commit_manually, a rolled-back transaction is accepted by the transaction
     71        management mechanisms
     72        """
     73        @commit_manually
     74        def roller_back():
     75            """
     76            Perform a database query, then rollback the transaction
     77            """
     78            _ = Mod.objects.count()
     79            transaction.rollback()
     80       
     81        try:
     82            roller_back()
     83        except TransactionManagementError:
     84            self.fail("Rollback did not clear the transaction state")
     85
     86    def test_commit_manually_enforced_after_commit(self):
     87        """
     88        Test that under commit_manually, if a transaction is committed and an operation is
     89        performed later, we still require the new transaction to be closed
     90        """
     91        @commit_manually
     92        def fake_committer():
     93            "Query, commit, then query again, leaving with a pending transaction"
     94            _ = Mod.objects.count()
     95            transaction.commit()
     96            _ = Mod.objects.count()           
     97           
     98        self.assertRaises(TransactionManagementError, fake_committer)
     99
     100    def test_reuse_cursor_reference(self):
     101        """
     102        Make sure transaction closure is enforced even when the queries are performed
     103        through a single cursor reference retrieved in the beginning
     104        (this is to show why it is wrong to set the transaction dirty only when a cursor
     105        is fetched from the connection).
     106        """
     107        @commit_on_success
     108        def reuse_cursor_ref():
     109            """
     110            Fetch a cursor, perform an query, rollback to close the transaction,
     111            then write a record (in a new transaction) using the same cursor object
     112            (reference). All this under commit_on_success, so the second insert should
     113            be committed.
     114            """
     115            cursor = connection.cursor()
     116            cursor.execute("INSERT into transaction_regress_mod (id,fld) values (1,2)")
     117            transaction.rollback()
     118            cursor.execute("INSERT into transaction_regress_mod (id,fld) values (1,2)")
     119           
     120        reuse_cursor_ref()
     121        # Rollback so that if the decorator didn't commit, the record is unwritten
     122        transaction.rollback()
     123        try:
     124            # Check that the record is in the DB
     125            obj = Mod.objects.get(pk=1)
     126            self.assertEquals(obj.fld, 2)
     127        except Mod.DoesNotExist:
     128            self.fail("After ending a transaction, cursor use no longer sets dirty")
     129
     130    def test_failing_query_transaction_closed(self):
     131        """
     132        Make sure that under commit_on_success, a transaction is rolled back even if
     133        the first database-modifying operation fails.
     134        This is prompted by http://code.djangoproject.com/ticket/6669 (and based on sample
     135        code posted there to exemplify the problem): Before Django 1.3,
     136        transactions were only marked "dirty" by the save() function after it successfully
     137        wrote the object to the database.
     138        """
     139        from django.contrib.auth.models import User
     140
     141        @transaction.commit_on_success
     142        def create_system_user():
     143            "Create a user in a transaction"
     144            user = User.objects.create_user(username='system', password='iamr00t', email='root@SITENAME.com')
     145            # Redundant, just makes sure the user id was read back from DB
     146            Mod.objects.create(fld=user.id)
     147
     148        # Create a user
     149        create_system_user()
     150
     151        try:
     152            # The second call to create_system_user should fail for violating a unique constraint
     153            # (it's trying to re-create the same user)
     154            create_system_user()
     155        except:
     156            pass
     157        else:
     158            raise ImproperlyConfigured('Unique constraint not enforced on django.contrib.auth.models.User')
     159
     160        try:
     161            # Try to read the database. If the last transaction was indeed closed,
     162            # this should cause no problems
     163            _ = User.objects.all()[0]
     164        except:
     165            self.fail("A transaction consisting of a failed operation was not closed.")
  • tests/regressiontests/transaction_regress/models.py

     
     1from django.db import models
     2
     3class Mod(models.Model):
     4    fld = models.IntegerField()
  • tests/regressiontests/delete_regress/tests.py

     
    6161        Book.objects.filter(pagecount__lt=250).delete()
    6262        transaction.commit()
    6363        self.assertEqual(1, Book.objects.count())
     64        transaction.commit()
    6465
    6566class DeleteCascadeTests(TestCase):
    6667    def test_generic_relation_cascade(self):
  • tests/regressiontests/fixtures_regress/tests.py

     
    610610        self.assertEqual(Thingy.objects.count(), 1)
    611611        transaction.rollback()
    612612        self.assertEqual(Thingy.objects.count(), 0)
     613        transaction.commit()
    613614
    614615    def test_ticket_11101(self):
    615616        """Test that fixtures can be rolled back (ticket #11101)."""
  • docs/topics/db/sql.txt

     
    231231
    232232Transactions and raw SQL
    233233------------------------
    234 If you are using transaction decorators (such as ``commit_on_success``) to
    235 wrap your views and provide transaction control, you don't have to make a
    236 manual call to ``transaction.commit_unless_managed()`` -- you can manually
    237 commit if you want to, but you aren't required to, since the decorator will
    238 commit for you. However, if you don't manually commit your changes, you will
    239 need to manually mark the transaction as dirty, using
    240 ``transaction.set_dirty()``::
    241234
    242     @commit_on_success
    243     def my_custom_sql_view(request, value):
    244         from django.db import connection, transaction
    245         cursor = connection.cursor()
     235.. versionchanged:: 1.3
    246236
    247         # Data modifying operation
    248         cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [value])
     237If you write functions that modify the database via raw SQL, you need to
     238consider the transaction management mode they run under. If this may be
     239Django's default mode (by default or using the ``auto_commit``
     240decorator), you need to make manual calls to
     241``transaction.commit_unless_managed()`` to make sure the changes are
     242committed. If you are using transaction decorators (such as
     243``commit_on_success``) to wrap your views and provide transaction
     244control, you don't have to make such calls.
    249245
    250         # Since we modified data, mark the transaction as dirty
    251         transaction.set_dirty()
    252246
    253         # Data retrieval operation. This doesn't dirty the transaction,
    254         # so no call to set_dirty() is required.
    255         cursor.execute("SELECT foo FROM bar WHERE baz = %s", [value])
    256         row = cursor.fetchone()
    257 
    258         return render_to_response('template.html', {'row': row})
    259 
    260 The call to ``set_dirty()`` is made automatically when you use the Django ORM
    261 to make data modifying database calls. However, when you use raw SQL, Django
    262 has no way of knowing if your SQL modifies data or not. The manual call to
    263 ``set_dirty()`` ensures that Django knows that there are modifications that
    264 must be committed.
    265 
    266247Connections and cursors
    267248-----------------------
    268249
  • docs/releases/1.3.txt

     
    378378
    379379
    380380
     381Transaction management
     382~~~~~~~~~~~~~~~~~~~~~~
     383
     384When using managed transactions -- that is, anything but the default
     385autocommit mode -- it is important when a transaction is marked as
     386"dirty". Dirty transactions are committed by the ``commit_on_success``
     387decorator or the ``TransactionMiddleware``, and ``commit_manually``
     388forces them to be closed explicitly; clean transactions "get a pass",
     389which means they are usually rolled back at the end of a request
     390when the connection is closed.
     391
     392Until Django 1.3, transactions were only marked dirty when Django
     393was aware of a modifying operation performed in them; that is, either
     394some model was saved, some bulk update or delete was performed, or
     395the user explicitly called ``transaction.set_dirty()``. In Django 1.3,
     396a transaction is marked dirty when any database operation is performed;
     397this means you no longer need to set a transaction dirty explicitly
     398when you execute raw SQL or use a data-modifying ``select``. On the
     399other hand, you do need to explicitly close read-only transactions
     400under ``commit_manually``. For example, take a look at this code::
     401
     402      @transaction.commit_manually
     403      def my_view(request, name):
     404          obj = get_object_or_404(MyObject, name__iexact=name)
     405          return render_to_response('template', {'object':obj})
     406
     407Until Django 1.3, this works fine. With Django 1.3, this raises a
     408``TransactionManagementError``; you need to end the transaction
     409explicitly, one way or another.
     410
    381411.. _deprecated-features-1.3:
    382412
    383413Features deprecated in 1.3
Back to Top