diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index c523c2638ab..a338805b637 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -37,6 +37,7 @@ use Doctrine\ORM\Repository\Exception\InvalidFindByCall; use Doctrine\ORM\UnitOfWork; use Doctrine\ORM\Utility\IdentifierFlattener; +use Doctrine\ORM\Utility\LockSqlHelper; use Doctrine\ORM\Utility\PersisterHelper; use LengthException; @@ -96,6 +97,8 @@ */ class BasicEntityPersister implements EntityPersister { + use LockSqlHelper; + /** @var array */ private static array $comparisonMap = [ Comparison::EQ => '= %s', @@ -1074,8 +1077,8 @@ public function getSelectSQL( : $this->getSelectConditionSQL($criteria, $assoc); $lockSql = match ($lockMode) { - LockMode::PESSIMISTIC_READ => ' ' . $this->platform->getReadLockSQL(), - LockMode::PESSIMISTIC_WRITE => ' ' . $this->platform->getWriteLockSQL(), + LockMode::PESSIMISTIC_READ => ' ' . $this->getReadLockSQL($this->platform), + LockMode::PESSIMISTIC_WRITE => ' ' . $this->getWriteLockSQL($this->platform), default => '', }; @@ -1505,8 +1508,8 @@ public function lock(array $criteria, LockMode|int $lockMode): void $conditionSql = $this->getSelectConditionSQL($criteria); $lockSql = match ($lockMode) { - LockMode::PESSIMISTIC_READ => $this->platform->getReadLockSQL(), - LockMode::PESSIMISTIC_WRITE => $this->platform->getWriteLockSQL(), + LockMode::PESSIMISTIC_READ => $this->getReadLockSQL($this->platform), + LockMode::PESSIMISTIC_WRITE => $this->getWriteLockSQL($this->platform), default => '', }; diff --git a/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php index 51520eb17bd..10eb31ec50b 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/JoinedSubclassPersister.php @@ -11,6 +11,7 @@ use Doctrine\ORM\Internal\SQLResultCasing; use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Utility\LockSqlHelper; use Doctrine\ORM\Utility\PersisterHelper; use LengthException; @@ -27,6 +28,7 @@ */ class JoinedSubclassPersister extends AbstractEntityInheritancePersister { + use LockSqlHelper; use SQLResultCasing; /** @@ -279,12 +281,12 @@ public function getSelectSQL( switch ($lockMode) { case LockMode::PESSIMISTIC_READ: - $lockSql = ' ' . $this->platform->getReadLockSQL(); + $lockSql = ' ' . $this->getReadLockSQL($this->platform); break; case LockMode::PESSIMISTIC_WRITE: - $lockSql = ' ' . $this->platform->getWriteLockSQL(); + $lockSql = ' ' . $this->getWriteLockSQL($this->platform); break; } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 351d9617ee8..eab73969b4b 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -15,6 +15,7 @@ use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\Query; use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver; +use Doctrine\ORM\Utility\LockSqlHelper; use Doctrine\ORM\Utility\PersisterHelper; use InvalidArgumentException; use LogicException; @@ -46,6 +47,8 @@ */ class SqlWalker { + use LockSqlHelper; + public const HINT_DISTINCT = 'doctrine.distinct'; private readonly ResultSetMapping $rsm; @@ -475,11 +478,11 @@ public function walkSelectStatement(AST\SelectStatement $selectStatement): strin } if ($lockMode === LockMode::PESSIMISTIC_READ) { - return $sql . ' ' . $this->platform->getReadLockSQL(); + return $sql . ' ' . $this->getReadLockSQL($this->platform); } if ($lockMode === LockMode::PESSIMISTIC_WRITE) { - return $sql . ' ' . $this->platform->getWriteLockSQL(); + return $sql . ' ' . $this->getWriteLockSQL($this->platform); } if ($lockMode !== LockMode::OPTIMISTIC) { diff --git a/lib/Doctrine/ORM/Utility/LockSqlHelper.php b/lib/Doctrine/ORM/Utility/LockSqlHelper.php new file mode 100644 index 00000000000..7d135eb8cb5 --- /dev/null +++ b/lib/Doctrine/ORM/Utility/LockSqlHelper.php @@ -0,0 +1,35 @@ + 'LOCK IN SHARE MODE', + $platform instanceof PostgreSQLPlatform => 'FOR SHARE', + default => $this->getWriteLockSQL($platform), + }; + } + + private function getWriteLockSQL(AbstractPlatform $platform): string + { + return match (true) { + $platform instanceof DB2Platform => 'WITH RR USE AND KEEP UPDATE LOCKS', + $platform instanceof SQLitePlatform, + $platform instanceof SQLServerPlatform => '', + default => 'FOR UPDATE', + }; + } +} diff --git a/phpstan-dbal3.neon b/phpstan-dbal3.neon index 772fb0298b5..e771133101b 100644 --- a/phpstan-dbal3.neon +++ b/phpstan-dbal3.neon @@ -24,6 +24,8 @@ parameters: message: '~^Unreachable statement \- code above always terminates\.$~' path: lib/Doctrine/ORM/Mapping/AssociationMapping.php + - '~^Class Doctrine\\DBAL\\Platforms\\SQLitePlatform not found\.$~' + # To be removed in 4.0 - message: '#Negated boolean expression is always false\.#' diff --git a/tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php b/tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php index 891cbf15542..63e3799d4ec 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/Locking/LockTest.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional\Locking; use Doctrine\DBAL\LockMode; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\Query; use Doctrine\ORM\TransactionRequiredException; @@ -136,9 +137,7 @@ public function testRefreshWithLockPessimisticWriteNoTransactionThrowsException( #[Group('locking')] public function testLockPessimisticWrite(): void { - $writeLockSql = $this->_em->getConnection()->getDatabasePlatform()->getWriteLockSQL(); - - if (! $writeLockSql) { + if ($this->_em->getConnection()->getDatabasePlatform() instanceof SQLitePlatform) { self::markTestSkipped('Database Driver has no Write Lock support.'); } @@ -163,15 +162,13 @@ public function testLockPessimisticWrite(): void $lastLoggedQuery = $this->getLastLoggedQuery(1)['sql']; } - self::assertStringContainsString($writeLockSql, $lastLoggedQuery); + self::assertStringContainsString('FOR UPDATE', $lastLoggedQuery); } #[Group('locking')] public function testRefreshWithLockPessimisticWrite(): void { - $writeLockSql = $this->_em->getConnection()->getDatabasePlatform()->getWriteLockSQL(); - - if (! $writeLockSql) { + if ($this->_em->getConnection()->getDatabasePlatform() instanceof SQLitePlatform) { self::markTestSkipped('Database Driver has no Write Lock support.'); } @@ -196,15 +193,13 @@ public function testRefreshWithLockPessimisticWrite(): void $lastLoggedQuery = $this->getLastLoggedQuery(1)['sql']; } - self::assertStringContainsString($writeLockSql, $lastLoggedQuery); + self::assertStringContainsString('FOR UPDATE', $lastLoggedQuery); } #[Group('DDC-178')] public function testLockPessimisticRead(): void { - $readLockSql = $this->_em->getConnection()->getDatabasePlatform()->getReadLockSQL(); - - if (! $readLockSql) { + if ($this->_em->getConnection()->getDatabasePlatform() instanceof SQLitePlatform) { self::markTestSkipped('Database Driver has no Write Lock support.'); } @@ -230,7 +225,11 @@ public function testLockPessimisticRead(): void $lastLoggedQuery = $this->getLastLoggedQuery(1)['sql']; } - self::assertStringContainsString($readLockSql, $lastLoggedQuery); + self::assertThat($lastLoggedQuery, self::logicalOr( + self::stringContains('FOR UPDATE'), + self::stringContains('FOR SHARE'), + self::stringContains('LOCK IN SHARE MODE'), + )); } #[Group('DDC-1693')]