Skip to content

Commit

Permalink
Fix incorrect handling of transactions using deferred constraints
Browse files Browse the repository at this point in the history
- Let root Errors bubble up
- Catch concrete Exception in TransactionTest as we can do it now
- Expose underlying errors on Oracle platform

Co-authored-by: Jakub Chábek <jakub.chabek@cdn77.com>
  • Loading branch information
simPod and grongor committed Oct 4, 2021
1 parent bda52c5 commit 3df5e8f
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 13 deletions.
26 changes: 19 additions & 7 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1202,14 +1202,15 @@ public function transactional(Closure $func)
$this->beginTransaction();
try {
$res = $func($this);
$this->commit();

return $res;
} catch (Throwable $e) {
$this->rollBack();

throw $e;
}

$this->commit();

return $res;
}

/**
Expand Down Expand Up @@ -1320,7 +1321,13 @@ public function commit()
$logger->startQuery('"COMMIT"');
}

$result = $connection->commit();
try {
$result = $connection->commit();
} catch (Driver\Exception $e) {
$this->updateTransactionStateAfterCommit();

throw $this->convertExceptionDuringQuery($e, 'COMMIT');
}

if ($logger !== null) {
$logger->stopQuery();
Expand All @@ -1336,17 +1343,22 @@ public function commit()
}
}

$this->updateTransactionStateAfterCommit();

return $result;
}

private function updateTransactionStateAfterCommit(): void
{
--$this->transactionNestingLevel;

$this->getEventManager()->dispatchEvent(Events::onTransactionCommit, new TransactionCommitEventArgs($this));

if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
return $result;
return;
}

$this->beginTransaction();

return $result;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Driver/OCI8/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public function beginTransaction()
*/
public function commit()
{
if (! oci_commit($this->dbh)) {
if (! @oci_commit($this->dbh)) {
throw Error::new($this->dbh);
}

Expand Down
18 changes: 17 additions & 1 deletion src/Driver/OCI8/Exception/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
use Doctrine\DBAL\Driver\AbstractException;

use function assert;
use function explode;
use function oci_error;
use function str_replace;

/**
* @internal
Expand All @@ -16,6 +18,8 @@
*/
final class Error extends AbstractException
{
private const CODE_TRANSACTION_ROLLED_BACK = 2091;

/**
* @param resource $resource
*/
Expand All @@ -24,6 +28,18 @@ public static function new($resource): self
$error = oci_error($resource);
assert($error !== false);

return new self($error['message'], null, $error['code']);
$code = $error['code'];
$message = $error['message'];
if ($code === self::CODE_TRANSACTION_ROLLED_BACK) {
// There's no way this can be unit-tested as it's impossible to mock $resource
//ORA-02091: transaction rolled back
//ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated
[$firstMessage, $secondMessage] = explode("\n", $message, 2);

[$code, $message] = explode(': ', $secondMessage, 2);
$code = (int) str_replace('ORA-', '', $code);
}

return new self($message, null, $code);
}
}
6 changes: 5 additions & 1 deletion src/Driver/PDO/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ public function beginTransaction()
*/
public function commit()
{
return $this->connection->commit();
try {
return $this->connection->commit();
} catch (PDOException $exception) {
throw Exception::new($exception);
}
}

/**
Expand Down
18 changes: 17 additions & 1 deletion src/Driver/PDO/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,38 @@
use Doctrine\DBAL\Driver\AbstractException;
use PDOException;

use function explode;
use function str_replace;

/**
* @internal
*
* @psalm-immutable
*/
final class Exception extends AbstractException
{
private const CODE_TRANSACTION_ROLLED_BACK = 2091;

public static function new(PDOException $exception): self
{
$message = $exception->getMessage();

if ($exception->errorInfo !== null) {
[$sqlState, $code] = $exception->errorInfo;
} else {
$code = $exception->getCode();
$sqlState = null;
}

return new self($exception->getMessage(), $sqlState, $code, $exception);
if ($code === self::CODE_TRANSACTION_ROLLED_BACK) {
//ORA-02091: transaction rolled back
//ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated
[$firstMessage, $secondMessage] = explode("\n", $message, 2);

[$code, $message] = explode(': ', $secondMessage, 2);
$code = (int) str_replace('ORA-', '', $code);
}

return new self($message, $sqlState, $code, $exception);
}
}
21 changes: 21 additions & 0 deletions tests/Driver/PDO/ExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,25 @@ public function testOriginalExceptionIsInChain(): void
{
self::assertSame($this->wrappedException, $this->exception->getPrevious());
}

public function testExposesUnderlyingErrorOnOracle(): void
{
$pdoException = new PDOException(<<<TEXT
OCITransCommit: ORA-02091: transaction rolled back
ORA-00001: unique constraint (DOCTRINE.C1_UNIQUE) violated
(/private/tmp/php-20211003-35441-1sggrmq/php-8.0.11/ext/pdo_oci/oci_driver.c:410)
TEXT);

$pdoException->errorInfo = [self::SQLSTATE, 2091,

];

$exception = Exception::new($pdoException);

self::assertSame(1, $exception->getCode());
self::assertStringContainsString(
'unique constraint (DOCTRINE.C1_UNIQUE) violated',
$exception->getMessage()
);
}
}
Loading

0 comments on commit 3df5e8f

Please sign in to comment.