Skip to content

Commit 3a6b51a

Browse files
committed
feat: Added 'FOR UPDATE SKIP LOCKED' and 'FOR UPDATE NOWAIT' to SelectQuery
1 parent d0db035 commit 3a6b51a

14 files changed

Lines changed: 524 additions & 46 deletions

File tree

src/Driver/Compiler.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Cycle\Database\Driver;
1313

14+
use Cycle\Database\Query\Enum\LockMode;
15+
use Cycle\Database\Query\Enum\LockBehavior;
1416
use Cycle\Database\Exception\CompilerException;
1517
use Cycle\Database\Injection\FragmentInterface;
1618
use Cycle\Database\Injection\Parameter;
@@ -197,7 +199,7 @@ protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens
197199
$this->optional("\n", $this->excepts($params, $q, $tokens['except'])),
198200
$this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])),
199201
$this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset'])),
200-
$this->optional(' ', $tokens['forUpdate'] ? 'FOR UPDATE' : ''),
202+
$this->optional(' ', $this->forUpdate($tokens['forUpdate'])),
201203
);
202204
}
203205

@@ -611,6 +613,46 @@ protected function compileJsonOrderBy(string $path): string|FragmentInterface
611613
return $path;
612614
}
613615

616+
/**
617+
* @param array{mode: LockMode, behavior: LockBehavior}|null $forUpdate
618+
*/
619+
protected function forUpdate(?array $forUpdate): string
620+
{
621+
if ($forUpdate !== null) {
622+
$arguments = [];
623+
624+
switch ($forUpdate['mode']) {
625+
case LockMode::Update:
626+
$arguments[] = 'UPDATE';
627+
break;
628+
case LockMode::Share:
629+
$arguments[] = 'SHARE';
630+
break;
631+
case LockMode::NoKeyUpdate:
632+
$arguments[] = 'NO KEY UPDATE';
633+
break;
634+
case LockMode::KeyShare:
635+
$arguments[] = 'KEY SHARE';
636+
break;
637+
}
638+
639+
switch ($forUpdate['behavior']) {
640+
case LockBehavior::Wait:
641+
break;
642+
case LockBehavior::NoWait:
643+
$arguments[] = 'NOWAIT';
644+
break;
645+
case LockBehavior::SkipLocked:
646+
$arguments[] = 'SKIP LOCKED';
647+
break;
648+
}
649+
650+
return \sprintf('FOR %s', \implode(' ', $arguments));
651+
}
652+
653+
return '';
654+
}
655+
614656
private function arrayToInOperator(QueryParameters $params, Quoter $q, array $values, bool $in): string
615657
{
616658
$operator = $in ? 'IN' : 'NOT IN';

src/Driver/CompilerCache.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,18 @@ protected function hashInsertQuery(QueryParameters $params, array $tokens): stri
151151
*/
152152
protected function hashSelectQuery(QueryParameters $params, array $tokens): string
153153
{
154+
$forUpdate = $tokens['forUpdate'];
155+
if ($forUpdate !== null) {
156+
$forUpdate = $forUpdate['mode']->name . '_' . $forUpdate['behavior']->name;
157+
} else {
158+
$forUpdate = '';
159+
}
160+
154161
// stable part of hash
155162
if (\is_array($tokens['distinct']) && isset($tokens['distinct']['on'])) {
156-
$hash = 's_' . $tokens['forUpdate'] . '_on_' . $tokens['distinct']['on'];
163+
$hash = 's_' . $forUpdate . '_on_' . $tokens['distinct']['on'];
157164
} else {
158-
$hash = 's_' . $tokens['forUpdate'] . '_' . $tokens['distinct'];
165+
$hash = 's_' . $forUpdate . '_' . $tokens['distinct'];
159166
}
160167

161168
foreach ($tokens['from'] as $table) {

src/Driver/MySQL/MySQLCompiler.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Cycle\Database\Driver\MySQL;
1313

14+
use Cycle\Database\Query\Enum\LockMode;
15+
use Cycle\Database\Query\Enum\LockBehavior;
1416
use Cycle\Database\Driver\CachingCompilerInterface;
1517
use Cycle\Database\Driver\Compiler;
1618
use Cycle\Database\Driver\MySQL\Injection\CompileJson;
@@ -69,4 +71,40 @@ protected function compileJsonOrderBy(string $path): FragmentInterface
6971
{
7072
return new CompileJson($path);
7173
}
74+
75+
/**
76+
* @param array{mode: LockMode, behavior: LockBehavior}|null $forUpdate
77+
*/
78+
protected function forUpdate(?array $forUpdate): string
79+
{
80+
if ($forUpdate !== null) {
81+
$arguments = [];
82+
83+
switch ($forUpdate['mode']) {
84+
case LockMode::Share:
85+
case LockMode::KeyShare:
86+
$arguments[] = 'SHARE';
87+
break;
88+
case LockMode::Update:
89+
case LockMode::NoKeyUpdate:
90+
$arguments[] = 'UPDATE';
91+
break;
92+
}
93+
94+
switch ($forUpdate['behavior']) {
95+
case LockBehavior::Wait:
96+
break;
97+
case LockBehavior::NoWait:
98+
$arguments[] = 'NOWAIT';
99+
break;
100+
case LockBehavior::SkipLocked:
101+
$arguments[] = 'SKIP LOCKED';
102+
break;
103+
}
104+
105+
return \sprintf('FOR %s', \implode(' ', $arguments));
106+
}
107+
108+
return '';
109+
}
72110
}

src/Driver/Postgres/Query/PostgresSelectQuery.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Cycle\Database\Driver\Postgres\Query;
1313

14+
use Cycle\Database\Query\Enum\LockMode;
15+
use Cycle\Database\Query\Enum\LockBehavior;
1416
use Cycle\Database\Driver\Postgres\Query\Traits\WhereJsonTrait;
1517
use Cycle\Database\Injection\FragmentInterface;
1618
use Cycle\Database\Query\SelectQuery;
@@ -28,4 +30,28 @@ public function distinctOn(FragmentInterface|string $distinctOn): SelectQuery
2830

2931
return $this;
3032
}
33+
34+
public function forShare(
35+
LockBehavior $behavior = LockBehavior::Wait,
36+
bool $keyOnly = false,
37+
): self {
38+
$this->forUpdate = [
39+
'behavior' => $behavior,
40+
'mode' => $keyOnly === true ? LockMode::KeyShare : LockMode::Share,
41+
];
42+
43+
return $this;
44+
}
45+
46+
public function forUpdate(
47+
LockBehavior $behavior = LockBehavior::Wait,
48+
bool $noKey = false,
49+
): self {
50+
$this->forUpdate = [
51+
'behavior' => $behavior,
52+
'mode' => $noKey === true ? LockMode::NoKeyUpdate : LockMode::Update,
53+
];
54+
55+
return $this;
56+
}
3157
}

src/Driver/SQLServer/SQLServerCompiler.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Cycle\Database\Driver\Compiler;
1515
use Cycle\Database\Driver\Quoter;
16+
use Cycle\Database\Query\Enum\LockMode;
17+
use Cycle\Database\Query\Enum\LockBehavior;
1618
use Cycle\Database\Driver\SQLServer\Injection\CompileJson;
1719
use Cycle\Database\Injection\Fragment;
1820
use Cycle\Database\Injection\FragmentInterface;
@@ -164,6 +166,42 @@ protected function compileJsonOrderBy(string $path): FragmentInterface
164166
return new CompileJson($path);
165167
}
166168

169+
/**
170+
* @param array{mode: LockMode, behavior: LockBehavior}|null $forUpdate
171+
*/
172+
protected function forUpdate(?array $forUpdate): string
173+
{
174+
if ($forUpdate !== null) {
175+
$arguments = [];
176+
177+
switch ($forUpdate['mode']) {
178+
case LockMode::Share:
179+
case LockMode::KeyShare:
180+
$arguments[] = 'HOLDLOCK';
181+
break;
182+
case LockMode::Update:
183+
case LockMode::NoKeyUpdate:
184+
$arguments[] = 'UPDLOCK';
185+
break;
186+
}
187+
188+
switch ($forUpdate['behavior']) {
189+
case LockBehavior::Wait:
190+
break;
191+
case LockBehavior::NoWait:
192+
$arguments[] = 'NOWAIT';
193+
break;
194+
case LockBehavior::SkipLocked:
195+
$arguments[] = 'READPAST';
196+
break;
197+
}
198+
199+
return \sprintf('WITH(%s)', \implode(',', $arguments));
200+
}
201+
202+
return '';
203+
}
204+
167205
private function baseSelect(QueryParameters $params, Quoter $q, array $tokens): string
168206
{
169207
// This statement(s) parts should be processed first to define set of table and column aliases
@@ -175,12 +213,14 @@ private function baseSelect(QueryParameters $params, Quoter $q, array $tokens):
175213
$this->nameWithAlias(new QueryParameters(), $q, $join['outer'], $join['alias'], true);
176214
}
177215

216+
217+
178218
return \sprintf(
179219
"SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s%s%s",
180220
$this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])),
181221
$this->columns($params, $q, $tokens['columns']),
182222
\implode(', ', $tables),
183-
$this->optional(' ', $tokens['forUpdate'] ? 'WITH (UPDLOCK,ROWLOCK)' : '', ' '),
223+
$this->optional(' ', $this->forUpdate($tokens['forUpdate']), ' '),
184224
$this->optional(' ', $this->joins($params, $q, $tokens['join']), ' '),
185225
$this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])),
186226
$this->optional("\nGROUP BY", $this->groupBy($params, $q, $tokens['groupBy']), ' '),

src/Driver/SQLite/SQLiteCompiler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ protected function limit(QueryParameters $params, Quoter $q, ?int $limit = null,
5252
protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens): string
5353
{
5454
// FOR UPDATE is not available
55-
$tokens['forUpdate'] = false;
55+
$tokens['forUpdate'] = null;
5656

5757
return parent::selectQuery($params, $q, $tokens);
5858
}

src/Query/Enum/LockBehavior.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Database\Query\Enum;
6+
7+
/**
8+
* Lock wait behavior when row is already locked.
9+
*/
10+
enum LockBehavior
11+
{
12+
/**
13+
* Default. Block until lock is released.
14+
*
15+
* Supports:
16+
* - MSSQL +
17+
* - MySQL +
18+
* - SQLITE -
19+
* - PostgreSQL +
20+
*/
21+
case Wait;
22+
23+
/**
24+
* Fail immediately if row is locked.
25+
*
26+
* Supports:
27+
* - MSSQL +
28+
* - MySQL +
29+
* - SQLITE -
30+
* - PostgreSQL +
31+
*/
32+
case NoWait;
33+
34+
/**
35+
* Skip locked rows, return only unlocked. Useful for job queues.
36+
*
37+
* Supports:
38+
* - MSSQL +
39+
* - MySQL +
40+
* - SQLITE -
41+
* - PostgreSQL +
42+
*/
43+
case SkipLocked;
44+
}

src/Query/Enum/LockMode.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cycle\Database\Query\Enum;
6+
7+
/**
8+
* Row-level lock strength.
9+
*/
10+
enum LockMode
11+
{
12+
/**
13+
* Exclusive lock.
14+
*
15+
* Supports:
16+
* - MSSQL +
17+
* - MySQL +
18+
* - SQLITE -
19+
* - PostgreSQL +
20+
*/
21+
case Update;
22+
23+
/**
24+
* Shared lock.
25+
*
26+
* Supports:
27+
* - MSSQL +
28+
* - MySQL +
29+
* - SQLITE -
30+
* - PostgreSQL +
31+
*/
32+
case Share;
33+
34+
/**
35+
* Weakest lock - blocks only DELETE and PK/FK updates.
36+
*
37+
* Supports:
38+
* - MSSQL - (Will be compiled as self::Share)
39+
* - MySQL - (Will be compiled as self::Share)
40+
* - SQLITE -
41+
* - PostgreSQL +
42+
*/
43+
case KeyShare;
44+
45+
/**
46+
* Like LockMode::Update, but doesn't block PK/FK columns.
47+
* (Use when not modifying PK/FK columns)
48+
*
49+
* Supports:
50+
* - MSSQL - (Will be compiled as self::Update)
51+
* - MySQL - (Will be compiled as self::Update)
52+
* - SQLITE -
53+
* - PostgreSQL +
54+
*/
55+
case NoKeyUpdate;
56+
}

0 commit comments

Comments
 (0)