diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index c800647a69..40a251a222 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -18,6 +18,21 @@ .. changelog:: :version: 1.0.10 + .. change:: + :tags: bug, orm + :versions: 1.1.0b1 + :tickets: 2696 + + A rare case which occurs when a :meth:`.Session.rollback` fails in the + scope of a :meth:`.Session.flush` operation that's raising an + exception, as has been observed in some MySQL SAVEPOINT cases, prevents + the original database exception from being observed when it was + emitted during flush, but only on Py2K because Py2K does not support + exception chaining; on Py3K the originating exception is chained. As + a workaround, a warning is emitted in this specific case showing at + least the string message of the original database error before we + proceed to raise the rollback-originating exception. + .. change:: :tags: bug, postgresql :versions: 1.1.0b1 diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 6c3f392bac..17808e66ba 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -412,11 +412,23 @@ class SessionTransaction(object): for subtransaction in stx._iterate_parents(upto=self): subtransaction.close() + if _capture_exception: + captured_exception = sys.exc_info()[1] + boundary = self if self._state in (ACTIVE, PREPARED): for transaction in self._iterate_parents(): if transaction._parent is None or transaction.nested: - transaction._rollback_impl() + try: + transaction._rollback_impl() + except Exception: + if _capture_exception: + util.warn( + "An exception raised during a Session " + "persistence operation cannot be raised " + "due to an additional ROLLBACK exception; " + "the exception is: %s" % captured_exception) + raise transaction._state = DEACTIVE boundary = transaction break @@ -438,7 +450,7 @@ class SessionTransaction(object): self.close() if self._parent and _capture_exception: - self._parent._rollback_exception = sys.exc_info()[1] + self._parent._rollback_exception = captured_exception sess.dispatch.after_soft_rollback(sess, self) diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 21dc3e71ac..63667654d4 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -121,7 +121,7 @@ def uses_deprecated(*messages): def _expect_warnings(exc_cls, messages, regex=True, assert_=True): if regex: - filters = [re.compile(msg, re.I) for msg in messages] + filters = [re.compile(msg, re.I | re.S) for msg in messages] else: filters = messages diff --git a/test/orm/test_transaction.py b/test/orm/test_transaction.py index 91846a67e6..6918996a02 100644 --- a/test/orm/test_transaction.py +++ b/test/orm/test_transaction.py @@ -657,6 +657,31 @@ class SessionTransactionTest(FixtureTest): assert session.transaction is not None, \ 'autocommit=False should start a new transaction' + @testing.requires.savepoints + def test_report_primary_error_when_rollback_fails(self): + User, users = self.classes.User, self.tables.users + + mapper(User, users) + + session = Session(testing.db) + + with expect_warnings(".*due to an additional ROLLBACK.*INSERT INTO"): + session.begin_nested() + savepoint = session.\ + connection()._Connection__transaction._savepoint + + # force the savepoint to disappear + session.execute("RELEASE SAVEPOINT %s" % savepoint) + + # now do a broken flush + session.add_all([User(id=1), User(id=1)]) + + assert_raises_message( + sa_exc.DBAPIError, + "ROLLBACK TO SAVEPOINT ", + session.flush + ) + class _LocalFixture(FixtureTest): run_setup_mappers = 'once'