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 Mar 4, 2024
1 parent 9e588fe commit 8edffa9
Show file tree
Hide file tree
Showing 6 changed files with 326 additions and 14 deletions.
32 changes: 21 additions & 11 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -919,14 +919,15 @@ public function transactional(Closure $func): mixed
$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 @@ -1005,17 +1006,26 @@ public function commit(): void

$connection = $this->connect();

if ($this->transactionNestingLevel === 1) {
try {
$connection->commit();
} catch (Driver\Exception $e) {
throw $this->convertException($e);
try {
if ($this->transactionNestingLevel === 1) {
try {
$connection->commit();
} catch (Driver\Exception $e) {

Check warning on line 1013 in src/Connection.php

View check run for this annotation

Codecov / codecov/patch

src/Connection.php#L1013

Added line #L1013 was not covered by tests
throw $this->convertException($e);
}
} else {
$this->releaseSavepoint($this->_getNestedTransactionSavePointName());
}
} else {
$this->releaseSavepoint($this->_getNestedTransactionSavePointName());
} finally {
$this->updateTransactionStateAfterCommit();
}
}

--$this->transactionNestingLevel;
private function updateTransactionStateAfterCommit(): void
{
if ($this->transactionNestingLevel !== 0) {
--$this->transactionNestingLevel;
}

if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) {
return;
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 @@ -95,7 +95,7 @@ public function beginTransaction(): void

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

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

View check run for this annotation

Codecov / codecov/patch

src/Driver/OCI8/Connection.php#L98

Added line #L98 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 failure on line 35 in src/Driver/OCI8/Exception/Error.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (8.3)

PossiblyUndefinedArrayOffset

src/Driver/OCI8/Exception/Error.php:35:29: PossiblyUndefinedArrayOffset: Possibly undefined array key (see https://psalm.dev/167)

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

Check failure on line 37 in src/Driver/OCI8/Exception/Error.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (8.3)

PossiblyUndefinedArrayOffset

src/Driver/OCI8/Exception/Error.php:37:21: PossiblyUndefinedArrayOffset: Possibly undefined array key (see https://psalm.dev/167)
$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);

Check failure on line 38 in src/Driver/PDO/Exception.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (8.3)

PossiblyUndefinedArrayOffset

src/Driver/PDO/Exception.php:38:29: PossiblyUndefinedArrayOffset: Possibly undefined array key (see https://psalm.dev/167)

[$code, $message] = explode(': ', $secondMessage, 2);

Check failure on line 40 in src/Driver/PDO/Exception.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (8.3)

PossiblyUndefinedArrayOffset

src/Driver/PDO/Exception.php:40:21: PossiblyUndefinedArrayOffset: Possibly undefined array key (see https://psalm.dev/167)
$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 @@ -56,4 +56,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 8edffa9

Please sign in to comment.