Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion src/Driver/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Cycle\Database\Driver;

use Cycle\Database\Query\Enum\LockMode;
use Cycle\Database\Query\Enum\LockBehavior;
use Cycle\Database\Exception\CompilerException;
use Cycle\Database\Injection\FragmentInterface;
use Cycle\Database\Injection\Parameter;
Expand Down Expand Up @@ -197,7 +199,7 @@ protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens
$this->optional("\n", $this->excepts($params, $q, $tokens['except'])),
$this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])),
$this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset'])),
$this->optional(' ', $tokens['forUpdate'] ? 'FOR UPDATE' : ''),
$this->optional(' ', $this->forUpdate($tokens['forUpdate'])),
);
}

Expand Down Expand Up @@ -611,6 +613,46 @@ protected function compileJsonOrderBy(string $path): string|FragmentInterface
return $path;
}

/**
* @param array{mode: LockMode, behavior: LockBehavior}|null $forUpdate
*/
protected function forUpdate(?array $forUpdate): string
{
if ($forUpdate !== null) {
$arguments = [];

switch ($forUpdate['mode']) {
case LockMode::Update:
$arguments[] = 'UPDATE';
break;
case LockMode::Share:
$arguments[] = 'SHARE';
break;
case LockMode::NoKeyUpdate:
$arguments[] = 'NO KEY UPDATE';
break;
case LockMode::KeyShare:
$arguments[] = 'KEY SHARE';
break;
}

switch ($forUpdate['behavior']) {
case LockBehavior::Wait:
break;
case LockBehavior::NoWait:
$arguments[] = 'NOWAIT';
break;
case LockBehavior::SkipLocked:
$arguments[] = 'SKIP LOCKED';
break;
}

return \sprintf('FOR %s', \implode(' ', $arguments));
}

return '';
}

private function arrayToInOperator(QueryParameters $params, Quoter $q, array $values, bool $in): string
{
$operator = $in ? 'IN' : 'NOT IN';
Expand Down
11 changes: 9 additions & 2 deletions src/Driver/CompilerCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,18 @@ protected function hashInsertQuery(QueryParameters $params, array $tokens): stri
*/
protected function hashSelectQuery(QueryParameters $params, array $tokens): string
{
$forUpdate = $tokens['forUpdate'];
if ($forUpdate !== null) {
$forUpdate = $forUpdate['mode']->name . '_' . $forUpdate['behavior']->name;
} else {
$forUpdate = '';
}

// stable part of hash
if (\is_array($tokens['distinct']) && isset($tokens['distinct']['on'])) {
$hash = 's_' . $tokens['forUpdate'] . '_on_' . $tokens['distinct']['on'];
$hash = 's_' . $forUpdate . '_on_' . $tokens['distinct']['on'];
} else {
$hash = 's_' . $tokens['forUpdate'] . '_' . $tokens['distinct'];
$hash = 's_' . $forUpdate . '_' . $tokens['distinct'];
}

foreach ($tokens['from'] as $table) {
Expand Down
38 changes: 38 additions & 0 deletions src/Driver/MySQL/MySQLCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Cycle\Database\Driver\MySQL;

use Cycle\Database\Query\Enum\LockMode;
use Cycle\Database\Query\Enum\LockBehavior;
use Cycle\Database\Driver\CachingCompilerInterface;
use Cycle\Database\Driver\Compiler;
use Cycle\Database\Driver\MySQL\Injection\CompileJson;
Expand Down Expand Up @@ -69,4 +71,40 @@ protected function compileJsonOrderBy(string $path): FragmentInterface
{
return new CompileJson($path);
}

/**
* @param array{mode: LockMode, behavior: LockBehavior}|null $forUpdate
*/
protected function forUpdate(?array $forUpdate): string
{
if ($forUpdate !== null) {
$arguments = [];

switch ($forUpdate['mode']) {
case LockMode::Share:
case LockMode::KeyShare:
$arguments[] = 'SHARE';
break;
case LockMode::Update:
case LockMode::NoKeyUpdate:
$arguments[] = 'UPDATE';
break;
}

switch ($forUpdate['behavior']) {
case LockBehavior::Wait:
break;
case LockBehavior::NoWait:
$arguments[] = 'NOWAIT';
break;
case LockBehavior::SkipLocked:
$arguments[] = 'SKIP LOCKED';
break;
}

return \sprintf('FOR %s', \implode(' ', $arguments));
}

return '';
}
}
42 changes: 41 additions & 1 deletion src/Driver/SQLServer/SQLServerCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

use Cycle\Database\Driver\Compiler;
use Cycle\Database\Driver\Quoter;
use Cycle\Database\Query\Enum\LockMode;
use Cycle\Database\Query\Enum\LockBehavior;
use Cycle\Database\Driver\SQLServer\Injection\CompileJson;
use Cycle\Database\Injection\Fragment;
use Cycle\Database\Injection\FragmentInterface;
Expand Down Expand Up @@ -164,6 +166,42 @@ protected function compileJsonOrderBy(string $path): FragmentInterface
return new CompileJson($path);
}

/**
* @param array{mode: LockMode, behavior: LockBehavior}|null $forUpdate
*/
protected function forUpdate(?array $forUpdate): string
{
if ($forUpdate !== null) {
$arguments = [];

switch ($forUpdate['mode']) {
case LockMode::Share:
case LockMode::KeyShare:
$arguments[] = 'HOLDLOCK';
break;
case LockMode::Update:
case LockMode::NoKeyUpdate:
$arguments[] = 'UPDLOCK';
break;
}

switch ($forUpdate['behavior']) {
case LockBehavior::Wait:
break;
case LockBehavior::NoWait:
$arguments[] = 'NOWAIT';
break;
case LockBehavior::SkipLocked:
$arguments[] = 'READPAST';
break;
}

return \sprintf('WITH(%s)', \implode(',', $arguments));
}

return '';
}

private function baseSelect(QueryParameters $params, Quoter $q, array $tokens): string
{
// This statement(s) parts should be processed first to define set of table and column aliases
Expand All @@ -175,12 +213,14 @@ private function baseSelect(QueryParameters $params, Quoter $q, array $tokens):
$this->nameWithAlias(new QueryParameters(), $q, $join['outer'], $join['alias'], true);
}



return \sprintf(
"SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s%s%s",
$this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])),
$this->columns($params, $q, $tokens['columns']),
\implode(', ', $tables),
$this->optional(' ', $tokens['forUpdate'] ? 'WITH (UPDLOCK,ROWLOCK)' : '', ' '),
$this->optional(' ', $this->forUpdate($tokens['forUpdate']), ' '),
$this->optional(' ', $this->joins($params, $q, $tokens['join']), ' '),
$this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])),
$this->optional("\nGROUP BY", $this->groupBy($params, $q, $tokens['groupBy']), ' '),
Expand Down
2 changes: 1 addition & 1 deletion src/Driver/SQLite/SQLiteCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ protected function limit(QueryParameters $params, Quoter $q, ?int $limit = null,
protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens): string
{
// FOR UPDATE is not available
$tokens['forUpdate'] = false;
$tokens['forUpdate'] = null;

return parent::selectQuery($params, $q, $tokens);
}
Expand Down
44 changes: 44 additions & 0 deletions src/Query/Enum/LockBehavior.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Cycle\Database\Query\Enum;

/**
* Lock wait behavior when row is already locked.
*/
enum LockBehavior
{
/**
* Default. Block until lock is released.
*
* Supports:
* - MSSQL +
* - MySQL +
* - SQLITE -
* - PostgreSQL +
*/
case Wait;

/**
* Fail immediately if row is locked.
*
* Supports:
* - MSSQL +
* - MySQL +
* - SQLITE -
* - PostgreSQL +
*/
case NoWait;

/**
* Skip locked rows, return only unlocked. Useful for job queues.
*
* Supports:
* - MSSQL +
* - MySQL +
* - SQLITE -
* - PostgreSQL +
*/
case SkipLocked;
}
56 changes: 56 additions & 0 deletions src/Query/Enum/LockMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Cycle\Database\Query\Enum;

/**
* Row-level lock strength.
*/
enum LockMode
{
/**
* Exclusive lock.
*
* Supports:
* - MSSQL +
* - MySQL +
* - SQLITE -
* - PostgreSQL +
*/
case Update;

/**
* Shared lock.
*
* Supports:
* - MSSQL +
* - MySQL +
* - SQLITE -
* - PostgreSQL +
*/
case Share;

/**
* Weakest lock - blocks only DELETE and PK/FK updates.
*
* Supports:
* - MSSQL - (Will be compiled as self::Share)
* - MySQL - (Will be compiled as self::Share)
* - SQLITE -
* - PostgreSQL +
*/
case KeyShare;

/**
* Like LockMode::Update, but doesn't block PK/FK columns.
* (Use when not modifying PK/FK columns)
*
* Supports:
* - MSSQL - (Will be compiled as self::Update)
* - MySQL - (Will be compiled as self::Update)
* - SQLITE -
* - PostgreSQL +
*/
case NoKeyUpdate;
}
23 changes: 18 additions & 5 deletions src/Query/SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

namespace Cycle\Database\Query;

use Cycle\Database\Query\Enum\LockMode;
use Cycle\Database\Injection\Expression;
use Cycle\Database\Injection\Fragment;
use Cycle\Database\Injection\SubQuery;
use Cycle\Database\Query\Enum\LockBehavior;
use Cycle\Database\Query\Traits\WhereJsonTrait;
use Cycle\Database\Driver\CompilerInterface;
use Cycle\Database\Injection\FragmentInterface;
Expand Down Expand Up @@ -53,7 +55,10 @@ class SelectQuery extends ActiveQuery implements
protected array $orderBy = [];

protected array $groupBy = [];
protected bool $forUpdate = false;

/** @var array{mode: LockMode, behavior: LockBehavior}|null */
protected ?array $forUpdate = null;

private ?int $limit = null;
private ?int $offset = null;

Expand Down Expand Up @@ -125,11 +130,19 @@ public function getColumns(): array
}

/**
* Select entities for the following update.
* Add row-level locking clause.
*
* @param LockMode $mode Lock strength. SQLite: ignored, uses BEGIN IMMEDIATE.
* @param LockBehavior $behavior Wait strategy. SQLite: only Wait supported.
*/
public function forUpdate(): self
{
$this->forUpdate = true;
public function forUpdate(
LockMode $mode = LockMode::Update,
LockBehavior $behavior = LockBehavior::Wait,
): self {
$this->forUpdate = [
'mode' => $mode,
'behavior' => $behavior,
];

return $this;
}
Expand Down
Loading
Loading