Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix incorrect handling of transactions using deferred constraints #4

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
}

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,
);

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

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

$this->beginTransaction();
}

/**
* 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)) {
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) {
// 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);
}
}
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);
}
}
25 changes: 23 additions & 2 deletions src/Driver/PDO/PDOException.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

use Doctrine\DBAL\Driver\Exception as DriverException;

use function explode;
use function is_numeric;
use function str_replace;
use function strpos;

/**
* @internal
*
Expand All @@ -17,10 +22,26 @@ final class PDOException extends \PDOException implements DriverException

public static function new(\PDOException $previous): self
{
$exception = new self($previous->message, 0, $previous);
if (isset($previous->errorInfo[2]) && strpos($previous->errorInfo[2], 'OCITransCommit: ORA-02091') === 0) {
// With pdo_oci driver, the root-cause error is in the second line
//ORA-02091: transaction rolled back
//ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated
[$firstMessage, $secondMessage] = explode("\n", $previous->message, 2);

[$code, $message] = explode(': ', $secondMessage, 2);
$code = (int) str_replace('ORA-', '', $code);
} else {
$message = $previous->message;
if (is_numeric($previous->code)) {
$code = (int) $previous->code;
} else {
$code = $previous->errorInfo[1] ?? 0;
}
}

$exception = new self($message, $code, $previous);

$exception->errorInfo = $previous->errorInfo;
$exception->code = $previous->code;
$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(),
);
}
}
5 changes: 2 additions & 3 deletions tests/Functional/TransactionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Doctrine\DBAL\Driver\Exception as DriverException;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Tests\FunctionalTestCase;
use PDOException;

use function sleep;

Expand All @@ -29,8 +28,8 @@ public function testCommitFalse(): void
sleep(2); // during the sleep mysql will close the connection

try {
self::assertFalse(@$this->connection->commit()); // we will ignore `MySQL server has gone away` warnings
} catch (PDOException $e) {
@$this->connection->commit(); // we will ignore `Packets out of order.` error
} catch (\Doctrine\DBAL\Exception\DriverException $e) {
self::assertInstanceOf(DriverException::class, $e);

/* For PDO, we are using ERRMODE EXCEPTION, so this catch should be
Expand Down
Loading
Loading