Skip to content

Commit

Permalink
Merge pull request #6546 from simPod/df-c_v4
Browse files Browse the repository at this point in the history
Fix incorrect handling of transactions when using deferred constraints (v4)
  • Loading branch information
greg0ire authored Nov 5, 2024
2 parents 604277e + 6061e27 commit 5ddafce
Show file tree
Hide file tree
Showing 8 changed files with 651 additions and 12 deletions.
55 changes: 44 additions & 11 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
use Doctrine\DBAL\Connection\StaticServerVersionProvider;
use Doctrine\DBAL\Driver\API\ExceptionConverter;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
use Doctrine\DBAL\Driver\Exception as TheDriverException;
use Doctrine\DBAL\Driver\Statement as DriverStatement;
use Doctrine\DBAL\Exception\CommitFailedRollbackOnly;
use Doctrine\DBAL\Exception\ConnectionLost;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\DriverException;
use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException;
use Doctrine\DBAL\Exception\NoActiveTransaction;
use Doctrine\DBAL\Exception\SavepointsNotSupported;
use Doctrine\DBAL\Exception\TransactionRolledBack;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
Expand Down Expand Up @@ -931,16 +936,35 @@ public function transactional(Closure $func): mixed

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) {
$shouldRollback = ! (
$t instanceof TransactionRolledBack
|| $t instanceof UniqueConstraintViolationException
|| $t instanceof ForeignKeyConstraintViolationException
|| $t instanceof DeadlockException
);

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

return $res;
}

/**
Expand Down Expand Up @@ -1019,17 +1043,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) {
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
28 changes: 28 additions & 0 deletions src/Driver/API/OCI/ExceptionConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Doctrine\DBAL\Driver\API\ExceptionConverter as ExceptionConverterInterface;
use Doctrine\DBAL\Driver\Exception;
use Doctrine\DBAL\Driver\OCI8\Exception\Error;
use Doctrine\DBAL\Driver\PDO\Exception as DriverPDOException;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
Expand All @@ -17,9 +19,15 @@
use Doctrine\DBAL\Exception\SyntaxErrorException;
use Doctrine\DBAL\Exception\TableExistsException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\Exception\TransactionRolledBack;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\DBAL\Query;

use function assert;
use function count;
use function explode;
use function str_replace;

/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
Expand All @@ -40,6 +48,26 @@ public function convert(Exception $exception, ?Query $query): DriverException
12545 => new ConnectionException($exception, $query),
1400 => new NotNullConstraintViolationException($exception, $query),
1918 => new DatabaseDoesNotExist($exception, $query),
2091 => (function () use ($exception, $query) {
//SQLSTATE[HY000]: General error: 2091 OCITransCommit: ORA-02091: transaction rolled back
//ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated
$lines = explode("\n", $exception->getMessage(), 2);
assert(count($lines) >= 2);

[, $causeError] = $lines;

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

$sqlState = $exception->getSQLState();
if ($exception instanceof DriverPDOException) {
$why = $this->convert(new DriverPDOException($causeError, $sqlState, $code, $exception), $query);
} else {
$why = $this->convert(new Error($causeError, $sqlState, $code, $exception), $query);
}

return new TransactionRolledBack($why, $query);
})(),
2289,
2443,
4080 => new DatabaseObjectNotFoundException($exception, $query),
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)) {
throw Error::new($this->connection);
}

Expand Down
1 change: 1 addition & 0 deletions src/Driver/OCI8/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use function oci_error;
use function oci_fetch_all;
use function oci_fetch_array;
use function oci_field_name;
use function oci_num_fields;
use function oci_num_rows;

Expand Down
10 changes: 10 additions & 0 deletions src/Exception/TransactionRolledBack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Exception;

/** @psalm-immutable */
class TransactionRolledBack extends DriverException
{
}
22 changes: 22 additions & 0 deletions tests/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -675,4 +675,26 @@ 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 {
});
}
}
Loading

0 comments on commit 5ddafce

Please sign in to comment.