From 3bcd86216cdf41339820909b0441d01e547af89f Mon Sep 17 00:00:00 2001 From: Taka Oyama Date: Wed, 19 Jul 2023 17:10:05 +0900 Subject: [PATCH] feat: add Connection methods selectWithOptions and cursorWithOptions --- CHANGELOG.md | 4 ++ src/Concerns/ManagesStaleReads.php | 32 +++------ src/Connection.php | 63 ++++++++++++----- src/Query/Builder.php | 12 ++-- tests/ConnectionTest.php | 106 ++++++++++++++++++++++++++++- tests/Query/BuilderTest.php | 84 ----------------------- 6 files changed, 170 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8f54c9b..945ef2af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # v6.0.0 [Not released Yet] +Added +- Deprecation warnings to `Connection`'s methods `cursorWithTimestampBound` `selectWithTimestampBound` `selectOneWithTimestampBound`. Use `cursorWithOptions` `selectWithOptions` instead. (#122) +- `Connection` has new methods `selectWithOptions` `cursorWithOptions` which allows spanner specific options to be set for each query. (#122) + Changed - [Breaking] Match `Query\Builder::forceIndex()` behavior with laravel's (`forceIndex` property no longer exists). (#114) diff --git a/src/Concerns/ManagesStaleReads.php b/src/Concerns/ManagesStaleReads.php index c2df1d85..4bdf25a2 100644 --- a/src/Concerns/ManagesStaleReads.php +++ b/src/Concerns/ManagesStaleReads.php @@ -19,11 +19,14 @@ use Colopl\Spanner\TimestampBound\TimestampBoundInterface; use Generator; -use Throwable; +/** + * @deprecated This trait will be removed in v7. + */ trait ManagesStaleReads { /** + * @deprecated use selectWithOptions() instead. This method will be removed in v7. * @param string $query * @param array $bindings * @param TimestampBoundInterface|null $timestampBound @@ -31,25 +34,11 @@ trait ManagesStaleReads */ public function cursorWithTimestampBound($query, $bindings = [], TimestampBoundInterface $timestampBound = null): Generator { - return $this->run($query, $bindings, function ($query, $bindings) use ($timestampBound) { - if ($this->pretending()) { - return call_user_func(function() { - yield from []; - }); - } - - $options = ['parameters' => $this->prepareBindings($bindings)]; - if ($timestampBound) { - $options = array_merge($options, $timestampBound->transactionOptions()); - } - - return $this->getSpannerDatabase() - ->execute($query, $options) - ->rows(); - }); + return $this->cursorWithOptions($query, $bindings, $timestampBound?->transactionOptions() ?? []); } /** + * @deprecated use selectWithOptions() instead. This method will be removed in v7. * @param string $query * @param array $bindings * @param TimestampBoundInterface|null $timestampBound @@ -57,12 +46,11 @@ public function cursorWithTimestampBound($query, $bindings = [], TimestampBoundI */ public function selectWithTimestampBound($query, $bindings = [], TimestampBoundInterface $timestampBound = null): array { - return $this->withSessionNotFoundHandling(function () use ($query, $bindings, $timestampBound) { - return iterator_to_array($this->cursorWithTimestampBound($query, $bindings, $timestampBound)); - }); + return $this->selectWithOptions($query, $bindings, $timestampBound?->transactionOptions() ?? []); } /** + * @deprecated use selectWithOptions() instead. This method will be removed in v7. * @param string $query * @param array $bindings * @param TimestampBoundInterface|null $timestampBound @@ -70,9 +58,7 @@ public function selectWithTimestampBound($query, $bindings = [], TimestampBoundI */ public function selectOneWithTimestampBound($query, $bindings = [], TimestampBoundInterface $timestampBound = null): ?array { - return $this->withSessionNotFoundHandling(function () use ($query, $bindings, $timestampBound) { - return $this->cursorWithTimestampBound($query, $bindings, $timestampBound)->current(); - }); + return $this->cursorWithTimestampBound($query, $bindings, $timestampBound)->current(); } } diff --git a/src/Connection.php b/src/Connection.php index b475726a..f3a44817 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -256,17 +256,7 @@ public function query(): QueryBuilder */ public function select($query, $bindings = [], $useReadPdo = true): array { - return $this->run($query, $bindings, function ($query, $bindings) { - if ($this->pretending()) { - return []; - } - - $generator = $this->getDatabaseContext() - ->execute($query, ['parameters' => $this->prepareBindings($bindings)]) - ->rows(); - - return iterator_to_array($generator); - }); + return $this->selectWithOptions($query, $bindings, []); } /** @@ -274,14 +264,36 @@ public function select($query, $bindings = [], $useReadPdo = true): array */ public function cursor($query, $bindings = [], $useReadPdo = true): Generator { - return $this->run($query, $bindings, function ($query, $bindings) { - if ($this->pretending()) { - return call_user_func(function() { yield from []; }); - } + return $this->cursorWithOptions($query, $bindings, []); + } - return $this->getDatabaseContext() - ->execute($query, ['parameters' => $this->prepareBindings($bindings)]) - ->rows(); + /** + * @param string $query + * @param array $bindings + * @param array $options + * @return array> + */ + public function selectWithOptions(string $query, array $bindings, array $options): array + { + return $this->run($query, $bindings, function ($query, $bindings) use ($options): array { + return !$this->pretending() + ? iterator_to_array($this->executeQuery($query, $bindings, $options)) + : []; + }); + } + + /** + * @param string $query + * @param array $bindings + * @param array $options + * @return Generator> + */ + public function cursorWithOptions(string $query, array $bindings, array $options): Generator + { + return $this->run($query, $bindings, function ($query, $bindings) use ($options): Generator { + return !$this->pretending() + ? $this->executeQuery($query, $bindings, $options) + : (static fn() => yield from [])(); }); } @@ -522,6 +534,21 @@ protected function withSessionNotFoundHandling(Closure $callback): mixed } } + /** + * @param string $query + * @param array $bindings + * @param array $options + * @return Generator> + */ + protected function executeQuery(string $query, array $bindings, array $options): Generator + { + $options += ['parameters' => $this->prepareBindings($bindings)]; + + return $this->getDatabaseContext() + ->execute($query, $options) + ->rows(); + } + /** * Check if this is "session not found" error * diff --git a/src/Query/Builder.php b/src/Query/Builder.php index 342f6d05..1918ce6a 100644 --- a/src/Query/Builder.php +++ b/src/Query/Builder.php @@ -132,12 +132,16 @@ protected function prepareInsertForDml($values) */ protected function runSelect() { + $sql = $this->toSql(); + $bindings = $this->getBindings(); + $options = []; + if ($this->timestampBound !== null) { - return $this->connection->selectWithTimestampBound( - $this->toSql(), $this->getBindings(), $this->timestampBound - ); + $options += $this->timestampBound->transactionOptions(); } - return parent::runSelect(); + return count($options) > 0 + ? $this->connection->selectWithOptions($sql, $bindings, $options) + : $this->connection->select($sql, $bindings); } } diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 928c420a..5ac707f7 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -25,9 +25,11 @@ use Colopl\Spanner\TimestampBound\MinReadTimestamp; use Colopl\Spanner\TimestampBound\ReadTimestamp; use Colopl\Spanner\TimestampBound\StrongRead; +use Generator; use Google\Auth\FetchAuthTokenInterface; use Google\Cloud\Core\Exception\AbortedException; use Google\Cloud\Core\Exception\NotFoundException; +use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Session\CacheSessionPool; use Google\Cloud\Spanner\SpannerClient; @@ -38,7 +40,6 @@ use Illuminate\Database\Events\TransactionCommitted; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Event; -use LogicException; use RuntimeException; use Symfony\Component\Cache\Adapter\ArrayAdapter; use function dirname; @@ -60,7 +61,7 @@ public function testReconnect(): void { $conn = $this->getDefaultConnection(); $conn->reconnect(); - $this->assertEquals([12345], $conn->selectOne('SELECT 12345')); + $this->assertSame([12345], $conn->selectOne('SELECT 12345')); } public function testQueryLog(): void @@ -75,6 +76,107 @@ public function testQueryLog(): void $this->assertCount(2, $conn->getQueryLog()); } + public function test_select(): void + { + $conn = $this->getDefaultConnection(); + $values = $conn->select('SELECT 12345'); + $this->assertCount(1, $values); + $this->assertSame(12345, $values[0][0]); + } + + public function test_selectWithOptions(): void + { + $conn = $this->getDefaultConnection(); + $conn->table(self::TABLE_NAME_USER)->insert(['userId' => $this->generateUuid(), 'name' => __FUNCTION__]); + $values = $conn->selectWithOptions('SELECT * FROM ' . self::TABLE_NAME_USER, [], ['exactStaleness' => new Duration(10)]); + $this->assertEmpty($values); + } + + public function test_cursorWithOptions(): void + { + $conn = $this->getDefaultConnection(); + $conn->table(self::TABLE_NAME_USER)->insert(['userId' => $this->generateUuid(), 'name' => __FUNCTION__]); + $cursor = $conn->cursorWithOptions('SELECT * FROM ' . self::TABLE_NAME_USER, [], ['exactStaleness' => new Duration(10)]); + $this->assertInstanceOf(Generator::class, $cursor); + $this->assertNull($cursor->current()); + } + + public function test_statement_with_select(): void + { + $executedCount = 0; + $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); + + $conn = $this->getDefaultConnection(); + $res = $conn->statement('SELECT ?', ['12345']); + + $this->assertTrue($res); + $this->assertSame(1, $executedCount); + } + + public function test_statement_with_dml(): void + { + $conn = $this->getDefaultConnection(); + $userId = $this->generateUuid(); + $executedCount = 0; + $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); + + $res[] = $conn->statement('INSERT '.self::TABLE_NAME_USER.' (`userId`, `name`) VALUES (?,?)', [$userId, __FUNCTION__]); + $res[] = $conn->statement('UPDATE '.self::TABLE_NAME_USER.' SET `name`=? WHERE `userId`=?', [__FUNCTION__.'2', $userId]); + $res[] = $conn->statement('DELETE '.self::TABLE_NAME_USER.' WHERE `userId`=?', [$this->generateUuid()]); + + $this->assertTrue($res[0]); + $this->assertTrue($res[1]); + $this->assertTrue($res[2]); + $this->assertSame(3, $executedCount); + } + + public function test_unprepared_with_select(): void + { + $executedCount = 0; + $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); + + $conn = $this->getDefaultConnection(); + $res = $conn->unprepared('SELECT 12345'); + + $this->assertTrue($res); + $this->assertSame(1, $executedCount); + } + + public function test_unprepared_with_dml(): void + { + $conn = $this->getDefaultConnection(); + $userId = $this->generateUuid(); + $executedCount = 0; + $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); + + $res[] = $conn->unprepared('INSERT '.self::TABLE_NAME_USER.' (`userId`, `name`) VALUES (\''.$userId.'\',\''.__FUNCTION__.'\')'); + $res[] = $conn->unprepared('UPDATE '.self::TABLE_NAME_USER.' SET `name`=\''.__FUNCTION__.'2'.'\' WHERE `userId`=\''.$userId.'\''); + $res[] = $conn->unprepared('DELETE '.self::TABLE_NAME_USER.' WHERE `userId`=\''.$userId.'\''); + + $this->assertTrue($res[0]); + $this->assertTrue($res[1]); + $this->assertTrue($res[2]); + $this->assertSame(3, $executedCount); + } + + public function test_pretend(): void + { + $executedCount = 0; + $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); + + $resSelect = null; + $resInsert = null; + $conn = $this->getDefaultConnection(); + $conn->pretend(function(Connection $conn) use (&$resSelect, &$resInsert) { + $resSelect = $conn->select('SELECT 12345'); + $resInsert = $conn->table(self::TABLE_NAME_USER)->insert(['userId' => $this->generateUuid(), 'name' => __FUNCTION__]); + }); + + $this->assertSame([], $resSelect); + $this->assertTrue($resInsert); + $this->assertSame(2, $executedCount); + } + public function testInsertUsingMutationWithTransaction(): void { Event::fake(); diff --git a/tests/Query/BuilderTest.php b/tests/Query/BuilderTest.php index d350b60f..a82d12b6 100644 --- a/tests/Query/BuilderTest.php +++ b/tests/Query/BuilderTest.php @@ -33,14 +33,6 @@ class BuilderTest extends TestCase { - public function testSimpleSelect(): void - { - $conn = $this->getDefaultConnection(); - $values = $conn->select('SELECT 12345'); - $this->assertCount(1, $values); - $this->assertEquals(12345, $values[0][0]); - } - public function testInsert(): void { $conn = $this->getDefaultConnection(); @@ -126,82 +118,6 @@ public function testDelete(): void $this->assertNull($insertedRow); } - public function testStatementWithSelect(): void - { - $executedCount = 0; - $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); - - $conn = $this->getDefaultConnection(); - $res = $conn->statement('SELECT ?', ['12345']); - - $this->assertTrue($res); - $this->assertEquals(1, $executedCount); - } - - public function testStatementWithDml(): void - { - $conn = $this->getDefaultConnection(); - $userId = $this->generateUuid(); - $executedCount = 0; - $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); - - $res[] = $conn->statement('INSERT '.self::TABLE_NAME_USER.' (`userId`, `name`) VALUES (?,?)', [$userId, __FUNCTION__]); - $res[] = $conn->statement('UPDATE '.self::TABLE_NAME_USER.' SET `name`=? WHERE `userId`=?', [__FUNCTION__.'2', $userId]); - $res[] = $conn->statement('DELETE '.self::TABLE_NAME_USER.' WHERE `userId`=?', [$this->generateUuid()]); - - $this->assertTrue($res[0]); - $this->assertTrue($res[1]); - $this->assertTrue($res[2]); - $this->assertEquals(3, $executedCount); - } - - public function testUnpreparedWithSelect(): void - { - $executedCount = 0; - $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); - - $conn = $this->getDefaultConnection(); - $res = $conn->unprepared('SELECT 12345'); - - $this->assertTrue($res); - $this->assertEquals(1, $executedCount); - } - - public function testUnpreparedWithDml(): void - { - $conn = $this->getDefaultConnection(); - $userId = $this->generateUuid(); - $executedCount = 0; - $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); - - $res[] = $conn->unprepared('INSERT '.self::TABLE_NAME_USER.' (`userId`, `name`) VALUES (\''.$userId.'\',\''.__FUNCTION__.'\')'); - $res[] = $conn->unprepared('UPDATE '.self::TABLE_NAME_USER.' SET `name`=\''.__FUNCTION__.'2'.'\' WHERE `userId`=\''.$userId.'\''); - $res[] = $conn->unprepared('DELETE '.self::TABLE_NAME_USER.' WHERE `userId`=\''.$userId.'\''); - - $this->assertTrue($res[0]); - $this->assertTrue($res[1]); - $this->assertTrue($res[2]); - $this->assertEquals(3, $executedCount); - } - - public function testPretend(): void - { - $executedCount = 0; - $this->app['events']->listen(QueryExecuted::class, function () use (&$executedCount) { $executedCount++; }); - - $resSelect = null; - $resInsert = null; - $conn = $this->getDefaultConnection(); - $conn->pretend(function(Connection $conn) use (&$resSelect, &$resInsert) { - $resSelect = $conn->select('SELECT 12345'); - $resInsert = $conn->table(self::TABLE_NAME_USER)->insert(['userId' => $this->generateUuid(), 'name' => __FUNCTION__]); - }); - - $this->assertEquals([], $resSelect); - $this->assertEquals(true, $resInsert); - $this->assertEquals(2, $executedCount); - } - public function testCompositePrimaryKeyTest(): void { $conn = $this->getDefaultConnection();