diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index ae9fdbb..add7aa6 100644
a
|
b
|
class BaseDatabaseWrapper(object):
|
361 | 361 | raise TransactionManagementError( |
362 | 362 | "This is forbidden when an 'atomic' block is active.") |
363 | 363 | |
| 364 | def validate_no_broken_transaction(self): |
| 365 | if self.needs_rollback: |
| 366 | raise TransactionManagementError( |
| 367 | "An error occurred in the current transaction. You can't " |
| 368 | "execute queries until the end of the 'atomic' block.") |
| 369 | |
364 | 370 | def abort(self): |
365 | 371 | """ |
366 | 372 | Roll back any ongoing transaction and clean the transaction state |
diff --git a/django/db/backends/utils.py b/django/db/backends/utils.py
index 0e2aa45..432f760 100644
a
|
b
|
class CursorWrapper(object):
|
19 | 19 | self.cursor = cursor |
20 | 20 | self.db = db |
21 | 21 | |
22 | | SET_DIRTY_ATTRS = frozenset(['execute', 'executemany', 'callproc']) |
23 | | WRAP_ERROR_ATTRS = frozenset([ |
24 | | 'callproc', 'close', 'execute', 'executemany', |
25 | | 'fetchone', 'fetchmany', 'fetchall', 'nextset']) |
| 22 | WRAP_ERROR_ATTRS = frozenset(['fetchone', 'fetchmany', 'fetchall', 'nextset']) |
26 | 23 | |
27 | 24 | def __getattr__(self, attr): |
28 | | if attr in CursorWrapper.SET_DIRTY_ATTRS: |
29 | | self.db.set_dirty() |
30 | 25 | cursor_attr = getattr(self.cursor, attr) |
31 | 26 | if attr in CursorWrapper.WRAP_ERROR_ATTRS: |
32 | 27 | return self.db.wrap_database_errors(cursor_attr) |
… |
… |
class CursorWrapper(object):
|
36 | 31 | def __iter__(self): |
37 | 32 | return iter(self.cursor) |
38 | 33 | |
| 34 | # The following methods cannot be implemented in __getattr__, because the |
| 35 | # code must run when the method is invoked, not just when it is accessed. |
39 | 36 | |
40 | | class CursorDebugWrapper(CursorWrapper): |
| 37 | def callproc(self, procname, params=None): |
| 38 | self.db.validate_no_broken_transaction() |
| 39 | self.db.set_dirty() |
| 40 | with self.db.wrap_database_errors: |
| 41 | if params is None: |
| 42 | return self.cursor.callproc(procname) |
| 43 | else: |
| 44 | return self.cursor.callproc(procname, params) |
41 | 45 | |
42 | 46 | def execute(self, sql, params=None): |
| 47 | self.db.validate_no_broken_transaction() |
43 | 48 | self.db.set_dirty() |
| 49 | with self.db.wrap_database_errors: |
| 50 | if params is None: |
| 51 | return self.cursor.execute(sql) |
| 52 | else: |
| 53 | return self.cursor.execute(sql, params) |
| 54 | |
| 55 | def executemany(self, sql, param_list): |
| 56 | self.db.validate_no_broken_transaction() |
| 57 | self.db.set_dirty() |
| 58 | with self.db.wrap_database_errors: |
| 59 | return self.cursor.executemany(sql, param_list) |
| 60 | |
| 61 | |
| 62 | class CursorDebugWrapper(CursorWrapper): |
| 63 | |
| 64 | # XXX callproc isn't instrumented at this time. |
| 65 | |
| 66 | def execute(self, sql, params=None): |
44 | 67 | start = time() |
45 | 68 | try: |
46 | | with self.db.wrap_database_errors: |
47 | | if params is None: |
48 | | # params default might be backend specific |
49 | | return self.cursor.execute(sql) |
50 | | return self.cursor.execute(sql, params) |
| 69 | return super(CursorDebugWrapper, self).execute(sql, params) |
51 | 70 | finally: |
52 | 71 | stop = time() |
53 | 72 | duration = stop - start |
… |
… |
class CursorDebugWrapper(CursorWrapper):
|
61 | 80 | ) |
62 | 81 | |
63 | 82 | def executemany(self, sql, param_list): |
64 | | self.db.set_dirty() |
65 | 83 | start = time() |
66 | 84 | try: |
67 | | with self.db.wrap_database_errors: |
68 | | return self.cursor.executemany(sql, param_list) |
| 85 | return super(CursorDebugWrapper, self).executemany(sql, param_list) |
69 | 86 | finally: |
70 | 87 | stop = time() |
71 | 88 | duration = stop - start |
diff --git a/django/db/transaction.py b/django/db/transaction.py
index 7509ad3..86a357f 100644
a
|
b
|
import warnings
|
16 | 16 | |
17 | 17 | from functools import wraps |
18 | 18 | |
19 | | from django.db import connections, DatabaseError, DEFAULT_DB_ALIAS |
| 19 | from django.db import ( |
| 20 | connections, DEFAULT_DB_ALIAS, |
| 21 | DatabaseError, ProgrammingError) |
20 | 22 | from django.utils.decorators import available_attrs |
21 | 23 | |
22 | 24 | |
23 | | class TransactionManagementError(Exception): |
| 25 | class TransactionManagementError(ProgrammingError): |
24 | 26 | """ |
25 | | This exception is thrown when something bad happens with transaction |
26 | | management. |
| 27 | This exception is thrown when transaction management is used improperly. |
27 | 28 | """ |
28 | 29 | pass |
29 | 30 | |
diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py
index 9cf8b4d..9521f60 100644
a
|
b
|
class AtomicErrorsTests(TransactionTestCase):
|
336 | 336 | with self.assertRaises(transaction.TransactionManagementError): |
337 | 337 | transaction.leave_transaction_management() |
338 | 338 | |
| 339 | def test_atomic_prevents_running_more_queries_after_an_error(self): |
| 340 | cursor = connection.cursor() |
| 341 | with transaction.atomic(): |
| 342 | with self.assertRaises(DatabaseError): |
| 343 | cursor.execute("INSERT INTO transactions_no_such_table (id) VALUES (0)") |
| 344 | with self.assertRaises(transaction.TransactionManagementError): |
| 345 | cursor.execute("INSERT INTO transactions_no_such_table (id) VALUES (0)") |
339 | 346 | |
340 | 347 | class AtomicMiscTests(TransactionTestCase): |
341 | 348 | |