Skip to content

Commit 41ff6b0

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

8 files changed

Lines changed: 111 additions & 7 deletions

File tree

src/Driver/Compiler.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Cycle\Database\Injection\Parameter;
1717
use Cycle\Database\Injection\ParameterInterface;
1818
use Cycle\Database\Query\QueryParameters;
19+
use Cycle\Database\Query\SelectQuery;
1920

2021
abstract class Compiler implements CompilerInterface
2122
{
@@ -183,6 +184,13 @@ protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens
183184
$this->nameWithAlias(new QueryParameters(), $q, $join['outer'], $join['alias'], true);
184185
}
185186

187+
$forUpdate = match ($tokens['forUpdate']) {
188+
SelectQuery::FOR_UPDATE => 'FOR UPDATE',
189+
SelectQuery::FOR_UPDATE_NOWAIT => 'FOR UPDATE NOWAIT',
190+
SelectQuery::FOR_UPDATE_SKIP_LOCKED => 'FOR UPDATE SKIP LOCKED',
191+
default => '',
192+
};
193+
186194
return \sprintf(
187195
"SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s%s%s",
188196
$this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])),
@@ -197,7 +205,7 @@ protected function selectQuery(QueryParameters $params, Quoter $q, array $tokens
197205
$this->optional("\n", $this->excepts($params, $q, $tokens['except'])),
198206
$this->optional("\nORDER BY", $this->orderBy($params, $q, $tokens['orderBy'])),
199207
$this->optional("\n", $this->limit($params, $q, $tokens['limit'], $tokens['offset'])),
200-
$this->optional(' ', $tokens['forUpdate'] ? 'FOR UPDATE' : ''),
208+
$this->optional(' ', $forUpdate),
201209
);
202210
}
203211

src/Driver/SQLServer/SQLServerCompiler.php

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

1414
use Cycle\Database\Driver\Compiler;
1515
use Cycle\Database\Driver\Quoter;
16+
use Cycle\Database\Query\SelectQuery;
1617
use Cycle\Database\Driver\SQLServer\Injection\CompileJson;
1718
use Cycle\Database\Injection\Fragment;
1819
use Cycle\Database\Injection\FragmentInterface;
@@ -175,12 +176,19 @@ private function baseSelect(QueryParameters $params, Quoter $q, array $tokens):
175176
$this->nameWithAlias(new QueryParameters(), $q, $join['outer'], $join['alias'], true);
176177
}
177178

179+
$forUpdate = match ($tokens['forUpdate']) {
180+
SelectQuery::FOR_UPDATE => 'WITH(UPDLOCK,ROWLOCK)',
181+
SelectQuery::FOR_UPDATE_SKIP_LOCKED => 'WITH(UPDLOCK,READPAST)',
182+
SelectQuery::FOR_UPDATE_NOWAIT => 'WITH(UPDLOCK,ROWLOCK,NOWAIT)',
183+
default => '',
184+
};
185+
178186
return \sprintf(
179187
"SELECT%s %s\nFROM %s%s%s%s%s%s%s%s%s%s%s",
180188
$this->optional(' ', $this->distinct($params, $q, $tokens['distinct'])),
181189
$this->columns($params, $q, $tokens['columns']),
182190
\implode(', ', $tables),
183-
$this->optional(' ', $tokens['forUpdate'] ? 'WITH (UPDLOCK,ROWLOCK)' : '', ' '),
191+
$this->optional(' ', $forUpdate, ' '),
184192
$this->optional(' ', $this->joins($params, $q, $tokens['join']), ' '),
185193
$this->optional("\nWHERE", $this->where($params, $q, $tokens['where'])),
186194
$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/SelectQuery.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ class SelectQuery extends ActiveQuery implements
4242
public const SORT_ASC = 'ASC';
4343
public const SORT_DESC = 'DESC';
4444

45+
// for update values
46+
public const FOR_UPDATE = 'FOR_UPDATE';
47+
public const FOR_UPDATE_NOWAIT = 'FOR_UPDATE_NOWAIT';
48+
public const FOR_UPDATE_SKIP_LOCKED = 'FOR_UPDATE_SKIP_LOCKED';
49+
4550
protected array $tables = [];
4651
protected array $unionTokens = [];
4752
protected array $exceptTokens = [];
@@ -53,7 +58,7 @@ class SelectQuery extends ActiveQuery implements
5358
protected array $orderBy = [];
5459

5560
protected array $groupBy = [];
56-
protected bool $forUpdate = false;
61+
protected ?string $forUpdate = null;
5762
private ?int $limit = null;
5863
private ?int $offset = null;
5964

@@ -126,10 +131,12 @@ public function getColumns(): array
126131

127132
/**
128133
* Select entities for the following update.
134+
*
135+
* @param self::FOR_UPDATE|self::FOR_UPDATE_NOWAIT|self::FOR_UPDATE_SKIP_LOCKED|null $forUpdate
129136
*/
130-
public function forUpdate(): self
137+
public function forUpdate(?string $forUpdate = self::FOR_UPDATE): self
131138
{
132-
$this->forUpdate = true;
139+
$this->forUpdate = $forUpdate;
133140

134141
return $this;
135142
}

tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,32 @@ public function testSelectForUpdate(): void
21672167
);
21682168
}
21692169

2170+
public function testSelectForUpdateNoWait(): void
2171+
{
2172+
$select = $this->database->select()
2173+
->from(['users'])
2174+
->where('name', 'Antony')
2175+
->forUpdate(SelectQuery::FOR_UPDATE_NOWAIT);
2176+
2177+
$this->assertSameQuery(
2178+
'SELECT * FROM {users} WHERE {name} = ? FOR UPDATE NOWAIT',
2179+
$select,
2180+
);
2181+
}
2182+
2183+
public function testSelectForUpdateSkipLocked(): void
2184+
{
2185+
$select = $this->database->select()
2186+
->from(['users'])
2187+
->where('name', 'Antony')
2188+
->forUpdate(SelectQuery::FOR_UPDATE_SKIP_LOCKED);
2189+
2190+
$this->assertSameQuery(
2191+
'SELECT * FROM {users} WHERE {name} = ? FOR UPDATE SKIP LOCKED',
2192+
$select,
2193+
);
2194+
}
2195+
21702196
public function testSelectWithParametricExpression(): void
21712197
{
21722198
$select = $this->database->select()

tests/Database/Functional/Driver/SQLServer/Query/SelectQueryTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Cycle\Database\Tests\Functional\Driver\SQLServer\Query;
66

77
// phpcs:ignore
8+
use Cycle\Database\Query\SelectQuery;
89
use Cycle\Database\Tests\Functional\Driver\Common\Query\SelectQueryTest as CommonClass;
910

1011
/**
@@ -113,6 +114,32 @@ public function testSelectForUpdate(): void
113114
);
114115
}
115116

117+
public function testSelectForUpdateNoWait(): void
118+
{
119+
$select = $this->database->select()
120+
->from(['users'])
121+
->where('name', 'Antony')
122+
->forUpdate(SelectQuery::FOR_UPDATE_NOWAIT);
123+
124+
$this->assertSameQuery(
125+
'SELECT * FROM {users} WITH(UPDLOCK,ROWLOCK,NOWAIT) WHERE {name} = ?',
126+
$select,
127+
);
128+
}
129+
130+
public function testSelectForUpdateSkipLocked(): void
131+
{
132+
$select = $this->database->select()
133+
->from(['users'])
134+
->where('name', 'Antony')
135+
->forUpdate(SelectQuery::FOR_UPDATE_SKIP_LOCKED);
136+
137+
$this->assertSameQuery(
138+
'SELECT * FROM {users} WITH(UPDLOCK,READPAST) WHERE {name} = ?',
139+
$select,
140+
);
141+
}
142+
116143
public function testSelectWithWhereJson(): void
117144
{
118145
$select = $this->database

tests/Database/Functional/Driver/SQLite/Query/SelectQueryTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Cycle\Database\Tests\Functional\Driver\SQLite\Query;
66

77
// phpcs:ignore
8+
use Cycle\Database\Query\SelectQuery;
89
use Cycle\Database\Tests\Functional\Driver\Common\Query\SelectQueryTest as CommonClass;
910

1011
/**
@@ -45,6 +46,32 @@ public function testSelectForUpdate(): void
4546
);
4647
}
4748

49+
public function testSelectForUpdateNoWait(): void
50+
{
51+
$select = $this->database->select()
52+
->from(['users'])
53+
->where('name', 'Antony')
54+
->forUpdate(SelectQuery::FOR_UPDATE_NOWAIT);
55+
56+
$this->assertSameQuery(
57+
'SELECT * FROM {users} WHERE {name} = ?',
58+
$select,
59+
);
60+
}
61+
62+
public function testSelectForUpdateSkipLocked(): void
63+
{
64+
$select = $this->database->select()
65+
->from(['users'])
66+
->where('name', 'Antony')
67+
->forUpdate(SelectQuery::FOR_UPDATE_SKIP_LOCKED);
68+
69+
$this->assertSameQuery(
70+
'SELECT * FROM {users} WHERE {name} = ?',
71+
$select,
72+
);
73+
}
74+
4875
public function testSelectWithWhereJson(): void
4976
{
5077
$select = $this->database

tests/Database/Unit/Query/Tokens/SelectQueryTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public function testBuildQuery(): void
1717
$select
1818
->from('table')
1919
->columns('name', 'value')
20+
->forUpdate(SelectQuery::FOR_UPDATE_SKIP_LOCKED)
2021
->where(['name' => 'Antony'])
2122
->orWhere('id', '>', 1)
2223
->orderBy('name', 'ASC')
@@ -34,7 +35,7 @@ public function testBuildQuery(): void
3435

3536
$this->assertEquals(
3637
[
37-
'forUpdate' => false,
38+
'forUpdate' => 'FOR_UPDATE_SKIP_LOCKED',
3839
'from' => ['table'],
3940
'join' => [],
4041
'columns' => ['name', 'value'],

0 commit comments

Comments
 (0)