diff --git a/asyncpg/transaction.py b/asyncpg/transaction.py index 562811e6..42edb0f5 100644 --- a/asyncpg/transaction.py +++ b/asyncpg/transaction.py @@ -184,12 +184,25 @@ async def __commit(self): query = 'COMMIT;' try: - await self._connection.execute(query) + status = await self._connection.execute(query) except BaseException: self._state = TransactionState.FAILED raise - else: - self._state = TransactionState.COMMITTED + + if not self._nested and status == 'ROLLBACK': + # PostgreSQL answers COMMIT of an aborted transaction with + # a ROLLBACK, so the changes are lost and must not be reported + # as committed. + self._state = TransactionState.ROLLEDBACK + raise apg_errors.InFailedSQLTransactionError.new({ + 'C': '25P02', + 'S': 'ERROR', + 'M': 'the transaction was rolled back by the server ' + 'instead of being committed', + 'H': 'An earlier statement in the transaction failed.', + }) + + self._state = TransactionState.COMMITTED async def __rollback(self): self.__check_state('rollback') diff --git a/tests/test_transaction.py b/tests/test_transaction.py index f84cf7c0..62b64f04 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -159,6 +159,41 @@ async def test_transaction_interface_errors(self): async with tr: pass + async def test_transaction_commit_rolled_back_by_server(self): + await self.con.execute('CREATE TABLE mytab (a int)') + try: + tr = self.con.transaction() + await tr.start() + await self.con.execute('INSERT INTO mytab (a) VALUES (1)') + with self.assertRaises(asyncpg.DivisionByZeroError): + await self.con.execute('SELECT 1 / 0') + + with self.assertRaisesRegex(asyncpg.InFailedSQLTransactionError, + 'rolled back by the server'): + await tr.commit() + + self.assertTrue(repr(tr).startswith( + '