diff --git a/lib/Doctrine/Common/DataFixtures/Purger/ORMPurger.php b/lib/Doctrine/Common/DataFixtures/Purger/ORMPurger.php index f6346a92..c12f434f 100644 --- a/lib/Doctrine/Common/DataFixtures/Purger/ORMPurger.php +++ b/lib/Doctrine/Common/DataFixtures/Purger/ORMPurger.php @@ -5,7 +5,10 @@ namespace Doctrine\Common\DataFixtures\Purger; use Doctrine\Common\DataFixtures\Sorter\TopologicalSorter; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Platforms\AbstractMySQLPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Schema\Identifier; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; @@ -13,6 +16,7 @@ use function array_reverse; use function array_search; use function assert; +use function class_exists; use function count; use function is_callable; use function method_exists; @@ -141,6 +145,8 @@ public function purge() 'getSchemaAssetsFilter' ) ? $connection->getConfiguration()->getSchemaAssetsFilter() : null; + $this->disableForeignKeyChecksForMySQL($connection); + foreach ($orderedTables as $tbl) { // If we have a filter expression, check it and skip if necessary if (! $emptyFilterExpression && ! preg_match($filterExpr, $tbl)) { @@ -163,6 +169,8 @@ public function purge() $connection->executeStatement($platform->getTruncateTableSQL($tbl, true)); } } + + $this->enableForeignKeyChecksForMySQL($connection); } /** @@ -280,4 +288,38 @@ private function getDeleteFromTableSQL(string $tableName, AbstractPlatform $plat return 'DELETE FROM ' . $tableIdentifier->getQuotedName($platform); } + + private function disableForeignKeyChecksForMySQL(Connection $connection): void + { + if ($this->purgeMode !== self::PURGE_MODE_TRUNCATE || ! $this->isMySQL($connection)) { + return; + } + + // see MySQL TRUNCATE TABLE Statement: fails for an InnoDB or NDB table + // if there are any FOREIGN KEY constraints from other tables that + // reference the table. + $connection->executeStatement('SET @DOCTRINE_OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS'); + $connection->executeStatement('SET FOREIGN_KEY_CHECKS = 0'); + } + + private function enableForeignKeyChecksForMySQL(Connection $connection): void + { + if ($this->purgeMode !== self::PURGE_MODE_TRUNCATE || ! $this->isMySQL($connection)) { + return; + } + + $connection->executeStatement('SET FOREIGN_KEY_CHECKS = @DOCTRINE_OLD_FOREIGN_KEY_CHECKS'); + } + + private function isMySQL(Connection $connection): bool + { + $platform = $connection->getDatabasePlatform(); + + if (! class_exists(AbstractMySQLPlatform::class)) { + // before DBAL 3.3 + return $platform instanceof MySQLPlatform; + } + + return $platform instanceof AbstractMySQLPlatform; + } } diff --git a/tests/Doctrine/Tests/Common/DataFixtures/Purger/ORMPurgerForeignKeyCheckTest.php b/tests/Doctrine/Tests/Common/DataFixtures/Purger/ORMPurgerForeignKeyCheckTest.php new file mode 100644 index 00000000..486356a0 --- /dev/null +++ b/tests/Doctrine/Tests/Common/DataFixtures/Purger/ORMPurgerForeignKeyCheckTest.php @@ -0,0 +1,104 @@ +createMock(MappingDriver::class); + $metadataDriver->method('getAllClassNames')->willReturn([self::TEST_CLASS_NAME]); + $metadataDriver->method('loadMetadataForClass') + ->willReturnCallback(static function (string $className, ClassMetadata $metadata): void { + if ($className !== self::TEST_CLASS_NAME) { + return; + } + + $metadata->setPrimaryTable(['name' => self::TEST_TABLE_NAME]); + $metadata->setIdentifier(['id']); + }); + $metadataDriver->method('isTransient')->willReturn(false); + + return $metadataDriver; + } + + /** @return Connection&MockObject */ + protected function getMockConnectionForPlatform(AbstractPlatform $platform): Connection + { + $driver = $this->createMock(AbstractDriverMiddleware::class); + $driver->method('getDatabasePlatform')->willReturn($platform); + + $connection = $this->createMock(Connection::class); + $connection->method('getDriver')->willReturn($driver); + $connection->method('getConfiguration')->willReturn(new Configuration()); + $connection->method('getEventManager')->willReturn(new EventManager()); + $connection->method('getDatabasePlatform')->willReturn($platform); + + return $connection; + } + + /** @dataProvider purgeForDifferentPlatformsProvider */ + public function testPurgeForDifferentPlatforms(AbstractPlatform $platform, int $purgeMode, bool $hasForeignKeyCheckString): void + { + $metadataDriver = $this->getMockMetadataDriver(); + $connection = $this->getMockConnectionForPlatform($platform); + + $config = ORMSetup::createConfiguration(true); + $config->setMetadataDriverImpl($metadataDriver); + + $em = EntityManager::create($connection, $config); + $purger = new ORMPurger($em); + $purger->setPurgeMode($purgeMode); + + if ($hasForeignKeyCheckString) { + $connection + ->expects($this->exactly(4)) + ->method('executeStatement') + ->withConsecutive( + ['SET @DOCTRINE_OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS'], + ['SET FOREIGN_KEY_CHECKS = 0'], + [$this->stringContains(self::TEST_TABLE_NAME)], + ['SET FOREIGN_KEY_CHECKS = @DOCTRINE_OLD_FOREIGN_KEY_CHECKS'] + ); + } else { + $connection + ->expects($this->exactly(1)) + ->method('executeStatement') + ->withConsecutive([$this->stringContains(self::TEST_TABLE_NAME)]); + } + + $purger->purge(); + } + + /** @return list */ + public function purgeForDifferentPlatformsProvider(): array + { + return [ + [new MySQLPlatform(), ORMPurger::PURGE_MODE_TRUNCATE, true], + [new MySQLPlatform(), ORMPurger::PURGE_MODE_DELETE, false], + [new SqlitePlatform(), ORMPurger::PURGE_MODE_TRUNCATE, false], + [new SqlitePlatform(), ORMPurger::PURGE_MODE_DELETE, false], + ]; + } +}