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 <[email protected]>
  • Loading branch information
simPod and grongor committed Nov 20, 2023
1 parent 493c768 commit 14dcd62
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 33 deletions.
70 changes: 41 additions & 29 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1275,14 +1275,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 @@ -1418,33 +1419,16 @@ public function commit()

$connection = $this->getWrappedConnection();

if ($this->transactionNestingLevel === 1) {
$result = $this->doCommit($connection);
} elseif ($this->nestTransactionsWithSavepoints) {
$this->releaseSavepoint($this->_getNestedTransactionSavePointName());
}

--$this->transactionNestingLevel;

$eventManager = $this->getEventManager();

if ($eventManager->hasListeners(Events::onTransactionCommit)) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/5784',
'Subscribing to %s events is deprecated.',
Events::onTransactionCommit,
);

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

if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
return $result;
try {
if ($this->transactionNestingLevel === 1) {
$result = $this->doCommit($connection);
} elseif ($this->nestTransactionsWithSavepoints) {
$this->releaseSavepoint($this->_getNestedTransactionSavePointName());
}
} finally {
$this->updateTransactionStateAfterCommit();
}

$this->beginTransaction();

return $result;
}

Expand All @@ -1461,7 +1445,11 @@ private function doCommit(DriverConnection $connection)
$logger->startQuery('"COMMIT"');
}

$result = $connection->commit();
try {
$result = $connection->commit();
} catch (Driver\Exception $e) {
throw $this->convertExceptionDuringQuery($e, 'COMMIT');

Check warning on line 1451 in src/Connection.php

View check run for this annotation

Codecov / codecov/patch

src/Connection.php#L1450-L1451

Added lines #L1450 - L1451 were not covered by tests
}

if ($logger !== null) {
$logger->stopQuery();
Expand All @@ -1470,6 +1458,30 @@ private function doCommit(DriverConnection $connection)
return $result;
}

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

$eventManager = $this->getEventManager();

if ($eventManager->hasListeners(Events::onTransactionCommit)) {
Deprecation::trigger(
'doctrine/dbal',
'https://github.com/doctrine/dbal/issues/5784',
'Subscribing to %s events is deprecated.',
Events::onTransactionCommit,
);

Check warning on line 1473 in src/Connection.php

View check run for this annotation

Codecov / codecov/patch

src/Connection.php#L1468-L1473

Added lines #L1468 - L1473 were not covered by tests

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

Check warning on line 1475 in src/Connection.php

View check run for this annotation

Codecov / codecov/patch

src/Connection.php#L1475

Added line #L1475 was not covered by tests
}

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

$this->beginTransaction();

Check warning on line 1482 in src/Connection.php

View check run for this annotation

Codecov / codecov/patch

src/Connection.php#L1482

Added line #L1482 was not covered by tests
}

/**
* Commits all current nesting transactions.
*
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 @@ -142,7 +142,7 @@ public function beginTransaction(): bool

public function commit(): bool
{
if (! oci_commit($this->connection)) {
if (! @oci_commit($this->connection)) {

Check warning on line 145 in src/Driver/OCI8/Connection.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Connection.php#L145

Added line #L145 was not covered by tests
throw Error::new($this->connection);
}

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,12 +18,26 @@
*/
final class Error extends AbstractException
{
private const CODE_TRANSACTION_ROLLED_BACK = 2091;

/** @param resource $resource */
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) {

Check warning on line 31 in src/Driver/OCI8/Exception/Error.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Exception/Error.php#L29-L31

Added lines #L29 - L31 were not covered by tests
// 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);

Check warning on line 35 in src/Driver/OCI8/Exception/Error.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Exception/Error.php#L35

Added line #L35 was not covered by tests

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

Check warning on line 38 in src/Driver/OCI8/Exception/Error.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Exception/Error.php#L37-L38

Added lines #L37 - L38 were not covered by tests
}

return new self($message, null, $code);

Check warning on line 41 in src/Driver/OCI8/Exception/Error.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Exception/Error.php#L41

Added line #L41 was not covered by tests
}
}
18 changes: 17 additions & 1 deletion src/Driver/PDO/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@
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;

Expand All @@ -25,6 +32,15 @@ public static function new(PDOException $exception): self
$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);
}
}
4 changes: 3 additions & 1 deletion src/Driver/PDO/PDOException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Doctrine\DBAL\Driver\Exception as DriverException;

use function is_int;

/**
* @internal
*
Expand All @@ -20,7 +22,7 @@ public static function new(\PDOException $previous): self
$exception = new self($previous->message, 0, $previous);

$exception->errorInfo = $previous->errorInfo;
$exception->code = $previous->code;
$exception->code = is_int($previous->code) ? $previous->code : 0;

Check warning on line 25 in src/Driver/PDO/PDOException.php

View check run for this annotation

Codecov / codecov/patch

src/Driver/PDO/PDOException.php#L25

Added line #L25 was not covered by tests
$exception->sqlState = $previous->errorInfo[0] ?? null;

return $exception;
Expand Down
21 changes: 21 additions & 0 deletions tests/Driver/PDO/ExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,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(),
);
}
}

0 comments on commit 14dcd62

Please sign in to comment.