diff --git a/UPGRADE.md b/UPGRADE.md
index 06e70fdcd3c..73282a807a4 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -8,6 +8,11 @@ awareness about deprecated code.
# Upgrade to 3.8
+## Deprecated lock-related `AbstractPlatform` methods
+
+The usage of `AbstractPlatform::getReadLockSQL()`, `::getWriteLockSQL()` and `::getForUpdateSQL` is deprecated as this
+API is not portable. Use `QueryBuilder::forUpdate()` as a replacement for the latter.
+
## Deprecated `AbstractMySQLPlatform` methods
* `AbstractMySQLPlatform::getColumnTypeSQLSnippets()` has been deprecated
diff --git a/psalm.xml.dist b/psalm.xml.dist
index 0eb1e5ced86..ba2e28f574d 100644
--- a/psalm.xml.dist
+++ b/psalm.xml.dist
@@ -515,6 +515,9 @@
+
+
+
diff --git a/src/Driver/AbstractMySQLDriver.php b/src/Driver/AbstractMySQLDriver.php
index 5f6ffd6f5f9..83159a540da 100644
--- a/src/Driver/AbstractMySQLDriver.php
+++ b/src/Driver/AbstractMySQLDriver.php
@@ -11,6 +11,7 @@
use Doctrine\DBAL\Platforms\MariaDb1027Platform;
use Doctrine\DBAL\Platforms\MariaDb1043Platform;
use Doctrine\DBAL\Platforms\MariaDb1052Platform;
+use Doctrine\DBAL\Platforms\MariaDb1060Platform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
use Doctrine\DBAL\Platforms\MySQLPlatform;
@@ -39,6 +40,10 @@ public function createDatabasePlatformForVersion($version)
if ($mariadb) {
$mariaDbVersion = $this->getMariaDbMysqlVersionNumber($version);
+ if (version_compare($mariaDbVersion, '10.6.0', '>=')) {
+ return new MariaDb1060Platform();
+ }
+
if (version_compare($mariaDbVersion, '10.5.2', '>=')) {
return new MariaDb1052Platform();
}
diff --git a/src/Id/TableGenerator.php b/src/Id/TableGenerator.php
index 7d7f210e219..51e541d54ff 100644
--- a/src/Id/TableGenerator.php
+++ b/src/Id/TableGenerator.php
@@ -6,7 +6,6 @@
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
-use Doctrine\DBAL\LockMode;
use Doctrine\Deprecations\Deprecation;
use Throwable;
@@ -115,11 +114,13 @@ public function nextValue($sequence)
$this->conn->beginTransaction();
try {
- $platform = $this->conn->getDatabasePlatform();
- $sql = 'SELECT sequence_value, sequence_increment_by'
- . ' FROM ' . $platform->appendLockHint($this->generatorTableName, LockMode::PESSIMISTIC_WRITE)
- . ' WHERE sequence_name = ? ' . $platform->getWriteLockSQL();
- $row = $this->conn->fetchAssociative($sql, [$sequence]);
+ $row = $this->conn->createQueryBuilder()
+ ->select('sequence_value', 'sequence_increment_by')
+ ->from($this->generatorTableName)
+ ->where('sequence_name = ?')
+ ->forUpdate()
+ ->setParameter(1, $sequence)
+ ->fetchAssociative();
if ($row !== false) {
$row = array_change_key_case($row, CASE_LOWER);
diff --git a/src/Platforms/AbstractMySQLPlatform.php b/src/Platforms/AbstractMySQLPlatform.php
index bc409bdf4cb..8651b82783d 100644
--- a/src/Platforms/AbstractMySQLPlatform.php
+++ b/src/Platforms/AbstractMySQLPlatform.php
@@ -11,6 +11,8 @@
use Doctrine\DBAL\Schema\MySQLSchemaManager;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\TextType;
@@ -541,6 +543,11 @@ protected function _getCreateTableSQL($name, array $columns, array $options = []
return $sql;
}
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null);
+ }
+
/**
* {@inheritDoc}
*
diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php
index eba3aefc890..c77882a1a53 100644
--- a/src/Platforms/AbstractPlatform.php
+++ b/src/Platforms/AbstractPlatform.php
@@ -29,6 +29,8 @@
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Schema\UniqueConstraint;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\SQL\Parser;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
@@ -1758,10 +1760,19 @@ abstract public function getCurrentDatabaseExpression(): string;
/**
* Returns the FOR UPDATE expression.
*
+ * @deprecated This API is not portable. Use {@link QueryBuilder::forUpdate()}` instead.
+ *
* @return string
*/
public function getForUpdateSQL()
{
+ Deprecation::triggerIfCalledFromOutside(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/6191',
+ '%s is deprecated as non-portable.',
+ __METHOD__,
+ );
+
return 'FOR UPDATE';
}
@@ -1793,10 +1804,19 @@ public function appendLockHint(string $fromClause, int $lockMode): string
* This defaults to the ANSI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database
* vendors allow to lighten this constraint up to be a real read lock.
*
+ * @deprecated This API is not portable.
+ *
* @return string
*/
public function getReadLockSQL()
{
+ Deprecation::trigger(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/6191',
+ '%s is deprecated as non-portable.',
+ __METHOD__,
+ );
+
return $this->getForUpdateSQL();
}
@@ -1805,10 +1825,19 @@ public function getReadLockSQL()
*
* The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ANSI SQL standard.
*
+ * @deprecated This API is not portable.
+ *
* @return string
*/
public function getWriteLockSQL()
{
+ Deprecation::trigger(
+ 'doctrine/dbal',
+ 'https://github.com/doctrine/dbal/pull/6191',
+ '%s is deprecated as non-portable.',
+ __METHOD__,
+ );
+
return $this->getForUpdateSQL();
}
@@ -2052,6 +2081,11 @@ public function getCreateTableSQL(Table $table, $createFlags = self::CREATE_INDE
);
}
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED');
+ }
+
/**
* @internal
*
diff --git a/src/Platforms/DB2Platform.php b/src/Platforms/DB2Platform.php
index b203ce8a052..64fabc32d0a 100644
--- a/src/Platforms/DB2Platform.php
+++ b/src/Platforms/DB2Platform.php
@@ -9,6 +9,8 @@
use Doctrine\DBAL\Schema\Identifier;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
@@ -974,8 +976,15 @@ public function prefersIdentityColumns()
return true;
}
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null);
+ }
+
/**
* {@inheritDoc}
+ *
+ * @deprecated This API is not portable.
*/
public function getForUpdateSQL()
{
diff --git a/src/Platforms/MariaDb1060Platform.php b/src/Platforms/MariaDb1060Platform.php
new file mode 100644
index 00000000000..82d11f21d41
--- /dev/null
+++ b/src/Platforms/MariaDb1060Platform.php
@@ -0,0 +1,16 @@
+platform = $platform;
+ }
+
+ public function buildSQL(SelectQuery $query): string
+ {
+ $parts = ['SELECT'];
+
+ if ($query->isDistinct()) {
+ $parts[] = 'DISTINCT';
+ }
+
+ $parts[] = implode(', ', $query->getColumns());
+
+ $from = $query->getFrom();
+
+ if (count($from) > 0) {
+ $parts[] = 'FROM ' . implode(', ', $from);
+ }
+
+ $forUpdate = $query->getForUpdate();
+
+ if ($forUpdate !== null) {
+ $with = ['UPDLOCK', 'ROWLOCK'];
+
+ if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) {
+ $with[] = 'READPAST';
+ }
+
+ $parts[] = 'WITH (' . implode(', ', $with) . ')';
+ }
+
+ $where = $query->getWhere();
+
+ if ($where !== null) {
+ $parts[] = 'WHERE ' . $where;
+ }
+
+ $groupBy = $query->getGroupBy();
+
+ if (count($groupBy) > 0) {
+ $parts[] = 'GROUP BY ' . implode(', ', $groupBy);
+ }
+
+ $having = $query->getHaving();
+
+ if ($having !== null) {
+ $parts[] = 'HAVING ' . $having;
+ }
+
+ $orderBy = $query->getOrderBy();
+
+ if (count($orderBy) > 0) {
+ $parts[] = 'ORDER BY ' . implode(', ', $orderBy);
+ }
+
+ $sql = implode(' ', $parts);
+ $limit = $query->getLimit();
+
+ if ($limit->isDefined()) {
+ $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult());
+ }
+
+ return $sql;
+ }
+}
diff --git a/src/Platforms/SQLServerPlatform.php b/src/Platforms/SQLServerPlatform.php
index 113055ba896..c8cd20b561c 100644
--- a/src/Platforms/SQLServerPlatform.php
+++ b/src/Platforms/SQLServerPlatform.php
@@ -5,6 +5,7 @@
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception\InvalidLockMode;
use Doctrine\DBAL\LockMode;
+use Doctrine\DBAL\Platforms\SQLServer\SQL\Builder\SQLServerSelectSQLBuilder;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ColumnDiff;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
@@ -14,6 +15,7 @@
use Doctrine\DBAL\Schema\SQLServerSchemaManager;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;
@@ -49,6 +51,11 @@
*/
class SQLServerPlatform extends AbstractPlatform
{
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new SQLServerSelectSQLBuilder($this);
+ }
+
/**
* {@inheritDoc}
*/
@@ -1610,6 +1617,8 @@ public function appendLockHint(string $fromClause, int $lockMode): string
/**
* {@inheritDoc}
+ *
+ * @deprecated This API is not portable.
*/
public function getForUpdateSQL()
{
diff --git a/src/Platforms/SqlitePlatform.php b/src/Platforms/SqlitePlatform.php
index 5acefc5c82f..d951662ae10 100644
--- a/src/Platforms/SqlitePlatform.php
+++ b/src/Platforms/SqlitePlatform.php
@@ -14,6 +14,8 @@
use Doctrine\DBAL\Schema\SqliteSchemaManager;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Schema\TableDiff;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
use Doctrine\DBAL\Types\IntegerType;
@@ -193,6 +195,12 @@ public function getCurrentDatabaseExpression(): string
return "'main'";
}
+ /** @link https://www2.sqlite.org/cvstrac/wiki?p=UnsupportedSql */
+ public function createSelectSQLBuilder(): SelectSQLBuilder
+ {
+ return new DefaultSelectSQLBuilder($this, null, null);
+ }
+
/**
* {@inheritDoc}
*/
@@ -734,6 +742,8 @@ public static function udfLocate($str, $substr, $offset = 0)
/**
* {@inheritDoc}
+ *
+ * @deprecated This API is not portable.
*/
public function getForUpdateSQL()
{
diff --git a/src/Query/ForUpdate.php b/src/Query/ForUpdate.php
new file mode 100644
index 00000000000..fe54df909ee
--- /dev/null
+++ b/src/Query/ForUpdate.php
@@ -0,0 +1,21 @@
+conflictResolutionMode = $conflictResolutionMode;
+ }
+
+ public function getConflictResolutionMode(): int
+ {
+ return $this->conflictResolutionMode;
+ }
+}
diff --git a/src/Query/ForUpdate/ConflictResolutionMode.php b/src/Query/ForUpdate/ConflictResolutionMode.php
new file mode 100644
index 00000000000..f968f7b941d
--- /dev/null
+++ b/src/Query/ForUpdate/ConflictResolutionMode.php
@@ -0,0 +1,27 @@
+maxResults = $maxResults;
+ $this->firstResult = $firstResult;
+ }
+
+ public function isDefined(): bool
+ {
+ return $this->maxResults !== null || $this->firstResult !== 0;
+ }
+
+ public function getMaxResults(): ?int
+ {
+ return $this->maxResults;
+ }
+
+ public function getFirstResult(): int
+ {
+ return $this->firstResult;
+ }
+}
diff --git a/src/Query/QueryBuilder.php b/src/Query/QueryBuilder.php
index c4d67cf5d39..6799c12c5e2 100644
--- a/src/Query/QueryBuilder.php
+++ b/src/Query/QueryBuilder.php
@@ -8,6 +8,7 @@
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
+use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\Statement;
use Doctrine\DBAL\Types\Type;
@@ -70,16 +71,17 @@ class QueryBuilder
* The default values of SQL parts collection
*/
private const SQL_PARTS_DEFAULTS = [
- 'select' => [],
- 'distinct' => false,
- 'from' => [],
- 'join' => [],
- 'set' => [],
- 'where' => null,
- 'groupBy' => [],
- 'having' => null,
- 'orderBy' => [],
- 'values' => [],
+ 'select' => [],
+ 'distinct' => false,
+ 'from' => [],
+ 'join' => [],
+ 'set' => [],
+ 'where' => null,
+ 'groupBy' => [],
+ 'having' => null,
+ 'orderBy' => [],
+ 'values' => [],
+ 'for_update' => null,
];
/**
@@ -591,6 +593,20 @@ public function getMaxResults()
return $this->maxResults;
}
+ /**
+ * Locks the queried rows for a subsequent update.
+ *
+ * @return $this
+ */
+ public function forUpdate(int $conflictResolutionMode = ConflictResolutionMode::ORDINARY): self
+ {
+ $this->state = self::STATE_DIRTY;
+
+ $this->sqlParts['for_update'] = new ForUpdate($conflictResolutionMode);
+
+ return $this;
+ }
+
/**
* Either appends to or replaces a single, generic query part.
*
@@ -1475,27 +1491,24 @@ public function resetOrderBy(): self
return $this;
}
- /** @throws QueryException */
+ /** @throws Exception */
private function getSQLForSelect(): string
{
- $query = 'SELECT ' . ($this->sqlParts['distinct'] ? 'DISTINCT ' : '') .
- implode(', ', $this->sqlParts['select']);
-
- $query .= ($this->sqlParts['from'] ? ' FROM ' . implode(', ', $this->getFromClauses()) : '')
- . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '')
- . ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '')
- . ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '')
- . ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : '');
-
- if ($this->isLimitQuery()) {
- return $this->connection->getDatabasePlatform()->modifyLimitQuery(
- $query,
- $this->maxResults,
- $this->firstResult,
+ return $this->connection->getDatabasePlatform()
+ ->createSelectSQLBuilder()
+ ->buildSQL(
+ new SelectQuery(
+ $this->sqlParts['distinct'],
+ $this->sqlParts['select'],
+ $this->getFromClauses(),
+ $this->sqlParts['where'],
+ $this->sqlParts['groupBy'],
+ $this->sqlParts['having'],
+ $this->sqlParts['orderBy'],
+ new Limit($this->maxResults, $this->firstResult),
+ $this->sqlParts['for_update'],
+ ),
);
- }
-
- return $query;
}
/**
@@ -1542,11 +1555,6 @@ private function verifyAllAliasesAreKnown(array $knownAliases): void
}
}
- private function isLimitQuery(): bool
- {
- return $this->maxResults !== null || $this->firstResult !== 0;
- }
-
/**
* Converts this instance into an INSERT string in SQL.
*/
diff --git a/src/Query/SelectQuery.php b/src/Query/SelectQuery.php
new file mode 100644
index 00000000000..ed9a7bc2dfb
--- /dev/null
+++ b/src/Query/SelectQuery.php
@@ -0,0 +1,107 @@
+distinct = $distinct;
+ $this->columns = $columns;
+ $this->from = $from;
+ $this->where = $where;
+ $this->groupBy = $groupBy;
+ $this->having = $having;
+ $this->orderBy = $orderBy;
+ $this->limit = $limit;
+ $this->forUpdate = $forUpdate;
+ }
+
+ public function isDistinct(): bool
+ {
+ return $this->distinct;
+ }
+
+ /** @return string[] */
+ public function getColumns(): array
+ {
+ return $this->columns;
+ }
+
+ /** @return string[] */
+ public function getFrom(): array
+ {
+ return $this->from;
+ }
+
+ public function getWhere(): ?string
+ {
+ return $this->where;
+ }
+
+ /** @return string[] */
+ public function getGroupBy(): array
+ {
+ return $this->groupBy;
+ }
+
+ public function getHaving(): ?string
+ {
+ return $this->having;
+ }
+
+ /** @return string[] */
+ public function getOrderBy(): array
+ {
+ return $this->orderBy;
+ }
+
+ public function getLimit(): Limit
+ {
+ return $this->limit;
+ }
+
+ public function getForUpdate(): ?ForUpdate
+ {
+ return $this->forUpdate;
+ }
+}
diff --git a/src/SQL/Builder/DefaultSelectSQLBuilder.php b/src/SQL/Builder/DefaultSelectSQLBuilder.php
new file mode 100644
index 00000000000..9dcb7a197fa
--- /dev/null
+++ b/src/SQL/Builder/DefaultSelectSQLBuilder.php
@@ -0,0 +1,95 @@
+platform = $platform;
+ $this->forUpdateSQL = $forUpdateSQL;
+ $this->skipLockedSQL = $skipLockedSQL;
+ }
+
+ /** @throws Exception */
+ public function buildSQL(SelectQuery $query): string
+ {
+ $parts = ['SELECT'];
+
+ if ($query->isDistinct()) {
+ $parts[] = 'DISTINCT';
+ }
+
+ $parts[] = implode(', ', $query->getColumns());
+
+ $from = $query->getFrom();
+
+ if (count($from) > 0) {
+ $parts[] = 'FROM ' . implode(', ', $from);
+ }
+
+ $where = $query->getWhere();
+
+ if ($where !== null) {
+ $parts[] = 'WHERE ' . $where;
+ }
+
+ $groupBy = $query->getGroupBy();
+
+ if (count($groupBy) > 0) {
+ $parts[] = 'GROUP BY ' . implode(', ', $groupBy);
+ }
+
+ $having = $query->getHaving();
+
+ if ($having !== null) {
+ $parts[] = 'HAVING ' . $having;
+ }
+
+ $orderBy = $query->getOrderBy();
+
+ if (count($orderBy) > 0) {
+ $parts[] = 'ORDER BY ' . implode(', ', $orderBy);
+ }
+
+ $sql = implode(' ', $parts);
+ $limit = $query->getLimit();
+
+ if ($limit->isDefined()) {
+ $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult());
+ }
+
+ $forUpdate = $query->getForUpdate();
+
+ if ($forUpdate !== null) {
+ if ($this->forUpdateSQL === null) {
+ throw Exception::notSupported('FOR UPDATE');
+ }
+
+ $sql .= ' ' . $this->forUpdateSQL;
+
+ if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) {
+ if ($this->skipLockedSQL === null) {
+ throw Exception::notSupported('SKIP LOCKED');
+ }
+
+ $sql .= ' ' . $this->skipLockedSQL;
+ }
+ }
+
+ return $sql;
+ }
+}
diff --git a/src/SQL/Builder/SelectSQLBuilder.php b/src/SQL/Builder/SelectSQLBuilder.php
new file mode 100644
index 00000000000..ddbf73c03ba
--- /dev/null
+++ b/src/SQL/Builder/SelectSQLBuilder.php
@@ -0,0 +1,12 @@
+addColumn('id', Types::INTEGER);
+ $table->setPrimaryKey(['id']);
+
+ $this->dropAndCreateTable($table);
+
+ $this->connection->insert('for_update', ['id' => 1]);
+ $this->connection->insert('for_update', ['id' => 2]);
+ }
+
+ protected function tearDown(): void
+ {
+ if (! $this->connection->isTransactionActive()) {
+ return;
+ }
+
+ $this->connection->rollBack();
+ }
+
+ public function testForUpdateOrdinary(): void
+ {
+ $platform = $this->connection->getDatabasePlatform();
+
+ if ($platform instanceof SqlitePlatform) {
+ self::markTestSkipped('Skipping on SQLite');
+ }
+
+ $qb1 = $this->connection->createQueryBuilder();
+ $qb1->select('id')
+ ->from('for_update')
+ ->forUpdate();
+
+ self::assertEquals([1, 2], $qb1->fetchFirstColumn());
+ }
+
+ public function testForUpdateSkipLockedWhenSupported(): void
+ {
+ if (! $this->platformSupportsSkipLocked()) {
+ self::markTestSkipped('The database platform does not support SKIP LOCKED.');
+ }
+
+ $qb1 = $this->connection->createQueryBuilder();
+ $qb1->select('id')
+ ->from('for_update')
+ ->where('id = 1')
+ ->forUpdate();
+
+ $this->connection->beginTransaction();
+
+ self::assertEquals([1], $qb1->fetchFirstColumn());
+
+ $params = TestUtil::getConnectionParams();
+
+ if (TestUtil::isDriverOneOf('oci8')) {
+ $params['driverOptions']['exclusive'] = true;
+ }
+
+ $connection2 = DriverManager::getConnection($params);
+
+ $qb2 = $connection2->createQueryBuilder();
+ $qb2->select('id')
+ ->from('for_update')
+ ->orderBy('id')
+ ->forUpdate(ConflictResolutionMode::SKIP_LOCKED);
+
+ self::assertEquals([2], $qb2->fetchFirstColumn());
+ }
+
+ public function testForUpdateSkipLockedWhenNotSupported(): void
+ {
+ if ($this->platformSupportsSkipLocked()) {
+ self::markTestSkipped('The database platform supports SKIP LOCKED.');
+ }
+
+ $qb = $this->connection->createQueryBuilder();
+ $qb->select('id')
+ ->from('for_update')
+ ->forUpdate(ConflictResolutionMode::SKIP_LOCKED);
+
+ self::expectException(Exception::class);
+ $qb->executeQuery();
+ }
+
+ private function platformSupportsSkipLocked(): bool
+ {
+ $platform = $this->connection->getDatabasePlatform();
+
+ if ($platform instanceof DB2Platform) {
+ return false;
+ }
+
+ if ($platform instanceof MySQLPlatform) {
+ if ($platform instanceof MariaDBPlatform) {
+ if (! $platform instanceof MariaDb1060Platform) {
+ return false;
+ }
+ } elseif (! $platform instanceof MySQL80Platform) {
+ return false;
+ }
+ }
+
+ if ($platform instanceof PostgreSQLPlatform && ! $platform instanceof PostgreSQL100Platform) {
+ return false;
+ }
+
+ return ! $platform instanceof SqlitePlatform;
+ }
+}
diff --git a/tests/Query/QueryBuilderTest.php b/tests/Query/QueryBuilderTest.php
index ba774b748aa..a9d488bfc05 100644
--- a/tests/Query/QueryBuilderTest.php
+++ b/tests/Query/QueryBuilderTest.php
@@ -6,10 +6,12 @@
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\ParameterType;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Query\QueryException;
use Doctrine\DBAL\Result;
+use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
@@ -31,9 +33,15 @@ protected function setUp(): void
$expressionBuilder = new ExpressionBuilder($this->conn);
- $this->conn->expects(self::any())
- ->method('getExpressionBuilder')
- ->willReturn($expressionBuilder);
+ $this->conn->method('getExpressionBuilder')
+ ->willReturn($expressionBuilder);
+
+ $platform = $this->createMock(AbstractPlatform::class);
+ $platform->method('createSelectSQLBuilder')
+ ->willReturn(new DefaultSelectSQLBuilder($platform, null, null));
+
+ $this->conn->method('getDatabasePlatform')
+ ->willReturn($platform);
}
public function testSimpleSelectWithoutFrom(): void