From b5127700e365ea57d7995f721ddc20170109202a Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Sun, 13 Oct 2024 12:05:44 +0200 Subject: [PATCH] Ensure nested transactions are not wrongly rolled back --- src/Connection.php | 20 +++++++++++++++- tests/Functional/TransactionTest.php | 34 +++++++++++++++++++++------- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/Connection.php b/src/Connection.php index baf620697f5..4410aa1b072 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -9,6 +9,7 @@ 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; @@ -1279,12 +1280,29 @@ public function transactional(Closure $func) { $this->beginTransaction(); + $successful = false; + try { $res = $func($this); + $successful = true; + } finally { + if (! $successful) { + $this->rollBack(); + } + } + + $shouldRollback = true; + try { $this->commit(); + + $shouldRollback = false; + } catch (TheDriverException $t) { + $shouldRollback = false; + + throw $t; } finally { - if ($this->isTransactionActive()) { + if ($shouldRollback) { $this->rollBack(); } } diff --git a/tests/Functional/TransactionTest.php b/tests/Functional/TransactionTest.php index 1421bd10a8a..08532838d6c 100644 --- a/tests/Functional/TransactionTest.php +++ b/tests/Functional/TransactionTest.php @@ -2,8 +2,11 @@ namespace Doctrine\DBAL\Tests\Functional; +use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver\Exception as DriverException; use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; +use Doctrine\DBAL\Platforms\DB2Platform; +use Doctrine\DBAL\Platforms\OraclePlatform; use Doctrine\DBAL\Tests\FunctionalTestCase; use PDOException; @@ -11,17 +14,12 @@ class TransactionTest extends FunctionalTestCase { - protected function setUp(): void + public function testCommitFalse(): void { - if ($this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { - return; + if (! $this->connection->getDatabasePlatform() instanceof AbstractMySQLPlatform) { + $this->markTestSkipped('Restricted to MySQL.'); } - $this->markTestSkipped('Restricted to MySQL.'); - } - - public function testCommitFalse(): void - { $this->connection->executeStatement('SET SESSION wait_timeout=1'); self::assertTrue($this->connection->beginTransaction()); @@ -40,4 +38,24 @@ public function testCommitFalse(): void $this->connection->close(); } } + + public function testNestedTransactionWalkthrough(): void + { + $platform = $this->connection->getDatabasePlatform(); + if ($platform instanceof OraclePlatform) { + $query = 'SELECT 1 FROM DUAL'; + } elseif ($platform instanceof DB2Platform) { + $query = 'SELECT 1 FROM sysibm.sysdummy1'; + } else { + $query = 'SELECT 1'; + } + + $result = $this->connection->transactional( + static fn (Connection $connection) => $connection->transactional( + static fn (Connection $connection) => $connection->fetchOne($query), + ), + ); + + self::assertSame('1', (string) $result); + } }