From 48cbdd38d51b2dfca03f203a4bcc6520208ce600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Niedzielski?= Date: Fri, 18 Sep 2026 16:35:38 +0200 Subject: [PATCH] IBX-12530: Stopped truncating fixture tables inside a transaction TRUNCATE is DDL. MySQL and MariaDB commit implicitly on it, which ends the transaction the test suite wraps each test in and discards its savepoints - the suite then loses isolation, rows disappear between tests and DAMA's rollback fails with "SAVEPOINT DAMA_TEST does not exist". PostgreSQL refuses it for any table a foreign key references and aborts the whole transaction, so the DELETE written here as a fallback could never actually run: it failed too, with "current transaction is aborted", which is all the caller ever got to see. Detecting the transaction needs the driver connection rather than Connection::isTransactionActive(): dama/doctrine-test-bundle opens its transaction underneath DBAL, so Doctrine's nesting counter reports zero while the session is inside one. Outside a transaction TRUNCATE is still used, so the fast path is unchanged where it is safe. --- .../Persistence/Fixture/FixtureImporter.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/contracts/Test/Persistence/Fixture/FixtureImporter.php b/src/contracts/Test/Persistence/Fixture/FixtureImporter.php index 973c6aa8cd..30143ff893 100644 --- a/src/contracts/Test/Persistence/Fixture/FixtureImporter.php +++ b/src/contracts/Test/Persistence/Fixture/FixtureImporter.php @@ -58,6 +58,24 @@ static function ($tableData): bool { } } + /** + * Doctrine's own nesting counter is not enough here: dama/doctrine-test-bundle opens the + * transaction wrapping each test on the driver connection, underneath DBAL, so + * Connection::isTransactionActive() reports false while the session is very much inside one. + * + * @throws \Doctrine\DBAL\Exception + */ + private function isInTransaction(): bool + { + if ($this->connection->isTransactionActive()) { + return true; + } + + $nativeConnection = $this->connection->getNativeConnection(); + + return $nativeConnection instanceof \PDO && $nativeConnection->inTransaction(); + } + /** * @param string[] $tables a list of table names * @@ -67,7 +85,20 @@ private function truncateTables(array $tables): void { $dbPlatform = $this->connection->getDatabasePlatform(); + // TRUNCATE is DDL, and inside a transaction both supported platforms punish it: MySQL and + // MariaDB commit implicitly, ending the transaction the test suite wraps each test in and + // discarding its savepoints, while PostgreSQL refuses it outright for any table a foreign + // key references and aborts the transaction, so not even the DELETE below can run. Nothing + // is being saved by it there either - the rows are a fixture, not a real data set. + $useTruncate = !$this->isInTransaction(); + foreach ($tables as $table) { + if (!$useTruncate) { + $this->connection->createQueryBuilder()->delete($table)->executeStatement(); + + continue; + } + try { // Cleanup before inserting (using TRUNCATE for speed, however not possible to rollback) $this->connection->executeStatement(