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

Oracle #10

Closed
wants to merge 8 commits 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
51 changes: 41 additions & 10 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Driver\API\ExceptionConverter;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Driver\Exception as TheDriverException;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\Driver\Statement as DriverStatement;
use Doctrine\DBAL\Event\TransactionBeginEventArgs;
use Doctrine\DBAL\Event\TransactionCommitEventArgs;
use Doctrine\DBAL\Event\TransactionRollBackEventArgs;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
Expand Down Expand Up @@ -1283,16 +1288,35 @@

try {
$res = $func($this);
$this->commit();

$successful = true;

return $res;
} finally {
if (! $successful) {
$this->rollBack();
}
}

$shouldRollback = true;
try {
$this->commit();

$shouldRollback = false;
} catch (TheDriverException $t) {
$convertedException = $this->handleDriverException($t, null);
$shouldRollback = ! (
$convertedException instanceof UniqueConstraintViolationException
|| ($convertedException instanceof ForeignKeyConstraintViolationException && $this->getDatabasePlatform() instanceof PostgreSQLPlatform)

Check warning on line 1308 in src/Connection.php

View workflow job for this annotation

GitHub Actions / Coding Standards / Coding Standards (PHP: 8.3)

Line exceeds 120 characters; contains 152 characters
|| $convertedException instanceof DeadlockException
);

throw $t;
} finally {
if ($shouldRollback) {
$this->rollBack();
}
}

return $res;
}

/**
Expand Down Expand Up @@ -1424,12 +1448,21 @@

$connection = $this->getWrappedConnection();

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

return $result;
}

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

$eventManager = $this->getEventManager();
Expand All @@ -1446,12 +1479,10 @@
}

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 @@ -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
22 changes: 22 additions & 0 deletions tests/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,28 @@ public function rollBack(): void
self::assertSame('Original exception', $e->getPrevious()->getMessage());
}
}

/**
* We are not sure if this can happen in real life scenario
*/
public function testItFailsDuringCommitBeforeTouchingDb(): void
{
$connection = new class (['memory' => true], new Driver\SQLite3\Driver()) extends Connection {
public function commit(): void
{
throw new \Exception('Fail before touching the db');
}

public function rollBack(): void
{
throw new \Exception('Rollback got triggered');
}
};

$this->expectExceptionMessage('Rollback got triggered');
$connection->transactional(static function (): void {
});
}
}

interface ConnectDispatchEventListener
Expand Down
Loading
Loading