Skip to content

Commit dd4601c

Browse files
authored
Merge pull request #6545 from simPod/df-c_v3
Fix incorrect `transactional()` handling when DB auto-rolled back the transaction
2 parents 2161a70 + ec90463 commit dd4601c

7 files changed

+695
-12
lines changed

src/Connection.php

+42-10
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99
use Doctrine\DBAL\Cache\QueryCacheProfile;
1010
use Doctrine\DBAL\Driver\API\ExceptionConverter;
1111
use Doctrine\DBAL\Driver\Connection as DriverConnection;
12+
use Doctrine\DBAL\Driver\Exception as TheDriverException;
1213
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
1314
use Doctrine\DBAL\Driver\Statement as DriverStatement;
1415
use Doctrine\DBAL\Event\TransactionBeginEventArgs;
1516
use Doctrine\DBAL\Event\TransactionCommitEventArgs;
1617
use Doctrine\DBAL\Event\TransactionRollBackEventArgs;
1718
use Doctrine\DBAL\Exception\ConnectionLost;
19+
use Doctrine\DBAL\Exception\DeadlockException;
1820
use Doctrine\DBAL\Exception\DriverException;
21+
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
1922
use Doctrine\DBAL\Exception\InvalidArgumentException;
23+
use Doctrine\DBAL\Exception\TransactionRolledBack;
24+
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
2025
use Doctrine\DBAL\Platforms\AbstractPlatform;
2126
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
2227
use Doctrine\DBAL\Query\QueryBuilder;
@@ -1283,16 +1288,36 @@ public function transactional(Closure $func)
12831288

12841289
try {
12851290
$res = $func($this);
1286-
$this->commit();
12871291

12881292
$successful = true;
1289-
1290-
return $res;
12911293
} finally {
12921294
if (! $successful) {
12931295
$this->rollBack();
12941296
}
12951297
}
1298+
1299+
$shouldRollback = true;
1300+
try {
1301+
$this->commit();
1302+
1303+
$shouldRollback = false;
1304+
} catch (TheDriverException $t) {
1305+
$convertedException = $this->handleDriverException($t, null);
1306+
$shouldRollback = ! (
1307+
$convertedException instanceof TransactionRolledBack
1308+
|| $convertedException instanceof UniqueConstraintViolationException
1309+
|| $convertedException instanceof ForeignKeyConstraintViolationException
1310+
|| $convertedException instanceof DeadlockException
1311+
);
1312+
1313+
throw $t;
1314+
} finally {
1315+
if ($shouldRollback) {
1316+
$this->rollBack();
1317+
}
1318+
}
1319+
1320+
return $res;
12961321
}
12971322

12981323
/**
@@ -1424,12 +1449,21 @@ public function commit()
14241449

14251450
$connection = $this->getWrappedConnection();
14261451

1427-
if ($this->transactionNestingLevel === 1) {
1428-
$result = $this->doCommit($connection);
1429-
} elseif ($this->nestTransactionsWithSavepoints) {
1430-
$this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1452+
try {
1453+
if ($this->transactionNestingLevel === 1) {
1454+
$result = $this->doCommit($connection);
1455+
} elseif ($this->nestTransactionsWithSavepoints) {
1456+
$this->releaseSavepoint($this->_getNestedTransactionSavePointName());
1457+
}
1458+
} finally {
1459+
$this->updateTransactionStateAfterCommit();
14311460
}
14321461

1462+
return $result;
1463+
}
1464+
1465+
private function updateTransactionStateAfterCommit(): void
1466+
{
14331467
--$this->transactionNestingLevel;
14341468

14351469
$eventManager = $this->getEventManager();
@@ -1446,12 +1480,10 @@ public function commit()
14461480
}
14471481

14481482
if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
1449-
return $result;
1483+
return;
14501484
}
14511485

14521486
$this->beginTransaction();
1453-
1454-
return $result;
14551487
}
14561488

14571489
/**

src/Driver/API/OCI/ExceptionConverter.php

+33-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
88
use Doctrine\DBAL\Driver\Exception;
9+
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
10+
use Doctrine\DBAL\Driver\PDO\PDOException;
911
use Doctrine\DBAL\Exception\ConnectionException;
1012
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
1113
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
@@ -17,16 +19,30 @@
1719
use Doctrine\DBAL\Exception\SyntaxErrorException;
1820
use Doctrine\DBAL\Exception\TableExistsException;
1921
use Doctrine\DBAL\Exception\TableNotFoundException;
22+
use Doctrine\DBAL\Exception\TransactionRolledBack;
2023
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
2124
use Doctrine\DBAL\Query;
2225

26+
use function explode;
27+
use function str_replace;
28+
2329
/** @internal */
2430
final class ExceptionConverter implements ExceptionConverterInterface
2531
{
2632
/** @link http://www.dba-oracle.com/t_error_code_list.htm */
2733
public function convert(Exception $exception, ?Query $query): DriverException
2834
{
29-
switch ($exception->getCode()) {
35+
/** @psalm-var int|'HY000' $code */
36+
$code = $exception->getCode();
37+
/** @psalm-suppress NoInterfaceProperties */
38+
if ($code === 'HY000' && isset($exception->errorInfo[1], $exception->errorInfo[2])) {
39+
$errorInfo = $exception->errorInfo;
40+
$exception = new PDOException($errorInfo[2], $errorInfo[1]);
41+
$exception->errorInfo = $errorInfo;
42+
$code = $exception->getCode();
43+
}
44+
45+
switch ($code) {
3046
case 1:
3147
case 2299:
3248
case 38911:
@@ -58,6 +74,22 @@ public function convert(Exception $exception, ?Query $query): DriverException
5874
case 1918:
5975
return new DatabaseDoesNotExist($exception, $query);
6076

77+
case 2091:
78+
//ORA-02091: transaction rolled back
79+
//ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated
80+
[, $causeError] = explode("\n", $exception->getMessage(), 2);
81+
82+
[$causeCode] = explode(': ', $causeError, 2);
83+
$code = (int) str_replace('ORA-', '', $causeCode);
84+
85+
if ($exception instanceof PDOException) {
86+
$why = $this->convert(new PDOException($causeError, $code, $exception), $query);
87+
} else {
88+
$why = $this->convert(new Error($causeError, null, $code, $exception), $query);
89+
}
90+
91+
return new TransactionRolledBack($why, $query);
92+
6193
case 2289:
6294
case 2443:
6395
case 4080:

src/Driver/OCI8/Connection.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public function beginTransaction(): bool
142142

143143
public function commit(): bool
144144
{
145-
if (! oci_commit($this->connection)) {
145+
if (! @oci_commit($this->connection)) {
146146
throw Error::new($this->connection);
147147
}
148148

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Doctrine\DBAL\Exception;
4+
5+
/** @psalm-immutable */
6+
class TransactionRolledBack extends DriverException
7+
{
8+
}

tests/ConnectionTest.php

+22
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,28 @@ public function rollBack(): void
10951095
self::assertSame('Original exception', $e->getPrevious()->getMessage());
10961096
}
10971097
}
1098+
1099+
/**
1100+
* We are not sure if this can happen in real life scenario
1101+
*/
1102+
public function testItFailsDuringCommitBeforeTouchingDb(): void
1103+
{
1104+
$connection = new class (['memory' => true], new Driver\SQLite3\Driver()) extends Connection {
1105+
public function commit(): void
1106+
{
1107+
throw new \Exception('Fail before touching the db');
1108+
}
1109+
1110+
public function rollBack(): void
1111+
{
1112+
throw new \Exception('Rollback got triggered');
1113+
}
1114+
};
1115+
1116+
$this->expectExceptionMessage('Rollback got triggered');
1117+
$connection->transactional(static function (): void {
1118+
});
1119+
}
10981120
}
10991121

11001122
interface ConnectDispatchEventListener

0 commit comments

Comments
 (0)