Skip to content

Commit 7180366

Browse files
taka-oyamahalnique
andcommitted
feat: add Connection methods selectWithOptions and cursorWithOptions (#122)
Co-authored-by: halnique <shunsuke4dev@gmail.com>
1 parent b2aaac6 commit 7180366

File tree

7 files changed

+173
-133
lines changed

7 files changed

+173
-133
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# v6.0.0 [Not released Yet]
22

3+
Added
4+
- Deprecation warnings to `Connection`'s methods `cursorWithTimestampBound` `selectWithTimestampBound` `selectOneWithTimestampBound`. Use `cursorWithOptions` `selectWithOptions` instead. (#122)
5+
- `Connection` has new methods `selectWithOptions` `cursorWithOptions` which allows spanner specific options to be set for each query. (#122)
6+
37
Changed
48
- [Breaking] Match `Query\Builder::forceIndex()` behavior with laravel's (`forceIndex` property no longer exists). (#114)
59

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ parameters:
1919
count: 2
2020
- message: "#^Parameter \\#1 \\$table of method Illuminate\\\\Database\\\\Query\\\\Builder\\:\\:from\\(\\) expects Closure\\|Illuminate\\\\Database\\\\Eloquent\\\\Builder\\|Illuminate\\\\Database\\\\Query\\\\Builder\\|string, Closure\\|Illuminate\\\\Contracts\\\\Database\\\\Query\\\\Expression\\|Illuminate\\\\Database\\\\Query\\\\Builder\\|string given\\.$#"
2121
path: src/Connection.php
22-
- message: '#^Method Colopl\\Spanner\\Connection::select\(\) should return array but returns mixed\.$#'
22+
- message: '#^Method Colopl\\Spanner\\Connection::selectWithOptions\(\) should return array<int, array> but returns mixed\.$#'
2323
path: src/Connection.php
2424
- message: '#^Cannot cast mixed to int\.$#'
2525
path: src/Eloquent/Model.php

src/Concerns/ManagesStaleReads.php

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,56 +20,45 @@
2020
use Colopl\Spanner\TimestampBound\TimestampBoundInterface;
2121
use Generator;
2222

23+
/**
24+
* @deprecated This trait will be removed in v7.
25+
*/
2326
trait ManagesStaleReads
2427
{
2528
/**
29+
* @deprecated use selectWithOptions() instead. This method will be removed in v7.
2630
* @param string $query
2731
* @param array<array-key, mixed> $bindings
2832
* @param TimestampBoundInterface|null $timestampBound
29-
* @return Generator<int, list<mixed>|null>
33+
* @return Generator<int, array<array-key, mixed>>
3034
*/
3135
public function cursorWithTimestampBound($query, $bindings = [], TimestampBoundInterface $timestampBound = null): Generator
3236
{
33-
return $this->run($query, $bindings, function ($query, $bindings) use ($timestampBound) {
34-
if ($this->pretending()) {
35-
return (static fn() => yield from [])();
36-
}
37-
38-
$options = ['parameters' => $this->prepareBindings($bindings)];
39-
if ($timestampBound) {
40-
$options = array_merge($options, $timestampBound->transactionOptions());
41-
}
42-
43-
return $this->getSpannerDatabase()
44-
->execute($query, $options)
45-
->rows();
46-
});
37+
return $this->cursorWithOptions($query, $bindings, $timestampBound?->transactionOptions() ?? []);
4738
}
4839

4940
/**
50-
* @param string $query
51-
* @param array<array-key, mixed> $bindings
52-
* @param TimestampBoundInterface|null $timestampBound
41+
* @deprecated use selectWithOptions() instead. This method will be removed in v7.
42+
* @param string $query
43+
* @param array<array-key, mixed> $bindings
44+
* @param TimestampBoundInterface|null $timestampBound
5345
* @return list<array<array-key, mixed>|null>
5446
*/
5547
public function selectWithTimestampBound($query, $bindings = [], TimestampBoundInterface $timestampBound = null): array
5648
{
57-
return $this->withSessionNotFoundHandling(function () use ($query, $bindings, $timestampBound) {
58-
return iterator_to_array($this->cursorWithTimestampBound($query, $bindings, $timestampBound));
59-
});
49+
return $this->selectWithOptions($query, $bindings, $timestampBound?->transactionOptions() ?? []);
6050
}
6151

6252
/**
53+
* @deprecated use selectWithOptions() instead. This method will be removed in v7.
6354
* @param string $query
6455
* @param array<array-key, mixed> $bindings
6556
* @param TimestampBoundInterface|null $timestampBound
6657
* @return array<array-key, mixed>|null
6758
*/
6859
public function selectOneWithTimestampBound($query, $bindings = [], TimestampBoundInterface $timestampBound = null): ?array
6960
{
70-
return $this->withSessionNotFoundHandling(function () use ($query, $bindings, $timestampBound) {
71-
return $this->cursorWithTimestampBound($query, $bindings, $timestampBound)->current();
72-
});
61+
return $this->selectWithTimestampBound($query, $bindings, $timestampBound)[0] ?? null;
7362
}
7463
}
7564

src/Connection.php

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -256,32 +256,44 @@ public function query(): QueryBuilder
256256
*/
257257
public function select($query, $bindings = [], $useReadPdo = true): array
258258
{
259-
return $this->run($query, $bindings, function ($query, $bindings) {
260-
if ($this->pretending()) {
261-
return [];
262-
}
263-
264-
$generator = $this->getDatabaseContext()
265-
->execute($query, ['parameters' => $this->prepareBindings($bindings)])
266-
->rows();
267-
268-
return iterator_to_array($generator);
269-
});
259+
return $this->selectWithOptions($query, $bindings, []);
270260
}
271261

272262
/**
273263
* @inheritDoc
274264
*/
275265
public function cursor($query, $bindings = [], $useReadPdo = true): Generator
276266
{
277-
return $this->run($query, $bindings, function ($query, $bindings) {
278-
if ($this->pretending()) {
279-
return (static fn() => yield from [])();
280-
}
267+
return $this->cursorWithOptions($query, $bindings, []);
268+
}
281269

282-
return $this->getDatabaseContext()
283-
->execute($query, ['parameters' => $this->prepareBindings($bindings)])
284-
->rows();
270+
/**
271+
* @param string $query
272+
* @param array<array-key, mixed> $bindings
273+
* @param array<string, mixed> $options
274+
* @return array<int, array<array-key, mixed>>
275+
*/
276+
public function selectWithOptions(string $query, array $bindings, array $options): array
277+
{
278+
return $this->run($query, $bindings, function ($query, $bindings) use ($options): array {
279+
return !$this->pretending()
280+
? iterator_to_array($this->executeQuery($query, $bindings, $options))
281+
: [];
282+
});
283+
}
284+
285+
/**
286+
* @param string $query
287+
* @param array<array-key, mixed> $bindings
288+
* @param array<string, mixed> $options
289+
* @return Generator<int, array<array-key, mixed>>
290+
*/
291+
public function cursorWithOptions(string $query, array $bindings, array $options): Generator
292+
{
293+
return $this->run($query, $bindings, function ($query, $bindings) use ($options): Generator {
294+
return !$this->pretending()
295+
? $this->executeQuery($query, $bindings, $options)
296+
: (static fn() => yield from [])();
285297
});
286298
}
287299

@@ -569,6 +581,21 @@ protected function withSessionNotFoundHandling(Closure $callback): mixed
569581
}
570582
}
571583

584+
/**
585+
* @param string $query
586+
* @param array<array-key, mixed> $bindings
587+
* @param array<string, mixed> $options
588+
* @return Generator<int, array<array-key, mixed>>
589+
*/
590+
protected function executeQuery(string $query, array $bindings, array $options): Generator
591+
{
592+
$options += ['parameters' => $this->prepareBindings($bindings)];
593+
594+
return $this->getDatabaseContext()
595+
->execute($query, $options)
596+
->rows();
597+
}
598+
572599
/**
573600
* Check if this is "session not found" error
574601
*

src/Query/Builder.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,14 @@ protected function prepareInsertForDml($values)
132132
*/
133133
protected function runSelect()
134134
{
135+
$sql = $this->toSql();
136+
$bindings = $this->getBindings();
137+
$options = [];
138+
135139
if ($this->timestampBound !== null) {
136-
return $this->connection->selectWithTimestampBound(
137-
$this->toSql(), $this->getBindings(), $this->timestampBound
138-
);
140+
$options += $this->timestampBound->transactionOptions();
139141
}
140142

141-
return parent::runSelect();
143+
return $this->connection->selectWithOptions($sql, $bindings, $options);
142144
}
143145
}

tests/ConnectionTest.php

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
use Colopl\Spanner\TimestampBound\MinReadTimestamp;
2727
use Colopl\Spanner\TimestampBound\ReadTimestamp;
2828
use Colopl\Spanner\TimestampBound\StrongRead;
29+
use Generator;
2930
use Google\Auth\FetchAuthTokenInterface;
3031
use Google\Cloud\Core\Exception\AbortedException;
3132
use Google\Cloud\Core\Exception\NotFoundException;
33+
use Google\Cloud\Spanner\Duration;
3234
use Google\Cloud\Spanner\KeySet;
3335
use Google\Cloud\Spanner\Session\CacheSessionPool;
3436
use Google\Cloud\Spanner\SpannerClient;
@@ -39,7 +41,6 @@
3941
use Illuminate\Database\Events\TransactionCommitted;
4042
use Illuminate\Support\Carbon;
4143
use Illuminate\Support\Facades\Event;
42-
use LogicException;
4344
use RuntimeException;
4445
use Symfony\Component\Cache\Adapter\ArrayAdapter;
4546
use function dirname;
@@ -61,7 +62,7 @@ public function testReconnect(): void
6162
{
6263
$conn = $this->getDefaultConnection();
6364
$conn->reconnect();
64-
$this->assertEquals([12345], $conn->selectOne('SELECT 12345'));
65+
$this->assertSame([12345], $conn->selectOne('SELECT 12345'));
6566
}
6667

6768
public function testQueryLog(): void
@@ -76,6 +77,107 @@ public function testQueryLog(): void
7677
$this->assertCount(2, $conn->getQueryLog());
7778
}
7879

80+
public function test_select(): void
81+
{
82+
$conn = $this->getDefaultConnection();
83+
$values = $conn->select('SELECT 12345');
84+
$this->assertCount(1, $values);
85+
$this->assertSame(12345, $values[0][0]);
86+
}
87+
88+
public function test_selectWithOptions(): void
89+
{
90+
$conn = $this->getDefaultConnection();
91+
$conn->table(self::TABLE_NAME_USER)->insert(['userId' => $this->generateUuid(), 'name' => __FUNCTION__]);
92+
$values = $conn->selectWithOptions('SELECT * FROM ' . self::TABLE_NAME_USER, [], ['exactStaleness' => new Duration(10)]);
93+
$this->assertEmpty($values);
94+
}
95+
96+
public function test_cursorWithOptions(): void
97+
{
98+
$conn = $this->getDefaultConnection();
99+
$conn->table(self::TABLE_NAME_USER)->insert(['userId' => $this->generateUuid(), 'name' => __FUNCTION__]);
100+
$cursor = $conn->cursorWithOptions('SELECT * FROM ' . self::TABLE_NAME_USER, [], ['exactStaleness' => new Duration(10)]);
101+
$this->assertInstanceOf(Generator::class, $cursor);
102+
$this->assertNull($cursor->current());
103+
}
104+
105+
public function test_statement_with_select(): void
106+
{
107+
$executedCount = 0;
108+
$this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; });
109+
110+
$conn = $this->getDefaultConnection();
111+
$res = $conn->statement('SELECT ?', ['12345']);
112+
113+
$this->assertTrue($res);
114+
$this->assertSame(1, $executedCount);
115+
}
116+
117+
public function test_statement_with_dml(): void
118+
{
119+
$conn = $this->getDefaultConnection();
120+
$userId = $this->generateUuid();
121+
$executedCount = 0;
122+
$this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; });
123+
124+
$res[] = $conn->statement('INSERT '.self::TABLE_NAME_USER.' (`userId`, `name`) VALUES (?,?)', [$userId, __FUNCTION__]);
125+
$res[] = $conn->statement('UPDATE '.self::TABLE_NAME_USER.' SET `name`=? WHERE `userId`=?', [__FUNCTION__.'2', $userId]);
126+
$res[] = $conn->statement('DELETE '.self::TABLE_NAME_USER.' WHERE `userId`=?', [$this->generateUuid()]);
127+
128+
$this->assertTrue($res[0]);
129+
$this->assertTrue($res[1]);
130+
$this->assertTrue($res[2]);
131+
$this->assertSame(3, $executedCount);
132+
}
133+
134+
public function test_unprepared_with_select(): void
135+
{
136+
$executedCount = 0;
137+
$this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; });
138+
139+
$conn = $this->getDefaultConnection();
140+
$res = $conn->unprepared('SELECT 12345');
141+
142+
$this->assertTrue($res);
143+
$this->assertSame(1, $executedCount);
144+
}
145+
146+
public function test_unprepared_with_dml(): void
147+
{
148+
$conn = $this->getDefaultConnection();
149+
$userId = $this->generateUuid();
150+
$executedCount = 0;
151+
$this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; });
152+
153+
$res[] = $conn->unprepared('INSERT '.self::TABLE_NAME_USER.' (`userId`, `name`) VALUES (\''.$userId.'\',\''.__FUNCTION__.'\')');
154+
$res[] = $conn->unprepared('UPDATE '.self::TABLE_NAME_USER.' SET `name`=\''.__FUNCTION__.'2'.'\' WHERE `userId`=\''.$userId.'\'');
155+
$res[] = $conn->unprepared('DELETE '.self::TABLE_NAME_USER.' WHERE `userId`=\''.$userId.'\'');
156+
157+
$this->assertTrue($res[0]);
158+
$this->assertTrue($res[1]);
159+
$this->assertTrue($res[2]);
160+
$this->assertSame(3, $executedCount);
161+
}
162+
163+
public function test_pretend(): void
164+
{
165+
$executedCount = 0;
166+
$this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; });
167+
168+
$resSelect = null;
169+
$resInsert = null;
170+
$conn = $this->getDefaultConnection();
171+
$conn->pretend(function(Connection $conn) use (&$resSelect, &$resInsert) {
172+
$resSelect = $conn->select('SELECT 12345');
173+
$resInsert = $conn->table(self::TABLE_NAME_USER)->insert(['userId' => $this->generateUuid(), 'name' => __FUNCTION__]);
174+
});
175+
176+
$this->assertSame([], $resSelect);
177+
$this->assertTrue($resInsert);
178+
$this->assertSame(2, $executedCount);
179+
}
180+
79181
public function testInsertUsingMutationWithTransaction(): void
80182
{
81183
Event::fake();

0 commit comments

Comments
 (0)