Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable skipping locked rows in QueryBuilder #6191

Merged
merged 3 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,9 @@

<!-- TODO for PHPUnit 10 -->
<referencedMethod name="PHPUnit\Framework\MockObject\Builder\InvocationMocker::withConsecutive"/>

<!-- TODO: remove in 4.0.0 -->
<referencedMethod name="Doctrine\DBAL\Platforms\DB2Platform::getForUpdateSQL"/>
</errorLevel>
</DeprecatedMethod>
<DeprecatedProperty>
Expand Down
5 changes: 5 additions & 0 deletions src/Driver/AbstractMySQLDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
1 change: 1 addition & 0 deletions src/Driver/OCI8/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 7 additions & 6 deletions src/Id/TableGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions src/Platforms/AbstractMySQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
*
Expand Down
34 changes: 34 additions & 0 deletions src/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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';
}

Expand Down Expand Up @@ -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();
}

Expand All @@ -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();
}

Expand Down Expand Up @@ -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
*
Expand Down
9 changes: 9 additions & 0 deletions src/Platforms/DB2Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should also add the Deprecation::trigger() call in this method (the call in the parent method won't be triggered as the method is overridden)

*/
public function getForUpdateSQL()
{
Expand Down
16 changes: 16 additions & 0 deletions src/Platforms/MariaDb1060Platform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;

/**
* Provides the behavior, features and SQL dialect of the MariaDB 10.6 (10.6.0 GA) database platform.
*/
class MariaDb1060Platform extends MariaDb1052Platform
{
public function createSelectSQLBuilder(): SelectSQLBuilder
{
return AbstractPlatform::createSelectSQLBuilder();
}
}
6 changes: 6 additions & 0 deletions src/Platforms/MySQL80Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\Deprecations\Deprecation;

/**
Expand All @@ -25,4 +26,9 @@ protected function getReservedKeywordsClass()

return Keywords\MySQL80Keywords::class;
}

public function createSelectSQLBuilder(): SelectSQLBuilder
{
return AbstractPlatform::createSelectSQLBuilder();
}
}
6 changes: 6 additions & 0 deletions src/Platforms/PostgreSQL100Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Platforms\Keywords\PostgreSQL100Keywords;
use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\Deprecations\Deprecation;

/**
Expand All @@ -27,4 +28,9 @@ protected function getReservedKeywordsClass(): string

return PostgreSQL100Keywords::class;
}

public function createSelectSQLBuilder(): SelectSQLBuilder
{
return AbstractPlatform::createSelectSQLBuilder();
}
}
7 changes: 7 additions & 0 deletions src/Platforms/PostgreSQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Doctrine\DBAL\Schema\Sequence;
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\Types\BinaryType;
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\Types;
Expand Down Expand Up @@ -266,6 +268,11 @@ public function hasNativeGuidType()
return true;
}

public function createSelectSQLBuilder(): SelectSQLBuilder
{
return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null);
}

/**
* {@inheritDoc}
*
Expand Down
86 changes: 86 additions & 0 deletions src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Platforms\SQLServer\SQL\Builder;

use Doctrine\DBAL\Platforms\SQLServerPlatform;
use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode;
use Doctrine\DBAL\Query\SelectQuery;
use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;

use function count;
use function implode;

final class SQLServerSelectSQLBuilder implements SelectSQLBuilder
{
private SQLServerPlatform $platform;

/** @internal The SQL builder should be instantiated only by database platforms. */
public function __construct(SQLServerPlatform $platform)
{
$this->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;
}
}