Skip to content

Commit

Permalink
Enable skipping locked rows in QueryBuilder
Browse files Browse the repository at this point in the history
Co-authored-by: Herberto Graca <herberto.graca@lendable.co.uk>
  • Loading branch information
morozov and Herberto Graca committed Oct 16, 2023
1 parent f4fb793 commit eece112
Show file tree
Hide file tree
Showing 24 changed files with 671 additions and 43 deletions.
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 reset methods from `QueryBuilder`

`QueryBuilder::resetQueryParts()` 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 @@ -509,6 +509,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
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 @@ -522,6 +524,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__,
);

Check warning on line 1774 in src/Platforms/AbstractPlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/AbstractPlatform.php#L1769-L1774

Added lines #L1769 - L1774 were not covered by tests

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__,
);

Check warning on line 1818 in src/Platforms/AbstractPlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/AbstractPlatform.php#L1813-L1818

Added lines #L1813 - L1818 were not covered by tests

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__,
);

Check warning on line 1839 in src/Platforms/AbstractPlatform.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/AbstractPlatform.php#L1834-L1839

Added lines #L1834 - L1839 were not covered by tests

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.
*/
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';

Check warning on line 30 in src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php#L30

Added line #L30 was not covered by tests
}

$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);

Check warning on line 62 in src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php#L62

Added line #L62 was not covered by tests
}

$having = $query->getHaving();

if ($having !== null) {
$parts[] = 'HAVING ' . $having;

Check warning on line 68 in src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php#L68

Added line #L68 was not covered by tests
}

$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());

Check warning on line 81 in src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php

View check run for this annotation

Codecov / codecov/patch

src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php#L81

Added line #L81 was not covered by tests
}

return $sql;
}
}
9 changes: 9 additions & 0 deletions src/Platforms/SQLServerPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -49,6 +51,11 @@
*/
class SQLServerPlatform extends AbstractPlatform
{
public function createSelectSQLBuilder(): SelectSQLBuilder
{
return new SQLServerSelectSQLBuilder($this);
}

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -1610,6 +1617,8 @@ public function appendLockHint(string $fromClause, int $lockMode): string

/**
* {@inheritDoc}
*
* @deprecated This API is not portable.
*/
public function getForUpdateSQL()
{
Expand Down
Loading

0 comments on commit eece112

Please sign in to comment.