Code

Ticket #9964: 9964-r15054-bugfix.patch

File 9964-r15054-bugfix.patch, 14.6 KB (added by shai, 3 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