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

test: cover nested transactions #12

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
52 changes: 42 additions & 10 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@
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\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 @@ -1283,16 +1288,36 @@ public function transactional(Closure $func)

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 TransactionRolledBack
|| $convertedException instanceof UniqueConstraintViolationException
|| $convertedException instanceof ForeignKeyConstraintViolationException
|| $convertedException instanceof DeadlockException
);

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

return $res;
}

/**
Expand Down Expand Up @@ -1424,12 +1449,21 @@ public function commit()

$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 +1480,10 @@ public function commit()
}

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

$this->beginTransaction();

return $result;
}

/**
Expand Down
34 changes: 33 additions & 1 deletion 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\PDOException;
use Doctrine\DBAL\Exception\ConnectionException;
use Doctrine\DBAL\Exception\DatabaseDoesNotExist;
use Doctrine\DBAL\Exception\DatabaseObjectNotFoundException;
Expand All @@ -17,16 +19,30 @@
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 explode;
use function str_replace;

/** @internal */
final class ExceptionConverter implements ExceptionConverterInterface
{
/** @link http://www.dba-oracle.com/t_error_code_list.htm */
public function convert(Exception $exception, ?Query $query): DriverException
{
switch ($exception->getCode()) {
/** @psalm-var int|'HY000' $code */
$code = $exception->getCode();
/** @psalm-suppress NoInterfaceProperties */
if ($code === 'HY000' && isset($exception->errorInfo[1], $exception->errorInfo[2])) {
$errorInfo = $exception->errorInfo;
$exception = new PDOException($errorInfo[2], $errorInfo[1]);
$exception->errorInfo = $errorInfo;
$code = $exception->getCode();
}

switch ($code) {
case 1:
case 2299:
case 38911:
Expand Down Expand Up @@ -58,6 +74,22 @@ public function convert(Exception $exception, ?Query $query): DriverException
case 1918:
return new DatabaseDoesNotExist($exception, $query);

case 2091:
//ORA-02091: transaction rolled back
//ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated
[, $causeError] = explode("\n", $exception->getMessage(), 2);

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

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

return new TransactionRolledBack($why, $exception, $query);

case 2289:
case 2443:
case 4080:
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
24 changes: 24 additions & 0 deletions src/Exception/TransactionRolledBack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Doctrine\DBAL\Exception;

use Doctrine\DBAL\Driver\Exception as TheDriverException;
use Doctrine\DBAL\Query;

/** @psalm-immutable */
class TransactionRolledBack extends DriverException
{
private DriverException $why;

public function __construct(DriverException $why, TheDriverException $driverException, ?Query $query)
{
parent::__construct($driverException, $query);

$this->why = $why;
}

public function why(): DriverException
{
return $this->why;
}
}
94 changes: 94 additions & 0 deletions tests/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use RuntimeException;
use stdClass;

/** @requires extension pdo_mysql */
Expand Down Expand Up @@ -396,6 +397,48 @@ public function testConnectStartsTransactionInNoAutoCommitMode(): void
self::assertTrue($conn->isTransactionActive());
}

public function testTransactionIsNotActiveAfterTransactionalInAutoCommitMode(): void
{
$driverMock = $this->createMock(Driver::class);
$driverMock->expects(self::any())
->method('connect')
->willReturn(
$this->createMock(DriverConnection::class),
);

$conn = new Connection([], $driverMock);

$conn->setAutoCommit(true);

self::assertFalse($conn->isTransactionActive());

$conn->transactional(static function (Connection $connection): void {
});

self::assertFalse($conn->isTransactionActive());
}

public function testTransactionIsActiveAfterTransactionalInNoAutoCommitMode(): void
{
$driverMock = $this->createMock(Driver::class);
$driverMock->expects(self::any())
->method('connect')
->willReturn(
$this->createMock(DriverConnection::class),
);

$conn = new Connection([], $driverMock);

$conn->setAutoCommit(false);

self::assertFalse($conn->isTransactionActive());

$conn->transactional(static function (Connection $connection): void {
});

self::assertTrue($conn->isTransactionActive());
}

public function testCommitStartsTransactionInNoAutoCommitMode(): void
{
$driverMock = $this->createMock(Driver::class);
Expand All @@ -413,6 +456,35 @@ public function testCommitStartsTransactionInNoAutoCommitMode(): void
self::assertTrue($conn->isTransactionActive());
}

public function testCoverBeginTransactionFailureAfterCommitInNoAutoCommitMode(): void
{
$driverConnectionMock = $this->createMock(DriverConnection::class);
$driverConnectionMock->expects(self::exactly(2))
->method('beginTransaction')
->willReturnOnConsecutiveCalls(
true,
self::throwException(new RuntimeException()),
);

$driverMock = $this->createMock(Driver::class);
$driverMock->expects(self::any())
->method('connect')
->willReturn(
$driverConnectionMock,
);
$conn = new Connection([], $driverMock);

$conn->setAutoCommit(false);

$conn->connect();
try {
$conn->commit();
} catch (RuntimeException $e) {
}

self::assertTrue($conn->isTransactionActive());
}

/** @dataProvider resultProvider */
public function testCommitReturn(bool $expectedResult): void
{
Expand Down Expand Up @@ -1025,6 +1097,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