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/Driver/OCI8/Driver.php b/src/Driver/OCI8/Driver.php
index fe091bfa982..650a4f9ab34 100644
--- a/src/Driver/OCI8/Driver.php
+++ b/src/Driver/OCI8/Driver.php
@@ -8,6 +8,7 @@
use SensitiveParameter;
use function oci_connect;
+use function oci_new_connect;
use function oci_pconnect;
use const OCI_NO_AUTO_COMMIT;
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 @@
+assertDriverInstantiatesDatabasePlatform(new Driver\PDO\MySQL\Driver(), $version, $expectedClass);
}
- /** @return array, 2?: string, 3?: bool}> */
+ /** @return array}> */
public static function mySQLVersionProvider(): array
{
return [
- ['5.6.9', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- ['5.7', MySQL57Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true],
- ['5.7.0', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- ['5.7.8', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- ['5.7.9', MySQL57Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- ['5.7.10', MySQL57Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- ['8', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true],
- ['8.0', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true],
- ['8.0.11', MySQL80Platform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
+ ['5.6.9', MySQLPlatform::class],
+ ['5.7', MySQL57Platform::class],
+ ['5.7.0', MySQLPlatform::class],
+ ['5.7.8', MySQLPlatform::class],
+ ['5.7.9', MySQL57Platform::class],
+ ['5.7.10', MySQL57Platform::class],
+ ['8', MySQL80Platform::class],
+ ['8.0', MySQL80Platform::class],
+ ['8.0.11', MySQL80Platform::class],
['6', MySQL57Platform::class],
- ['10.0.15-MariaDB-1~wheezy', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- ['5.5.5-10.1.25-MariaDB', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- ['10.1.2a-MariaDB-a1~lenny-log', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- ['5.5.40-MariaDB-1~wheezy', MySQLPlatform::class, 'https://github.com/doctrine/dbal/pull/5779', false],
- [
- '5.5.5-MariaDB-10.2.8+maria~xenial-log',
- MariaDb1027Platform::class,
- 'https://github.com/doctrine/dbal/pull/5779',
- false,
- ],
- [
- '10.2.8-MariaDB-10.2.8+maria~xenial-log',
- MariaDb1027Platform::class,
- 'https://github.com/doctrine/dbal/pull/5779',
- false,
- ],
- [
- '10.2.8-MariaDB-1~lenny-log',
- MariaDb1027Platform::class,
- 'https://github.com/doctrine/dbal/pull/5779',
- false,
- ],
- ['mariadb-10.9.3', MariaDB1052Platform::class, 'https://github.com/doctrine/dbal/pull/5779', true],
- [
- '10.5.2-MariaDB-1~lenny-log',
- MariaDB1052Platform::class,
- 'https://github.com/doctrine/dbal/pull/5779',
- false,
- ],
- [
- '11.0.2-MariaDB-1:11.0.2+maria~ubu2204',
- MariaDB1052Platform::class,
- 'https://github.com/doctrine/dbal/pull/5779',
- false,
- ],
+ ['10.0.15-MariaDB-1~wheezy', MySQLPlatform::class],
+ ['5.5.5-10.1.25-MariaDB', MySQLPlatform::class],
+ ['10.1.2a-MariaDB-a1~lenny-log', MySQLPlatform::class],
+ ['5.5.40-MariaDB-1~wheezy', MySQLPlatform::class],
+ ['5.5.5-MariaDB-10.2.8+maria~xenial-log', MariaDb1027Platform::class],
+ ['10.2.8-MariaDB-10.2.8+maria~xenial-log', MariaDb1027Platform::class],
+ ['10.2.8-MariaDB-1~lenny-log', MariaDb1027Platform::class],
+ ['10.5.2-MariaDB-1~lenny-log', MariaDB1052Platform::class],
+ ['mariadb-10.6.0', MariaDb1060Platform::class],
+ ['mariadb-10.9.3', MariaDb1060Platform::class],
+ ['11.0.2-MariaDB-1:11.0.2+maria~ubu2204', MariaDb1060Platform::class],
];
}
@@ -118,13 +95,13 @@ public function testIBMDB2(string $version, string $expectedClass): void
$this->assertDriverInstantiatesDatabasePlatform(new Driver\IBMDB2\Driver(), $version, $expectedClass);
}
- /** @return array, 2?: string, 3?: bool}> */
+ /** @return array}> */
public static function db2VersionProvider(): array
{
return [
- ['10.1.0', DB2Platform::class, 'https://github.com/doctrine/dbal/pull/5156', true],
- ['10.1.0.0', DB2Platform::class, 'https://github.com/doctrine/dbal/pull/5156', true],
- ['DB2/LINUXX8664 10.1.0.0', DB2Platform::class, 'https://github.com/doctrine/dbal/pull/5156', true],
+ ['10.1.0', DB2Platform::class],
+ ['10.1.0.0', DB2Platform::class],
+ ['DB2/LINUXX8664 10.1.0.0', DB2Platform::class],
['11.1.0', DB2111Platform::class],
['11.1.0.0', DB2111Platform::class],
['DB2/LINUXX8664 11.1.0.0', DB2111Platform::class],
diff --git a/tests/Functional/Query/QueryBuilderTest.php b/tests/Functional/Query/QueryBuilderTest.php
new file mode 100644
index 00000000000..16cc0f20dd2
--- /dev/null
+++ b/tests/Functional/Query/QueryBuilderTest.php
@@ -0,0 +1,134 @@
+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